Enhance type argument completions (#62170)

This commit is contained in:
Matt Kantor 2025-09-30 16:00:20 -04:00 committed by GitHub
parent 83ff20281e
commit d4b15eb56d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 402 additions and 13 deletions

View File

@ -421,6 +421,7 @@ import {
hasSyntacticModifier,
hasSyntacticModifiers,
hasType,
hasTypeArguments,
HeritageClause,
hostGetCanonicalFileName,
Identifier,
@ -42620,6 +42621,51 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return undefined;
}
/**
* Gets generic signatures from the function's/constructor's type.
*/
function getUninstantiatedSignatures(node: CallLikeExpression): readonly Signature[] {
switch (node.kind) {
case SyntaxKind.CallExpression:
case SyntaxKind.Decorator:
return getSignaturesOfType(
getTypeOfExpression(node.expression),
SignatureKind.Call,
);
case SyntaxKind.NewExpression:
return getSignaturesOfType(
getTypeOfExpression(node.expression),
SignatureKind.Construct,
);
case SyntaxKind.JsxSelfClosingElement:
case SyntaxKind.JsxOpeningElement:
if (isJsxIntrinsicTagName(node.tagName)) return [];
return getSignaturesOfType(
getTypeOfExpression(node.tagName),
SignatureKind.Call,
);
case SyntaxKind.TaggedTemplateExpression:
return getSignaturesOfType(
getTypeOfExpression(node.tag),
SignatureKind.Call,
);
case SyntaxKind.BinaryExpression:
case SyntaxKind.JsxOpeningFragment:
return [];
}
}
function getTypeParameterConstraintForPositionAcrossSignatures(signatures: readonly Signature[], position: number) {
const relevantTypeParameterConstraints = flatMap(signatures, signature => {
const relevantTypeParameter = signature.typeParameters?.[position];
if (relevantTypeParameter === undefined) return [];
const relevantConstraint = getConstraintOfTypeParameter(relevantTypeParameter);
if (relevantConstraint === undefined) return [];
return [relevantConstraint];
});
return getUnionType(relevantTypeParameterConstraints);
}
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) {
checkGrammarTypeArguments(node, node.typeArguments);
if (node.kind === SyntaxKind.TypeReference && !isInJSFile(node) && !isInJSDoc(node) && node.typeArguments && node.typeName.end !== node.typeArguments.pos) {
@ -42658,12 +42704,59 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function getTypeArgumentConstraint(node: TypeNode): Type | undefined {
const typeReferenceNode = tryCast(node.parent, isTypeReferenceType);
if (!typeReferenceNode) return undefined;
const typeParameters = getTypeParametersForTypeReferenceOrImport(typeReferenceNode);
if (!typeParameters) return undefined;
const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]);
return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters)));
let typeArgumentPosition;
if (hasTypeArguments(node.parent) && Array.isArray(node.parent.typeArguments)) {
typeArgumentPosition = node.parent.typeArguments.indexOf(node);
}
if (typeArgumentPosition !== undefined) {
// The node could be a type argument of a call, a `new` expression, a decorator, an
// instantiation expression, or a generic type instantiation.
if (isCallLikeExpression(node.parent)) {
return getTypeParameterConstraintForPositionAcrossSignatures(
getUninstantiatedSignatures(node.parent),
typeArgumentPosition,
);
}
if (isDecorator(node.parent.parent)) {
return getTypeParameterConstraintForPositionAcrossSignatures(
getUninstantiatedSignatures(node.parent.parent),
typeArgumentPosition,
);
}
if (isExpressionWithTypeArguments(node.parent) && isExpressionStatement(node.parent.parent)) {
const uninstantiatedType = checkExpression(node.parent.expression);
const callConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
getSignaturesOfType(uninstantiatedType, SignatureKind.Call),
typeArgumentPosition,
);
const constructConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
getSignaturesOfType(uninstantiatedType, SignatureKind.Construct),
typeArgumentPosition,
);
// An instantiation expression instantiates both call and construct signatures, so
// if both exist type arguments must be assignable to both constraints.
if (constructConstraint.flags & TypeFlags.Never) return callConstraint;
if (callConstraint.flags & TypeFlags.Never) return constructConstraint;
return getIntersectionType([callConstraint, constructConstraint]);
}
if (isTypeReferenceType(node.parent)) {
const typeParameters = getTypeParametersForTypeReferenceOrImport(node.parent);
if (!typeParameters) return undefined;
const relevantTypeParameter = typeParameters[typeArgumentPosition];
const constraint = getConstraintOfTypeParameter(relevantTypeParameter);
return constraint && instantiateType(
constraint,
createTypeMapper(typeParameters, getEffectiveTypeArguments(node.parent, typeParameters)),
);
}
}
}
function checkTypeQuery(node: TypeQueryNode) {

View File

@ -256,7 +256,6 @@ import {
isTypeOnlyImportDeclaration,
isTypeOnlyImportOrExportDeclaration,
isTypeParameterDeclaration,
isTypeReferenceType,
isValidTypeOnlyAliasUseSite,
isVariableDeclaration,
isVariableLike,
@ -3626,17 +3625,20 @@ function getCompletionData(
}
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker);
const contextualTypeOrConstraint = previousToken && (
getContextualType(previousToken, position, sourceFile, typeChecker) ??
getConstraintOfTypeArgumentProperty(previousToken, typeChecker)
);
// exclude literal suggestions after <input type="text" [||] /> (#51667) and after closing quote (#52675)
// for strings getStringLiteralCompletions handles completions
const isLiteralExpected = !tryCast(previousToken, isStringLiteralLike) && !isJsxIdentifierExpected;
const literals = !isLiteralExpected ? [] : mapDefined(
contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]),
contextualTypeOrConstraint && (contextualTypeOrConstraint.isUnion() ? contextualTypeOrConstraint.types : [contextualTypeOrConstraint]),
t => t.isLiteral() && !(t.flags & TypeFlags.EnumLiteral) ? t.value : undefined,
);
const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker);
const recommendedCompletion = previousToken && contextualTypeOrConstraint && getRecommendedCompletion(previousToken, contextualTypeOrConstraint, typeChecker);
return {
kind: CompletionDataKind.Data,
symbols,
@ -5766,11 +5768,13 @@ function tryGetTypeLiteralNode(node: Node): TypeLiteralNode | undefined {
return undefined;
}
function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker): Type | undefined {
/** @internal */
export function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker): Type | undefined {
if (!node) return undefined;
if (isTypeNode(node) && isTypeReferenceType(node.parent)) {
return checker.getTypeArgumentConstraint(node);
if (isTypeNode(node)) {
const constraint = checker.getTypeArgumentConstraint(node);
if (constraint) return constraint;
}
const t = getConstraintOfTypeArgumentProperty(node.parent, checker);
@ -5779,10 +5783,19 @@ function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker):
switch (node.kind) {
case SyntaxKind.PropertySignature:
return checker.getTypeOfPropertyOfContextualType(t, (node as PropertySignature).symbol.escapedName);
case SyntaxKind.ColonToken:
if (node.parent.kind === SyntaxKind.PropertySignature) {
// The cursor is at a property value location like `Foo<{ x: | }`.
// `t` already refers to the appropriate property type.
return t;
}
break;
case SyntaxKind.IntersectionType:
case SyntaxKind.TypeLiteral:
case SyntaxKind.UnionType:
return t;
case SyntaxKind.OpenBracketToken:
return checker.getElementTypeOfArrayType(t);
}
}

View File

@ -3,6 +3,7 @@ import {
createCompletionDetails,
createCompletionDetailsForSymbol,
getCompletionEntriesFromSymbols,
getConstraintOfTypeArgumentProperty,
getDefaultCommitCharacters,
getPropertiesForObjectExpression,
Log,
@ -509,7 +510,12 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
function fromUnionableLiteralType(grandParent: Node): StringLiteralCompletionsFromTypes | StringLiteralCompletionsFromProperties | undefined {
switch (grandParent.kind) {
case SyntaxKind.CallExpression:
case SyntaxKind.ExpressionWithTypeArguments:
case SyntaxKind.JsxOpeningElement:
case SyntaxKind.JsxSelfClosingElement:
case SyntaxKind.NewExpression:
case SyntaxKind.TaggedTemplateExpression:
case SyntaxKind.TypeReference: {
const typeArgument = findAncestor(parent, n => n.parent === grandParent) as LiteralTypeNode;
if (typeArgument) {
@ -529,6 +535,8 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
return undefined;
}
return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType));
case SyntaxKind.PropertySignature:
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getConstraintOfTypeArgumentProperty(grandParent, typeChecker)), isNewIdentifier: false };
case SyntaxKind.UnionType: {
const result = fromUnionableLiteralType(walkUpParentheses(grandParent.parent));
if (!result) {

View File

@ -0,0 +1,38 @@
/// <reference path="fourslash.ts" />
////interface Foo {
//// one: string;
//// two: number;
////}
////interface Bar {
//// three: boolean;
//// four: {
//// five: unknown;
//// };
////}
////
////function a<T extends Foo>() {}
////a<{/*0*/}>();
////
////var b = () => <T extends Foo>() => {};
////b()<{/*1*/}>();
////
////declare function c<T extends Foo>(): void
////declare function c<T extends Bar>(): void
////c<{/*2*/}>();
////
////function d<T extends Foo, U extends Bar>() {}
////d<{/*3*/}, {/*4*/}>();
////d<Foo, { four: {/*5*/} }>();
////
////(<T extends Foo>() => {})<{/*6*/}>();
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "2", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
{ marker: "3", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "4", unsorted: ["three", "four"], isNewIdentifierLocation: true },
{ marker: "5", unsorted: ["five"], isNewIdentifierLocation: true },
{ marker: "6", unsorted: ["one", "two"], isNewIdentifierLocation: true },
);

View File

@ -0,0 +1,32 @@
/// <reference path="fourslash.ts" />
////interface Foo {
//// one: string;
//// two: number;
////}
////interface Bar {
//// three: boolean;
//// four: symbol;
////}
////
////class A<T extends Foo> {}
////new A<{/*0*/}>();
////
////class B<T extends Foo, U extends Bar> {}
////new B<{/*1*/}, {/*2*/}>();
////
////declare const C: {
//// new <T extends Foo>(): unknown
//// new <T extends Bar>(): unknown
////}
////new C<{/*3*/}>()
////
////new (class <T extends Foo> {})<{/*4*/}>();
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "2", unsorted: ["three", "four"], isNewIdentifierLocation: true },
{ marker: "3", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
{ marker: "4", unsorted: ["one", "two"], isNewIdentifierLocation: true },
);

View File

@ -0,0 +1,25 @@
/// <reference path="fourslash.ts" />
////interface Foo {
//// kind: 'foo';
//// one: string;
////}
////interface Bar {
//// kind: 'bar';
//// two: number;
////}
////
////declare function a<T extends Foo>(): void
////declare function a<T extends Bar>(): void
////a<{ kind: 'bar', /*0*/ }>();
////
////declare function b<T extends Foo>(kind: 'foo'): void
////declare function b<T extends Bar>(kind: 'bar'): void
////b<{/*1*/}>('bar');
// The completion lists are unfortunately not narrowed here (ideally only
// properties of `Bar` would be suggested).
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "1", unsorted: ["kind", "one", "two"], isNewIdentifierLocation: true },
);

View File

@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />
// @jsx: preserve
// @filename: a.tsx
////interface Foo {
//// one: string;
//// two: number;
////}
////
////const Component = <T extends Foo>() => <></>;
////
////<Component<{/*0*/}>></Component>;
////<Component<{/*1*/}>/>;
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
);

View File

@ -0,0 +1,10 @@
/// <reference path="fourslash.ts" />
////interface Foo {
//// one: string;
//// two: number;
////}
////declare function f<T extends Foo>(x: TemplateStringsArray): void;
////f<{/*0*/}>``;
verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true });

View File

@ -0,0 +1,15 @@
/// <reference path="fourslash.ts" />
////interface Foo {
//// one: string;
//// two: number;
////}
////
////declare function decorator<T extends Foo>(originalMethod: unknown, _context: unknown): never
////
////class {
//// @decorator<{/*0*/}>
//// method() {}
////}
verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true });

View File

@ -0,0 +1,35 @@
/// <reference path="fourslash.ts" />
////interface Foo {
//// one: string;
//// two: number;
////}
////interface Bar {
//// three: boolean;
//// four: {
//// five: unknown;
//// };
////}
////
////(<T extends Foo>() => {})<{/*0*/}>;
////
////(class <T extends Foo>{})<{/*1*/}>;
////
////declare const a: {
//// new <T extends Foo>(): {};
//// <T extends Bar>(): {};
////}
////a<{/*2*/}>;
////
////declare const b: {
//// new <T extends { one: true }>(): {};
//// <T extends { one: false }>(): {};
////}
////b<{/*3*/}>;
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
{ marker: "2", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
{ marker: "3", unsorted: [], isNewIdentifierLocation: true },
);

View File

@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />
////class Foo<T extends { x: 'one' | 2 }> {}
////function foo<T extends { x: 'one' | 2 }>() {}
////
////type A = Foo<{ x: /*0*/ }>;
////new Foo<{ x: /*1*/ }>();
////foo<{ x: /*2*/ }>();
////foo<{ x: /*3*/ }>;
////Foo<{ x: /*4*/ }>;
verify.completions(
{ marker: "0", includes: ['"one"', '2'], isNewIdentifierLocation: false },
{ marker: "1", includes: ['"one"', '2'], isNewIdentifierLocation: false },
{ marker: "2", includes: ['"one"', '2'], isNewIdentifierLocation: false },
{ marker: "3", includes: ['"one"', '2'], isNewIdentifierLocation: false },
{ marker: "4", includes: ['"one"', '2'], isNewIdentifierLocation: false },
);

View File

@ -0,0 +1,24 @@
/// <reference path="fourslash.ts" />
////class Foo<T extends { x: 'one' | 'two' }> {}
////function foo<T extends { x: 'one' | 'two' }>() {}
////declare function tag<T extends { x: 'one' | 'two' }>(x: TemplateStringsArray): void;
////declare function decorator<T extends { x: 'one' | 'two' }>(...args: unknown[]): never
////
////type A = Foo<{ x: '/*0*/' }>;
////new Foo<{ x: '/*1*/' }>();
////foo<{ x: '/*2*/' }>();
////foo<{ x: '/*3*/' }>;
////Foo<{ x: '/*4*/' }>;
////tag<{ x: '/*5*/' }>``;
////class { @decorator<{ x: '/*6*/' }>; method() {} }
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "2", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "3", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "4", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "5", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "6", unsorted: ["one", "two"], isNewIdentifierLocation: false },
);

View File

@ -0,0 +1,24 @@
/// <reference path="fourslash.ts" />
////class Foo<T extends 'one' | 'two'> {}
////function foo<T extends 'one' | 'two'>() {}
////declare function tag<T extends 'one' | 'two'>(x: TemplateStringsArray): void;
////declare function decorator<T extends 'one' | 'two'>(...args: unknown[]): never
////
////type A = Foo<'/*0*/'>;
////new Foo<'/*1*/'>();
////foo<'/*2*/'>();
////foo<'/*3*/'>;
////Foo<'/*4*/'>;
////tag<'/*5*/'>``;
////class { @decorator<'/*6*/'>; method() {} }
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "2", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "3", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "4", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "5", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "6", unsorted: ["one", "two"], isNewIdentifierLocation: false },
);

View File

@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />
// @jsx: preserve
// @filename: a.tsx
////const Component1 = <T extends { x: 'one' | 'two' }>() => <></>;
////const Component2 = <T extends 'one' | 'two'>() => <></>;
////
////<Component1<{ x: '/*0*/' }>></Component>;
////<Component1<{ x: '/*1*/' }>/>;
////<Component2<'/*2*/'>></Component>;
////<Component2<'/*3*/'>/>;
verify.completions(
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "2", unsorted: ["one", "two"], isNewIdentifierLocation: false },
{ marker: "3", unsorted: ["one", "two"], isNewIdentifierLocation: false },
);

View File

@ -0,0 +1,18 @@
/// <reference path="fourslash.ts" />
////class Foo<T extends ('one' | 2)[]> {}
////function foo<T extends ('one' | 2)[]>() {}
////
////type A = Foo<[/*0*/]>;
////new Foo<[/*1*/]>();
////foo<[/*2*/]>();
////foo<[/*3*/]>;
////Foo<[/*4*/]>;
verify.completions(
{ marker: "0", includes: ['"one"', '2'], isNewIdentifierLocation: true },
{ marker: "1", includes: ['"one"', '2'], isNewIdentifierLocation: true },
{ marker: "2", includes: ['"one"', '2'], isNewIdentifierLocation: true },
{ marker: "3", includes: ['"one"', '2'], isNewIdentifierLocation: true },
{ marker: "4", includes: ['"one"', '2'], isNewIdentifierLocation: true }
);