Skip to content

Perform partial inference with partially filled type parameter lists #54047

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 56 additions & 11 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var markerSubTypeForCheck = createTypeParameter();
markerSubTypeForCheck.constraint = markerSuperTypeForCheck;

var declaredSyntheticInferType = createTypeParameter();

var noTypePredicate = createTypePredicate(TypePredicateKind.Identifier, "<<unresolved>>", 0, anyType);

var anySignature = createSignature(/*declaration*/ undefined, /*typeParameters*/ undefined, /*thisParameter*/ undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, SignatureFlags.None);
Expand Down Expand Up @@ -18472,6 +18474,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return neverType;
case SyntaxKind.ObjectKeyword:
return node.flags & NodeFlags.JavaScriptFile && !noImplicitAny ? anyType : nonPrimitiveType;
case SyntaxKind.OmittedType:
return declaredSyntheticInferType;
case SyntaxKind.IntrinsicKeyword:
return intrinsicMarkerType;
case SyntaxKind.ThisType:
Expand Down Expand Up @@ -32319,13 +32323,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return true;
}

function hasCorrectTypeArgumentArity(signature: Signature, typeArguments: NodeArray<TypeNode> | undefined) {
function hasCorrectTypeArgumentArity(signature: Signature, typeArguments: NodeArray<TypeNode> | undefined, inferenceLocation?: boolean) {
// If the user supplied type arguments, but the number of type arguments does not match
// the declared number of type parameters, the call has an incorrect arity.
const numTypeParameters = length(signature.typeParameters);
const minTypeArgumentCount = getMinTypeArgumentCount(signature.typeParameters);
const minTypeArgumentCount = inferenceLocation ? 0 : getMinTypeArgumentCount(signature.typeParameters);
return !some(typeArguments) ||
(typeArguments.length >= minTypeArgumentCount && typeArguments.length <= numTypeParameters);
(length(typeArguments) >= minTypeArgumentCount && typeArguments.length <= numTypeParameters);
}

// If type has a single call signature and no other members, return that signature. Otherwise, return undefined.
Expand Down Expand Up @@ -32405,7 +32409,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const contextualType = getContextualType(node, skipBindingPatterns ? ContextFlags.SkipBindingPatterns : ContextFlags.None);
if (contextualType) {
const inferenceTargetType = getReturnTypeOfSignature(signature);
if (couldContainTypeVariables(inferenceTargetType)) {
if (signature.typeParameters && couldContainTypeVariables(inferenceTargetType)) {
const outerContext = getInferenceContext(node);
const isFromBindingPattern = !skipBindingPatterns && getContextualType(node, ContextFlags.SkipBindingPatterns) !== contextualType;
// A return type inference from a binding pattern can be used in instantiating the contextual
Expand Down Expand Up @@ -32442,7 +32446,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// from the return type. We need a separate inference pass here because (a) instantiation of
// the source type uses the outer context's return mapper (which excludes inferences made from
// outer arguments), and (b) we don't want any further inferences going into this context.
const returnContext = createInferenceContext(signature.typeParameters!, signature, context.flags);
const returnContext = createInferenceContext(signature.typeParameters, signature, context.flags);
const returnSourceType = instantiateType(contextualType, outerContext && outerContext.returnMapper);
inferTypes(returnContext.inferences, returnSourceType, inferenceTargetType);
context.returnMapper = some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined;
Expand Down Expand Up @@ -32541,10 +32545,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return createTupleType(types, flags, inConstContext, length(names) === length(types) ? names : undefined);
}

function checkTypeArguments(signature: Signature, typeArgumentNodes: readonly TypeNode[], reportErrors: boolean, headMessage?: DiagnosticMessage): Type[] | undefined {
function checkTypeArguments(signature: Signature, typeArgumentNodes: readonly TypeNode[], reportErrors: boolean, headMessage?: DiagnosticMessage, typeArgumentTypes?: Type[]): Type[] | undefined {
const isJavascript = isInJSFile(signature.declaration);
const typeParameters = signature.typeParameters!;
const typeArgumentTypes = fillMissingTypeArguments(map(typeArgumentNodes, getTypeFromTypeNode), typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript);
const argTypes = map(typeParameters, (_, i) => typeArgumentNodes[i] && getTypeFromTypeNode(typeArgumentNodes[i]) || declaredSyntheticInferType);
typeArgumentTypes ||= fillMissingTypeArguments(argTypes, typeParameters, getMinTypeArgumentCount(typeParameters), isJavascript);
if (some(typeArgumentTypes, isSyntheticInferType)) {
// Do validation once partial inference is complete
return typeArgumentTypes;
}
let mapper: TypeMapper | undefined;
for (let i = 0; i < typeArgumentNodes.length; i++) {
Debug.assert(typeParameters[i] !== undefined, "Should not call checkTypeArguments with too many type arguments");
Expand All @@ -32569,6 +32578,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return typeArgumentTypes;
}

function isSyntheticInferType(type: Type) {
return type === declaredSyntheticInferType;
}

function getJsxReferenceKind(node: JsxOpeningLikeElement): JsxReferenceKind {
if (isJsxIntrinsicTagName(node.tagName)) {
return JsxReferenceKind.Mixed;
Expand Down Expand Up @@ -33136,6 +33149,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let candidatesForArgumentError: Signature[] | undefined;
let candidateForArgumentArityError: Signature | undefined;
let candidateForTypeArgumentError: Signature | undefined;
let candidateForTypeArgumentErrorTypeArguments: Type[] | undefined;
let result: Signature | undefined;

// If we are in signature help, a trailing comma indicates that we intend to provide another argument,
Expand Down Expand Up @@ -33253,10 +33267,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
diagnostics.add(getArgumentArityError(node, [candidateForArgumentArityError], args, headMessage));
}
else if (candidateForTypeArgumentError) {
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, headMessage);
checkTypeArguments(candidateForTypeArgumentError, (node as CallExpression | TaggedTemplateExpression | JsxOpeningLikeElement).typeArguments!, /*reportErrors*/ true, headMessage, candidateForTypeArgumentErrorTypeArguments);
}
else {
const signaturesWithCorrectTypeArgumentArity = filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments));
const signaturesWithCorrectTypeArgumentArity = filter(signatures, s => hasCorrectTypeArgumentArity(s, typeArguments, /*inferenceLocation*/ true));
if (signaturesWithCorrectTypeArgumentArity.length === 0) {
diagnostics.add(getTypeArgumentArityError(node, signatures, typeArguments!, headMessage));
}
Expand All @@ -33272,6 +33286,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const oldCandidatesForArgumentError = candidatesForArgumentError;
const oldCandidateForArgumentArityError = candidateForArgumentArityError;
const oldCandidateForTypeArgumentError = candidateForTypeArgumentError;
const oldCandidateForTypeArgumentErrorTypeArguments = candidateForTypeArgumentErrorTypeArguments;

const failedSignatureDeclarations = failed.declaration?.symbol?.declarations || emptyArray;
const isOverload = failedSignatureDeclarations.length > 1;
Expand All @@ -33287,12 +33302,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
candidatesForArgumentError = oldCandidatesForArgumentError;
candidateForArgumentArityError = oldCandidateForArgumentArityError;
candidateForTypeArgumentError = oldCandidateForTypeArgumentError;
candidateForTypeArgumentErrorTypeArguments = oldCandidateForTypeArgumentErrorTypeArguments;
}

function chooseOverload(candidates: Signature[], relation: Map<string, RelationComparisonResult>, isSingleNonGenericCandidate: boolean, signatureHelpTrailingComma = false) {
candidatesForArgumentError = undefined;
candidateForArgumentArityError = undefined;
candidateForTypeArgumentError = undefined;
candidateForTypeArgumentErrorTypeArguments = undefined;

if (isSingleNonGenericCandidate) {
const candidate = candidates[0];
Expand All @@ -33308,19 +33325,46 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

for (let candidateIndex = 0; candidateIndex < candidates.length; candidateIndex++) {
const candidate = candidates[candidateIndex];
if (!hasCorrectTypeArgumentArity(candidate, typeArguments) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) {
if (!hasCorrectTypeArgumentArity(candidate, typeArguments, /*inferenceLocation*/ true) || !hasCorrectArity(node, args, candidate, signatureHelpTrailingComma)) {
continue;
}

const isJavascript = isInJSFile(candidate.declaration);
let checkCandidate: Signature;
let inferenceContext: InferenceContext | undefined;

if (candidate.typeParameters) {
let typeArgumentTypes: Type[] | undefined;
if (some(typeArguments)) {
typeArgumentTypes = checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false);
if (typeArgumentTypes) {
if (some(typeArgumentTypes, isSyntheticInferType)) {
// There are implied inferences we must make, despite having type arguments
const originalParams = candidate.typeParameters;
const withOriginalArgs = map(typeArgumentTypes, (r, i) => isSyntheticInferType(r) ? originalParams[i] : r);
const uninferredInstantiation = getSignatureInstantiation(candidate, withOriginalArgs, isJavascript);
inferenceContext = createInferenceContext(originalParams, uninferredInstantiation, isInJSFile(node) ? InferenceFlags.AnyDefault : InferenceFlags.None);
for (let i = 0; i < inferenceContext.inferences.length; i++) {
const correspondingArgument = typeArgumentTypes[i];
if (!isSyntheticInferType(correspondingArgument)) {
const inference = inferenceContext.inferences[i];
inference.inferredType = correspondingArgument;
inference.isFixed = true;
inference.priority = 0;
}
}
typeArgumentTypes = inferTypeArguments(node, uninferredInstantiation, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext);
argCheckMode |= inferenceContext.flags & InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal;
candidateForTypeArgumentError = candidate;
candidateForTypeArgumentErrorTypeArguments = typeArgumentTypes;
if (!checkTypeArguments(candidate, typeArguments, /*reportErrors*/ false, /*headMessage*/ undefined, typeArgumentTypes)) {
continue;
}
}
}
if (!typeArgumentTypes) {
candidateForTypeArgumentError = candidate;
candidateForTypeArgumentErrorTypeArguments = undefined;
continue;
}
}
Expand All @@ -33329,7 +33373,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
typeArgumentTypes = inferTypeArguments(node, candidate, args, argCheckMode | CheckMode.SkipGenericFunctions, inferenceContext);
argCheckMode |= inferenceContext.flags & InferenceFlags.SkippedGenericFunction ? CheckMode.SkipGenericFunctions : CheckMode.Normal;
}
checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isInJSFile(candidate.declaration), inferenceContext && inferenceContext.inferredTypeParameters);
checkCandidate = getSignatureInstantiation(candidate, typeArgumentTypes, isJavascript, inferenceContext && 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.
if (getNonArrayRestType(candidate) && !hasCorrectArity(node, args, checkCandidate, signatureHelpTrailingComma)) {
Expand Down Expand Up @@ -47699,6 +47743,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}

function checkGrammarTypeArguments(node: Node, typeArguments: NodeArray<TypeNode> | undefined): boolean {
if (isCallLikeExpression(node)) return false; // expressions allow trailing commas and zero-length lists
return checkGrammarForDisallowedTrailingComma(typeArguments) ||
checkGrammarForAtLeastOneTypeArgument(node, typeArguments);
}
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1936,6 +1936,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
return emitTemplateType(node as TemplateLiteralTypeNode);
case SyntaxKind.TemplateLiteralTypeSpan:
return emitTemplateTypeSpan(node as TemplateLiteralTypeSpan);
case SyntaxKind.OmittedType:
return;
case SyntaxKind.ImportType:
return emitImportTypeNode(node as ImportTypeNode);

Expand Down
7 changes: 7 additions & 0 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ import {
ObjectLiteralElementLike,
ObjectLiteralExpression,
OmittedExpression,
OmittedType,
OptionalTypeNode,
OuterExpression,
OuterExpressionKinds,
Expand Down Expand Up @@ -698,6 +699,7 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
createClassExpression,
updateClassExpression,
createOmittedExpression,
createOmittedType,
createExpressionWithTypeArguments,
updateExpressionWithTypeArguments,
createAsExpression,
Expand Down Expand Up @@ -3566,6 +3568,11 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
return createBaseNode<OmittedExpression>(SyntaxKind.OmittedExpression);
}

// @api
function createOmittedType() {
return createBaseNode<OmittedType>(SyntaxKind.OmittedType);
}

// @api
function createExpressionWithTypeArguments(expression: Expression, typeArguments: readonly TypeNode[] | undefined) {
const node = createBaseNode<ExpressionWithTypeArguments>(SyntaxKind.ExpressionWithTypeArguments);
Expand Down
12 changes: 8 additions & 4 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6076,7 +6076,7 @@ namespace Parser {
return finishNode(factory.createJsxOpeningFragment(), pos);
}
const tagName = parseJsxElementName();
const typeArguments = (contextFlags & NodeFlags.JavaScriptFile) === 0 ? tryParseTypeArguments() : undefined;
const typeArguments = (contextFlags & NodeFlags.JavaScriptFile) === 0 ? tryParseTypeArguments(/*isInferencePosition*/ true) : undefined;
const attributes = parseJsxAttributes();

let node: JsxOpeningLikeElement;
Expand Down Expand Up @@ -6427,6 +6427,10 @@ namespace Parser {
return result;
}

function parseTypeOrOmittedType() {
return token() === SyntaxKind.CommaToken ? finishNode(factory.createOmittedType(), getNodePos()) : parseType();
}

function parseTypeArgumentsInExpression() {
if ((contextFlags & NodeFlags.JavaScriptFile) !== 0) {
// TypeArguments must not be parsed in JavaScript files to avoid ambiguity with binary operators.
Expand All @@ -6438,7 +6442,7 @@ namespace Parser {
}
nextToken();

const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parseType);
const typeArguments = parseDelimitedList(ParsingContext.TypeArguments, parseTypeOrOmittedType);
if (reScanGreaterToken() !== SyntaxKind.GreaterThanToken) {
// If it doesn't have the closing `>` then it's definitely not an type argument list.
return undefined;
Expand Down Expand Up @@ -7988,9 +7992,9 @@ namespace Parser {
return finishNode(factory.createExpressionWithTypeArguments(expression, typeArguments), pos);
}

function tryParseTypeArguments(): NodeArray<TypeNode> | undefined {
function tryParseTypeArguments(isInferencePosition?: boolean): NodeArray<TypeNode> | undefined {
return token() === SyntaxKind.LessThanToken ?
parseBracketedList(ParsingContext.TypeArguments, parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) : undefined;
parseBracketedList(ParsingContext.TypeArguments, isInferencePosition ? parseTypeOrOmittedType : parseType, SyntaxKind.LessThanToken, SyntaxKind.GreaterThanToken) : undefined;
}

function isHeritageClause(): boolean {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/transformers/ts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,8 @@ export function transformTypeScript(context: TransformationContext) {
case SyntaxKind.IndexedAccessType:
case SyntaxKind.MappedType:
case SyntaxKind.LiteralType:
case SyntaxKind.InferType:
case SyntaxKind.OmittedType:
// TypeScript type nodes are elided.
// falls through

Expand Down
Loading