Skip to content

Commit d4b15eb

Browse files
authored
Enhance type argument completions (#62170)
1 parent 83ff202 commit d4b15eb

15 files changed

+402
-13
lines changed

src/compiler/checker.ts

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ import {
421421
hasSyntacticModifier,
422422
hasSyntacticModifiers,
423423
hasType,
424+
hasTypeArguments,
424425
HeritageClause,
425426
hostGetCanonicalFileName,
426427
Identifier,
@@ -42620,6 +42621,51 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4262042621
return undefined;
4262142622
}
4262242623

42624+
/**
42625+
* Gets generic signatures from the function's/constructor's type.
42626+
*/
42627+
function getUninstantiatedSignatures(node: CallLikeExpression): readonly Signature[] {
42628+
switch (node.kind) {
42629+
case SyntaxKind.CallExpression:
42630+
case SyntaxKind.Decorator:
42631+
return getSignaturesOfType(
42632+
getTypeOfExpression(node.expression),
42633+
SignatureKind.Call,
42634+
);
42635+
case SyntaxKind.NewExpression:
42636+
return getSignaturesOfType(
42637+
getTypeOfExpression(node.expression),
42638+
SignatureKind.Construct,
42639+
);
42640+
case SyntaxKind.JsxSelfClosingElement:
42641+
case SyntaxKind.JsxOpeningElement:
42642+
if (isJsxIntrinsicTagName(node.tagName)) return [];
42643+
return getSignaturesOfType(
42644+
getTypeOfExpression(node.tagName),
42645+
SignatureKind.Call,
42646+
);
42647+
case SyntaxKind.TaggedTemplateExpression:
42648+
return getSignaturesOfType(
42649+
getTypeOfExpression(node.tag),
42650+
SignatureKind.Call,
42651+
);
42652+
case SyntaxKind.BinaryExpression:
42653+
case SyntaxKind.JsxOpeningFragment:
42654+
return [];
42655+
}
42656+
}
42657+
42658+
function getTypeParameterConstraintForPositionAcrossSignatures(signatures: readonly Signature[], position: number) {
42659+
const relevantTypeParameterConstraints = flatMap(signatures, signature => {
42660+
const relevantTypeParameter = signature.typeParameters?.[position];
42661+
if (relevantTypeParameter === undefined) return [];
42662+
const relevantConstraint = getConstraintOfTypeParameter(relevantTypeParameter);
42663+
if (relevantConstraint === undefined) return [];
42664+
return [relevantConstraint];
42665+
});
42666+
return getUnionType(relevantTypeParameterConstraints);
42667+
}
42668+
4262342669
function checkTypeReferenceNode(node: TypeReferenceNode | ExpressionWithTypeArguments) {
4262442670
checkGrammarTypeArguments(node, node.typeArguments);
4262542671
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 {
4265842704
}
4265942705

4266042706
function getTypeArgumentConstraint(node: TypeNode): Type | undefined {
42661-
const typeReferenceNode = tryCast(node.parent, isTypeReferenceType);
42662-
if (!typeReferenceNode) return undefined;
42663-
const typeParameters = getTypeParametersForTypeReferenceOrImport(typeReferenceNode);
42664-
if (!typeParameters) return undefined;
42665-
const constraint = getConstraintOfTypeParameter(typeParameters[typeReferenceNode.typeArguments!.indexOf(node)]);
42666-
return constraint && instantiateType(constraint, createTypeMapper(typeParameters, getEffectiveTypeArguments(typeReferenceNode, typeParameters)));
42707+
let typeArgumentPosition;
42708+
if (hasTypeArguments(node.parent) && Array.isArray(node.parent.typeArguments)) {
42709+
typeArgumentPosition = node.parent.typeArguments.indexOf(node);
42710+
}
42711+
42712+
if (typeArgumentPosition !== undefined) {
42713+
// The node could be a type argument of a call, a `new` expression, a decorator, an
42714+
// instantiation expression, or a generic type instantiation.
42715+
42716+
if (isCallLikeExpression(node.parent)) {
42717+
return getTypeParameterConstraintForPositionAcrossSignatures(
42718+
getUninstantiatedSignatures(node.parent),
42719+
typeArgumentPosition,
42720+
);
42721+
}
42722+
42723+
if (isDecorator(node.parent.parent)) {
42724+
return getTypeParameterConstraintForPositionAcrossSignatures(
42725+
getUninstantiatedSignatures(node.parent.parent),
42726+
typeArgumentPosition,
42727+
);
42728+
}
42729+
42730+
if (isExpressionWithTypeArguments(node.parent) && isExpressionStatement(node.parent.parent)) {
42731+
const uninstantiatedType = checkExpression(node.parent.expression);
42732+
42733+
const callConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42734+
getSignaturesOfType(uninstantiatedType, SignatureKind.Call),
42735+
typeArgumentPosition,
42736+
);
42737+
const constructConstraint = getTypeParameterConstraintForPositionAcrossSignatures(
42738+
getSignaturesOfType(uninstantiatedType, SignatureKind.Construct),
42739+
typeArgumentPosition,
42740+
);
42741+
42742+
// An instantiation expression instantiates both call and construct signatures, so
42743+
// if both exist type arguments must be assignable to both constraints.
42744+
if (constructConstraint.flags & TypeFlags.Never) return callConstraint;
42745+
if (callConstraint.flags & TypeFlags.Never) return constructConstraint;
42746+
return getIntersectionType([callConstraint, constructConstraint]);
42747+
}
42748+
42749+
if (isTypeReferenceType(node.parent)) {
42750+
const typeParameters = getTypeParametersForTypeReferenceOrImport(node.parent);
42751+
if (!typeParameters) return undefined;
42752+
const relevantTypeParameter = typeParameters[typeArgumentPosition];
42753+
const constraint = getConstraintOfTypeParameter(relevantTypeParameter);
42754+
return constraint && instantiateType(
42755+
constraint,
42756+
createTypeMapper(typeParameters, getEffectiveTypeArguments(node.parent, typeParameters)),
42757+
);
42758+
}
42759+
}
4266742760
}
4266842761

4266942762
function checkTypeQuery(node: TypeQueryNode) {

src/services/completions.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,6 @@ import {
256256
isTypeOnlyImportDeclaration,
257257
isTypeOnlyImportOrExportDeclaration,
258258
isTypeParameterDeclaration,
259-
isTypeReferenceType,
260259
isValidTypeOnlyAliasUseSite,
261260
isVariableDeclaration,
262261
isVariableLike,
@@ -3626,17 +3625,20 @@ function getCompletionData(
36263625
}
36273626

36283627
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
3629-
const contextualType = previousToken && getContextualType(previousToken, position, sourceFile, typeChecker);
3628+
const contextualTypeOrConstraint = previousToken && (
3629+
getContextualType(previousToken, position, sourceFile, typeChecker) ??
3630+
getConstraintOfTypeArgumentProperty(previousToken, typeChecker)
3631+
);
36303632

36313633
// exclude literal suggestions after <input type="text" [||] /> (#51667) and after closing quote (#52675)
36323634
// for strings getStringLiteralCompletions handles completions
36333635
const isLiteralExpected = !tryCast(previousToken, isStringLiteralLike) && !isJsxIdentifierExpected;
36343636
const literals = !isLiteralExpected ? [] : mapDefined(
3635-
contextualType && (contextualType.isUnion() ? contextualType.types : [contextualType]),
3637+
contextualTypeOrConstraint && (contextualTypeOrConstraint.isUnion() ? contextualTypeOrConstraint.types : [contextualTypeOrConstraint]),
36363638
t => t.isLiteral() && !(t.flags & TypeFlags.EnumLiteral) ? t.value : undefined,
36373639
);
36383640

3639-
const recommendedCompletion = previousToken && contextualType && getRecommendedCompletion(previousToken, contextualType, typeChecker);
3641+
const recommendedCompletion = previousToken && contextualTypeOrConstraint && getRecommendedCompletion(previousToken, contextualTypeOrConstraint, typeChecker);
36403642
return {
36413643
kind: CompletionDataKind.Data,
36423644
symbols,
@@ -5766,11 +5768,13 @@ function tryGetTypeLiteralNode(node: Node): TypeLiteralNode | undefined {
57665768
return undefined;
57675769
}
57685770

5769-
function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker): Type | undefined {
5771+
/** @internal */
5772+
export function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker): Type | undefined {
57705773
if (!node) return undefined;
57715774

5772-
if (isTypeNode(node) && isTypeReferenceType(node.parent)) {
5773-
return checker.getTypeArgumentConstraint(node);
5775+
if (isTypeNode(node)) {
5776+
const constraint = checker.getTypeArgumentConstraint(node);
5777+
if (constraint) return constraint;
57745778
}
57755779

57765780
const t = getConstraintOfTypeArgumentProperty(node.parent, checker);
@@ -5779,10 +5783,19 @@ function getConstraintOfTypeArgumentProperty(node: Node, checker: TypeChecker):
57795783
switch (node.kind) {
57805784
case SyntaxKind.PropertySignature:
57815785
return checker.getTypeOfPropertyOfContextualType(t, (node as PropertySignature).symbol.escapedName);
5786+
case SyntaxKind.ColonToken:
5787+
if (node.parent.kind === SyntaxKind.PropertySignature) {
5788+
// The cursor is at a property value location like `Foo<{ x: | }`.
5789+
// `t` already refers to the appropriate property type.
5790+
return t;
5791+
}
5792+
break;
57825793
case SyntaxKind.IntersectionType:
57835794
case SyntaxKind.TypeLiteral:
57845795
case SyntaxKind.UnionType:
57855796
return t;
5797+
case SyntaxKind.OpenBracketToken:
5798+
return checker.getElementTypeOfArrayType(t);
57865799
}
57875800
}
57885801

src/services/stringCompletions.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
createCompletionDetails,
44
createCompletionDetailsForSymbol,
55
getCompletionEntriesFromSymbols,
6+
getConstraintOfTypeArgumentProperty,
67
getDefaultCommitCharacters,
78
getPropertiesForObjectExpression,
89
Log,
@@ -509,7 +510,12 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
509510

510511
function fromUnionableLiteralType(grandParent: Node): StringLiteralCompletionsFromTypes | StringLiteralCompletionsFromProperties | undefined {
511512
switch (grandParent.kind) {
513+
case SyntaxKind.CallExpression:
512514
case SyntaxKind.ExpressionWithTypeArguments:
515+
case SyntaxKind.JsxOpeningElement:
516+
case SyntaxKind.JsxSelfClosingElement:
517+
case SyntaxKind.NewExpression:
518+
case SyntaxKind.TaggedTemplateExpression:
513519
case SyntaxKind.TypeReference: {
514520
const typeArgument = findAncestor(parent, n => n.parent === grandParent) as LiteralTypeNode;
515521
if (typeArgument) {
@@ -529,6 +535,8 @@ function getStringLiteralCompletionEntries(sourceFile: SourceFile, node: StringL
529535
return undefined;
530536
}
531537
return stringLiteralCompletionsFromProperties(typeChecker.getTypeFromTypeNode(objectType));
538+
case SyntaxKind.PropertySignature:
539+
return { kind: StringLiteralCompletionKind.Types, types: getStringLiteralTypes(getConstraintOfTypeArgumentProperty(grandParent, typeChecker)), isNewIdentifier: false };
532540
case SyntaxKind.UnionType: {
533541
const result = fromUnionableLiteralType(walkUpParentheses(grandParent.parent));
534542
if (!result) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////interface Bar {
8+
//// three: boolean;
9+
//// four: {
10+
//// five: unknown;
11+
//// };
12+
////}
13+
////
14+
////function a<T extends Foo>() {}
15+
////a<{/*0*/}>();
16+
////
17+
////var b = () => <T extends Foo>() => {};
18+
////b()<{/*1*/}>();
19+
////
20+
////declare function c<T extends Foo>(): void
21+
////declare function c<T extends Bar>(): void
22+
////c<{/*2*/}>();
23+
////
24+
////function d<T extends Foo, U extends Bar>() {}
25+
////d<{/*3*/}, {/*4*/}>();
26+
////d<Foo, { four: {/*5*/} }>();
27+
////
28+
////(<T extends Foo>() => {})<{/*6*/}>();
29+
30+
verify.completions(
31+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
32+
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
33+
{ marker: "2", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
34+
{ marker: "3", unsorted: ["one", "two"], isNewIdentifierLocation: true },
35+
{ marker: "4", unsorted: ["three", "four"], isNewIdentifierLocation: true },
36+
{ marker: "5", unsorted: ["five"], isNewIdentifierLocation: true },
37+
{ marker: "6", unsorted: ["one", "two"], isNewIdentifierLocation: true },
38+
);
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////interface Bar {
8+
//// three: boolean;
9+
//// four: symbol;
10+
////}
11+
////
12+
////class A<T extends Foo> {}
13+
////new A<{/*0*/}>();
14+
////
15+
////class B<T extends Foo, U extends Bar> {}
16+
////new B<{/*1*/}, {/*2*/}>();
17+
////
18+
////declare const C: {
19+
//// new <T extends Foo>(): unknown
20+
//// new <T extends Bar>(): unknown
21+
////}
22+
////new C<{/*3*/}>()
23+
////
24+
////new (class <T extends Foo> {})<{/*4*/}>();
25+
26+
verify.completions(
27+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
28+
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
29+
{ marker: "2", unsorted: ["three", "four"], isNewIdentifierLocation: true },
30+
{ marker: "3", unsorted: ["one", "two", "three", "four"], isNewIdentifierLocation: true },
31+
{ marker: "4", unsorted: ["one", "two"], isNewIdentifierLocation: true },
32+
);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// kind: 'foo';
5+
//// one: string;
6+
////}
7+
////interface Bar {
8+
//// kind: 'bar';
9+
//// two: number;
10+
////}
11+
////
12+
////declare function a<T extends Foo>(): void
13+
////declare function a<T extends Bar>(): void
14+
////a<{ kind: 'bar', /*0*/ }>();
15+
////
16+
////declare function b<T extends Foo>(kind: 'foo'): void
17+
////declare function b<T extends Bar>(kind: 'bar'): void
18+
////b<{/*1*/}>('bar');
19+
20+
// The completion lists are unfortunately not narrowed here (ideally only
21+
// properties of `Bar` would be suggested).
22+
verify.completions(
23+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
24+
{ marker: "1", unsorted: ["kind", "one", "two"], isNewIdentifierLocation: true },
25+
);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: preserve
4+
// @filename: a.tsx
5+
////interface Foo {
6+
//// one: string;
7+
//// two: number;
8+
////}
9+
////
10+
////const Component = <T extends Foo>() => <></>;
11+
////
12+
////<Component<{/*0*/}>></Component>;
13+
////<Component<{/*1*/}>/>;
14+
15+
verify.completions(
16+
{ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true },
17+
{ marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true },
18+
);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////declare function f<T extends Foo>(x: TemplateStringsArray): void;
8+
////f<{/*0*/}>``;
9+
10+
verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true });
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
////interface Foo {
4+
//// one: string;
5+
//// two: number;
6+
////}
7+
////
8+
////declare function decorator<T extends Foo>(originalMethod: unknown, _context: unknown): never
9+
////
10+
////class {
11+
//// @decorator<{/*0*/}>
12+
//// method() {}
13+
////}
14+
15+
verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true });

0 commit comments

Comments
 (0)