-
Notifications
You must be signed in to change notification settings - Fork 13k
Enhance type argument completions #62170
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
base: main
Are you sure you want to change the base?
Changes from all commits
65296fe
c086eb4
2136987
d5bbf02
e8f8ef9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -420,6 +420,7 @@ import { | |
hasSyntacticModifier, | ||
hasSyntacticModifiers, | ||
hasType, | ||
hasTypeArguments, | ||
HeritageClause, | ||
hostGetCanonicalFileName, | ||
Identifier, | ||
|
@@ -42624,6 +42625,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) { | ||
|
@@ -42662,12 +42705,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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We typically avoid these kinds of checks; is there no helper that achieves the same thing? Or at least, a switch case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was originally calling However, some code change I made in the meantime must have sidestepped those issues, as I just tried I'll push a commit switching back to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }, | ||
); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels suspicious that this is the first time we'd need a func like this; there's nothing else that does this? Not just
getResolvedSignature
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I need the original un-instantiated generic signatures, but
getResolvedSignature
always gives me concrete ones, even withCheckMode.SkipGenericFunctions
.When asking for completions there is a (possibly-incomplete) type argument already present (the node looks something like
f<{}>()
). I tried artificially clearing out the type argument list before callinggetResolvedSignature
, but that still gives me concrete signatures (with type parameters substituted for their constraints). I traced the logic for regular function calls and in that case concretion occurs withinresolveCall
(the call stack isgetResolvedSignature
→resolveSignature
→resolveCallExpression
→resolveCall
).I searched for other existing utilities that would meet my needs but came up empty. It's entirely possible I missed something though; I wasn't sure exactly what to look for.
Maybe I could rename
getSignaturesFromCallLike
to clarify how it's different fromgetResolvedSignature
. Something likegetUninstantiatedSignatures
, perhaps? A docblock might also help. Let me know if you have any specific suggestions on these fronts.