Propagate outer type parameters of single signature types (#57403)

This commit is contained in:
Wesley Wigham
2024-03-27 09:57:09 -07:00
committed by GitHub
parent 6d0cc1beda
commit e1874f3ffe
11 changed files with 423 additions and 17 deletions

View File

@@ -967,6 +967,7 @@ import {
SignatureFlags,
SignatureKind,
singleElementArray,
SingleSignatureType,
skipOuterExpressions,
skipParentheses,
skipTrivia,
@@ -7165,7 +7166,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const abstractSignatures = filter(resolved.constructSignatures, signature => !!(signature.flags & SignatureFlags.Abstract));
if (some(abstractSignatures)) {
const types = map(abstractSignatures, getOrCreateTypeFromSignature);
const types = map(abstractSignatures, s => getOrCreateTypeFromSignature(s));
// count the number of type elements excluding abstract constructors
const typeElementCount = resolved.callSignatures.length +
(resolved.constructSignatures.length - abstractSignatures.length) +
@@ -15672,7 +15673,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return undefined;
}
function getSignatureInstantiation(signature: Signature, typeArguments: Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): Signature {
function getSignatureInstantiation(signature: Signature, typeArguments: readonly Type[] | undefined, isJavascript: boolean, inferredTypeParameters?: readonly TypeParameter[]): Signature {
const instantiatedSignature = getSignatureInstantiationWithoutFillingInTypeArguments(signature, fillMissingTypeArguments(typeArguments, signature.typeParameters, getMinTypeArgumentCount(signature.typeParameters), isJavascript));
if (inferredTypeParameters) {
const returnSignature = getSingleCallOrConstructSignature(getReturnTypeOfSignature(instantiatedSignature));
@@ -15736,6 +15737,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
);
}
function getImplementationSignature(signature: Signature) {
return signature.typeParameters ?
signature.implementationSignatureCache ||= createImplementationSignature(signature) :
signature;
}
function createImplementationSignature(signature: Signature) {
return signature.typeParameters ? instantiateSignature(signature, createTypeMapper([], [])) : signature;
}
function getBaseSignature(signature: Signature) {
const typeParameters = signature.typeParameters;
if (typeParameters) {
@@ -15757,7 +15768,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return signature;
}
function getOrCreateTypeFromSignature(signature: Signature): ObjectType {
function getOrCreateTypeFromSignature(signature: Signature, outerTypeParameters?: TypeParameter[]): ObjectType {
// There are two ways to declare a construct signature, one is by declaring a class constructor
// using the constructor keyword, and the other is declaring a bare construct signature in an
// object type literal or interface (using the new keyword). Each way of declaring a constructor
@@ -15768,7 +15779,16 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// If declaration is undefined, it is likely to be the signature of the default constructor.
const isConstructor = kind === undefined || kind === SyntaxKind.Constructor || kind === SyntaxKind.ConstructSignature || kind === SyntaxKind.ConstructorType;
const type = createObjectType(ObjectFlags.Anonymous);
// The type must have a symbol with a `Function` flag and a declaration in order to be correctly flagged as possibly containing
// type variables by `couldContainTypeVariables`
const type = createObjectType(ObjectFlags.Anonymous | ObjectFlags.SingleSignatureType, createSymbol(SymbolFlags.Function, InternalSymbolName.Function)) as SingleSignatureType;
if (signature.declaration && !nodeIsSynthesized(signature.declaration)) { // skip synthetic declarations - keeping those around could be bad, since they lack a parent pointer
type.symbol.declarations = [signature.declaration];
type.symbol.valueDeclaration = signature.declaration;
}
outerTypeParameters ||= signature.declaration && getOuterTypeParameters(signature.declaration, /*includeThisTypes*/ true);
type.outerTypeParameters = outerTypeParameters;
type.members = emptySymbols;
type.properties = emptyArray;
type.callSignatures = !isConstructor ? [signature] : emptyArray;
@@ -19749,7 +19769,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const links = getNodeLinks(declaration);
const target = type.objectFlags & ObjectFlags.Reference ? links.resolvedType! as DeferredTypeReference :
type.objectFlags & ObjectFlags.Instantiated ? type.target! : type;
let typeParameters = links.outerTypeParameters;
let typeParameters = type.objectFlags & ObjectFlags.SingleSignatureType ? (type as SingleSignatureType).outerTypeParameters : links.outerTypeParameters;
if (!typeParameters) {
// The first time an anonymous type is instantiated we compute and store a list of the type
// parameters that are in scope (and therefore potentially referenced). For type literals that
@@ -19980,6 +20000,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (type.objectFlags & ObjectFlags.InstantiationExpressionType) {
(result as InstantiationExpressionType).node = (type as InstantiationExpressionType).node;
}
if (type.objectFlags & ObjectFlags.SingleSignatureType) {
(result as SingleSignatureType).outerTypeParameters = (type as SingleSignatureType).outerTypeParameters;
}
result.target = type;
result.mapper = mapper;
result.aliasSymbol = aliasSymbol || type.aliasSymbol;
@@ -25263,6 +25286,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const result = !!(type.flags & TypeFlags.Instantiable ||
type.flags & TypeFlags.Object && !isNonGenericTopLevelType(type) && (
objectFlags & ObjectFlags.Reference && ((type as TypeReference).node || some(getTypeArguments(type as TypeReference), couldContainTypeVariables)) ||
objectFlags & ObjectFlags.SingleSignatureType && !!length((type as SingleSignatureType).outerTypeParameters) ||
objectFlags & ObjectFlags.Anonymous && type.symbol && type.symbol.flags & (SymbolFlags.Function | SymbolFlags.Method | SymbolFlags.Class | SymbolFlags.TypeLiteral | SymbolFlags.ObjectLiteral) && type.symbol.declarations ||
objectFlags & (ObjectFlags.Mapped | ObjectFlags.ReverseMapped | ObjectFlags.ObjectRestType | ObjectFlags.InstantiationExpressionType)
) ||
@@ -25622,6 +25646,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
/**
* @returns `true` if `type` has the shape `[T[0]]` where `T` is `typeParameter`
*/
function isTupleOfSelf(typeParameter: TypeParameter, type: Type) {
return isTupleType(type) && getTupleElementType(type, 0) === getIndexedAccessType(typeParameter, getNumberLiteralType(0)) && !getTypeOfPropertyOfType(type, "1" as __String);
}
function inferTypes(inferences: InferenceInfo[], originalSource: Type, originalTarget: Type, priority = InferencePriority.None, contravariant = false) {
let bivariant = false;
let propagationType: Type;
@@ -25750,6 +25781,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
inference.priority = priority;
}
if (priority === inference.priority) {
// Inferring A to [A[0]] is a zero information inference (it guarantees A becomes its constraint), but oft arises from generic argument list inferences
// By discarding it early, we can allow more fruitful results to be used instead.
if (isTupleOfSelf(inference.typeParameter, candidate)) {
return;
}
// We make contravariant inferences only if we are in a pure contravariant position,
// i.e. only if we have not descended into a bivariant position.
if (contravariant && !bivariant) {
@@ -34388,6 +34424,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
checkMode: CheckMode,
reportErrors: boolean,
containingMessageChain: (() => DiagnosticMessageChain | undefined) | undefined,
inferenceContext: InferenceContext | undefined,
): readonly Diagnostic[] | undefined {
const errorOutputContainer: { errors?: Diagnostic[]; skipLogging?: boolean; } = { errors: undefined, skipLogging: true };
if (isJsxOpeningLikeElement(node)) {
@@ -34422,7 +34459,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// If one or more arguments are still excluded (as indicated by CheckMode.SkipContextSensitive),
// we obtain the regular type of any object literal arguments because we may not have inferred complete
// parameter types yet and therefore excess property checks may yield false positives (see #17041).
const checkArgType = checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType;
const regularArgType = checkMode & CheckMode.SkipContextSensitive ? getRegularTypeOfObjectLiteral(argType) : argType;
// If this was inferred under a given inference context, we may need to instantiate the expression type to finish resolving
// the type variables in the expression.
const checkArgType = inferenceContext ? instantiateType(regularArgType, inferenceContext.nonFixingMapper) : regularArgType;
const effectiveCheckArgumentNode = getEffectiveCheckNode(arg);
if (!checkTypeRelatedToAndOptionallyElaborate(checkArgType, paramType, relation, reportErrors ? effectiveCheckArgumentNode : undefined, effectiveCheckArgumentNode, headMessage, containingMessageChain, errorOutputContainer)) {
Debug.assert(!reportErrors || !!errorOutputContainer.errors, "parameter should have errors when reporting errors");
@@ -34887,7 +34927,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (headMessage) {
chain = chainDiagnosticMessages(chain, headMessage);
}
const diags = getSignatureApplicabilityError(node, args, last, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, () => chain);
const diags = getSignatureApplicabilityError(node, args, last, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, () => chain, /*inferenceContext*/ undefined);
if (diags) {
for (const d of diags) {
if (last.declaration && candidatesForArgumentError.length > 3) {
@@ -34909,7 +34949,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let i = 0;
for (const c of candidatesForArgumentError) {
const chain = () => chainDiagnosticMessages(/*details*/ undefined, Diagnostics.Overload_0_of_1_2_gave_the_following_error, i + 1, candidates.length, signatureToString(c));
const diags = getSignatureApplicabilityError(node, args, c, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, chain);
const diags = getSignatureApplicabilityError(node, args, c, assignableRelation, CheckMode.Normal, /*reportErrors*/ true, chain, /*inferenceContext*/ undefined);
if (diags) {
if (diags.length <= min) {
min = diags.length;
@@ -34998,7 +35038,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (some(typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) {
return undefined;
}
if (getSignatureApplicabilityError(node, args, candidate, relation, CheckMode.Normal, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) {
if (getSignatureApplicabilityError(node, args, candidate, relation, CheckMode.Normal, /*reportErrors*/ false, /*containingMessageChain*/ undefined, /*inferenceContext*/ undefined)) {
candidatesForArgumentError = [candidate];
return undefined;
}
@@ -35006,7 +35046,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
const candidate = candidates[candidateIndex];
let candidate = candidates[candidateIndex];
if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) {
continue;
}
@@ -35015,7 +35055,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let inferenceContext: InferenceContext | undefined;
if (candidate.typeParameters) {
let typeArgumentTypes: Type[] | undefined;
// If we are *inside the body of candidate*, we need to create a clone of `candidate` with differing type parameter identities,
// so our inference results for this call doesn't pollute expression types referencing the outer type parameter!
if (candidate.declaration && findAncestor(node, a => a === candidate.declaration)) {
candidate = getImplementationSignature(candidate);
}
let typeArgumentTypes: readonly Type[] | undefined;
if (some(typeArguments)) {
typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false);
if (!typeArgumentTypes) {
@@ -35024,8 +35069,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
else {
inferenceContext = createInferenceContext(candidate.typeParameters, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None);
typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext);
inferenceContext = createInferenceContext(candidate.typeParameters!, candidate, /*flags*/ isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None);
// The resulting type arguments are instantiated with the inference context mapper, as the inferred types may still contain references to the inference context's
// type variables via contextual projection. These are kept generic until all inferences are locked in, so the dependencies expressed can pass constraint checks.
typeArgumentTypes = instantiateTypes(inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext), inferenceContext.nonFixingMapper);
argCheckMode |= inferenceContext.flags & InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal;
}
checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext && inferenceContext.inferredTypeParameters);
@@ -35039,7 +35086,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
else {
checkCandidate = candidate;
}
if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) {
if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined, inferenceContext)) {
// Give preference to error candidates that have no rest parameters (as they are more specific)
(candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate);
continue;
@@ -35050,7 +35097,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// round of type inference and applicability checking for this particular candidate.
argCheckMode = CheckMode.Normal;
if (inferenceContext) {
const typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext);
const typeArgumentTypes = instantiateTypes(inferTypeArguments(node, candidate, args, argCheckMode, inferenceContext), inferenceContext.mapper);
checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext.inferredTypeParameters);
// If the original signature has a generic rest type, instantiation may produce a
// signature with different arity and we need to perform another arity check.
@@ -35059,7 +35106,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
continue;
}
}
if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined)) {
if (getSignatureApplicabilityError(node, args, checkCandidate, relation, argCheckMode, /*reportErrors*/ false, /*containingMessageChain*/ undefined, inferenceContext)) {
// Give preference to error candidates that have no rest parameters (as they are more specific)
(candidatesForArgumentError || (candidatesForArgumentError = [])).push(checkCandidate);
continue;
@@ -39515,7 +39562,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
}
}
return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context));
// TODO: The signature may reference any outer inference contexts, but we map pop off and then apply new inference contexts, and thus get different inferred types.
// That this is cached on the *first* such attempt is not currently an issue, since expression types *also* get cached on the first pass. If we ever properly speculate, though,
// the cached "isolatedSignatureType" signature field absolutely needs to be included in the list of speculative caches.
return getOrCreateTypeFromSignature(instantiateSignatureInContextOf(signature, contextualSignature, context), flatMap(inferenceContexts, c => c && map(c.inferences, i => i.typeParameter)).slice());
}
}
}

View File

@@ -6221,6 +6221,7 @@ export const enum ObjectFlags {
ContainsSpread = 1 << 21, // Object literal contains spread operation
ObjectRestType = 1 << 22, // Originates in object rest declaration
InstantiationExpressionType = 1 << 23, // Originates in instantiation expression
SingleSignatureType = 1 << 27, // A single signature type extracted from a potentially broader type
/** @internal */
IsClassInstanceClone = 1 << 24, // Type is a clone of a class instance type
// Flags that require TypeFlags.Object and ObjectFlags.Reference
@@ -6431,6 +6432,12 @@ export interface AnonymousType extends ObjectType {
instantiations?: Map<string, Type>; // Instantiations of generic type alias (undefined if non-generic)
}
/** @internal */
// A SingleSignatureType may have bespoke outer type parameters to handle free type variable inferences
export interface SingleSignatureType extends AnonymousType {
outerTypeParameters?: TypeParameter[];
}
/** @internal */
export interface InstantiationExpressionType extends AnonymousType {
node: NodeWithTypeArguments;
@@ -6716,6 +6723,8 @@ export interface Signature {
isolatedSignatureType?: ObjectType; // A manufactured type that just contains the signature for purposes of signature comparison
/** @internal */
instantiations?: Map<string, Signature>; // Generic signature instantiation cache
/** @internal */
implementationSignatureCache?: Signature; // Copy of the signature with fresh type parameters to use in checking the body of a potentially self-referential generic function (deferred)
}
export const enum IndexKind {