From 4bf65ffefd0cf7bf95634e0d6a65800dee95fad9 Mon Sep 17 00:00:00 2001 From: Matt Kantor Date: Tue, 20 May 2025 16:00:08 -0400 Subject: [PATCH] Make `getTypeArgumentConstraint` work for type arguments of expressions Previously, `getTypeArgumentConstraint` could only find constraints for type arguments of generic type instantiations. Now it additionally supports type arguments of the following expression kinds: - function calls - `new` expressions - tagged templates - JSX expressions - decorators - instantiation expressions --- src/compiler/checker.ts | 104 +++++++++++++++++- src/services/completions.ts | 6 +- ...etionListInTypeLiteralInTypeParameter10.ts | 38 +++++++ ...etionListInTypeLiteralInTypeParameter11.ts | 32 ++++++ ...etionListInTypeLiteralInTypeParameter12.ts | 25 +++++ ...etionListInTypeLiteralInTypeParameter13.ts | 18 +++ ...etionListInTypeLiteralInTypeParameter14.ts | 10 ++ ...etionListInTypeLiteralInTypeParameter15.ts | 15 +++ ...etionListInTypeLiteralInTypeParameter16.ts | 35 ++++++ 9 files changed, 274 insertions(+), 9 deletions(-) create mode 100644 tests/cases/fourslash/completionListInTypeLiteralInTypeParameter10.ts create mode 100644 tests/cases/fourslash/completionListInTypeLiteralInTypeParameter11.ts create mode 100644 tests/cases/fourslash/completionListInTypeLiteralInTypeParameter12.ts create mode 100644 tests/cases/fourslash/completionListInTypeLiteralInTypeParameter13.ts create mode 100644 tests/cases/fourslash/completionListInTypeLiteralInTypeParameter14.ts create mode 100644 tests/cases/fourslash/completionListInTypeLiteralInTypeParameter15.ts create mode 100644 tests/cases/fourslash/completionListInTypeLiteralInTypeParameter16.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c5701087ebfd4..790f97db3eb8e 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -42606,6 +42606,48 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return undefined; } + function getSignaturesFromCallLike(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) { @@ -42644,12 +42686,62 @@ 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 ( + "typeArguments" in node.parent && // eslint-disable-line local/no-in-operator + 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( + getSignaturesFromCallLike(node.parent), + typeArgumentPosition, + ); + } + + if (isDecorator(node.parent.parent)) { + return getTypeParameterConstraintForPositionAcrossSignatures( + getSignaturesFromCallLike(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) { diff --git a/src/services/completions.ts b/src/services/completions.ts index dc01ea8ede4b9..9e90922d445d4 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -256,7 +256,6 @@ import { isTypeOnlyImportDeclaration, isTypeOnlyImportOrExportDeclaration, isTypeParameterDeclaration, - isTypeReferenceType, isValidTypeOnlyAliasUseSite, isVariableDeclaration, isVariableLike, @@ -5769,8 +5768,9 @@ function tryGetTypeLiteralNode(node: Node): TypeLiteralNode | undefined { 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); diff --git a/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter10.ts b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter10.ts new file mode 100644 index 0000000000000..892e88a882558 --- /dev/null +++ b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter10.ts @@ -0,0 +1,38 @@ +/// + +////interface Foo { +//// one: string; +//// two: number; +////} +////interface Bar { +//// three: boolean; +//// four: { +//// five: unknown; +//// }; +////} +//// +////function a() {} +////a<{/*0*/}>(); +//// +////var b = () => () => {}; +////b()<{/*1*/}>(); +//// +////declare function c(): void +////declare function c(): void +////c<{/*2*/}>(); +//// +////function d() {} +////d<{/*3*/}, {/*4*/}>(); +////d(); +//// +////(() => {})<{/*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 }, +); diff --git a/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter11.ts b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter11.ts new file mode 100644 index 0000000000000..634746dd38582 --- /dev/null +++ b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter11.ts @@ -0,0 +1,32 @@ +/// + +////interface Foo { +//// one: string; +//// two: number; +////} +////interface Bar { +//// three: boolean; +//// four: symbol; +////} +//// +////class A {} +////new A<{/*0*/}>(); +//// +////class B {} +////new B<{/*1*/}, {/*2*/}>(); +//// +////declare const C: { +//// new (): unknown +//// new (): unknown +////} +////new C<{/*3*/}>() +//// +////new (class {})<{/*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 }, +); diff --git a/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter12.ts b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter12.ts new file mode 100644 index 0000000000000..9bf8ec21afaae --- /dev/null +++ b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter12.ts @@ -0,0 +1,25 @@ +/// + +////interface Foo { +//// kind: 'foo'; +//// one: string; +////} +////interface Bar { +//// kind: 'bar'; +//// two: number; +////} +//// +////declare function a(): void +////declare function a(): void +////a<{ kind: 'bar', /*0*/ }>(); +//// +////declare function b(kind: 'foo'): void +////declare function b(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 }, +); diff --git a/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter13.ts b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter13.ts new file mode 100644 index 0000000000000..5797134b2b15e --- /dev/null +++ b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter13.ts @@ -0,0 +1,18 @@ +/// + +// @jsx: preserve +// @filename: a.tsx +////interface Foo { +//// one: string; +//// two: number; +////} +//// +////const Component = () => <>; +//// +////>; +/////>; + +verify.completions( + { marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true }, + { marker: "1", unsorted: ["one", "two"], isNewIdentifierLocation: true }, +); diff --git a/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter14.ts b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter14.ts new file mode 100644 index 0000000000000..79aa45875715e --- /dev/null +++ b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter14.ts @@ -0,0 +1,10 @@ +/// + +////interface Foo { +//// one: string; +//// two: number; +////} +////declare function f(x: TemplateStringsArray): void; +////f<{/*0*/}>``; + +verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true }); diff --git a/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter15.ts b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter15.ts new file mode 100644 index 0000000000000..352facb90a46d --- /dev/null +++ b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter15.ts @@ -0,0 +1,15 @@ +/// + +////interface Foo { +//// one: string; +//// two: number; +////} +//// +////declare function decorator(originalMethod: unknown, _context: unknown): never +//// +////class { +//// @decorator<{/*0*/}> +//// method() {} +////} + +verify.completions({ marker: "0", unsorted: ["one", "two"], isNewIdentifierLocation: true }); diff --git a/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter16.ts b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter16.ts new file mode 100644 index 0000000000000..4c32c2d19fc22 --- /dev/null +++ b/tests/cases/fourslash/completionListInTypeLiteralInTypeParameter16.ts @@ -0,0 +1,35 @@ +/// + +////interface Foo { +//// one: string; +//// two: number; +////} +////interface Bar { +//// three: boolean; +//// four: { +//// five: unknown; +//// }; +////} +//// +////(() => {})<{/*0*/}>; +//// +////(class {})<{/*1*/}>; +//// +////declare const a: { +//// new (): {}; +//// (): {}; +////} +////a<{/*2*/}>; +//// +////declare const b: { +//// new (): {}; +//// (): {}; +////} +////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 }, +);