Skip to content

Commit 69abe49

Browse files
author
Andy
authored
Supports more locations for completions contextual types (#21946)
1 parent 9ee51fa commit 69abe49

File tree

6 files changed

+80
-31
lines changed

6 files changed

+80
-31
lines changed

src/compiler/checker.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ namespace ts {
191191
node = getParseTreeNode(node, isExpression);
192192
return node ? getContextualType(node) : undefined;
193193
},
194+
getContextualTypeForArgumentAtIndex: (node, argIndex) => {
195+
node = getParseTreeNode(node, isCallLikeExpression);
196+
return node && getContextualTypeForArgumentAtIndex(node, argIndex);
197+
},
198+
getContextualTypeForJsxAttribute: (node) => {
199+
node = getParseTreeNode(node, isJsxAttributeLike);
200+
return node && getContextualTypeForJsxAttribute(node);
201+
},
194202
isContextSensitive,
195203
getFullyQualifiedName,
196204
getResolvedSignature: (node, candidatesOutArray, theArgumentCount) => {
@@ -14183,14 +14191,15 @@ namespace ts {
1418314191
// In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter.
1418414192
function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression): Type {
1418514193
const args = getEffectiveCallArguments(callTarget);
14186-
const argIndex = args.indexOf(arg);
14187-
if (argIndex >= 0) {
14188-
// If we're already in the process of resolving the given signature, don't resolve again as
14189-
// that could cause infinite recursion. Instead, return anySignature.
14190-
const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget);
14191-
return getTypeAtPosition(signature, argIndex);
14192-
}
14193-
return undefined;
14194+
const argIndex = args.indexOf(arg); // -1 for e.g. the expression of a CallExpression, or the tag of a TaggedTemplateExpression
14195+
return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex);
14196+
}
14197+
14198+
function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number): Type {
14199+
// If we're already in the process of resolving the given signature, don't resolve again as
14200+
// that could cause infinite recursion. Instead, return anySignature.
14201+
const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget);
14202+
return getTypeAtPosition(signature, argIndex);
1419414203
}
1419514204

1419614205
function getContextualTypeForSubstitutionExpression(template: TemplateExpression, substitutionExpression: Expression) {
@@ -14324,7 +14333,7 @@ namespace ts {
1432414333
: undefined;
1432514334
}
1432614335

14327-
function getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute) {
14336+
function getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute): Type | undefined {
1432814337
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give its attributes a contextual type
1432914338
// which is a type of the parameter of the signature we are trying out.
1433014339
// If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2835,6 +2835,8 @@ namespace ts {
28352835
getAugmentedPropertiesOfType(type: Type): Symbol[];
28362836
getRootSymbols(symbol: Symbol): Symbol[];
28372837
getContextualType(node: Expression): Type | undefined;
2838+
/* @internal */ getContextualTypeForArgumentAtIndex(call: CallLikeExpression, argIndex: number): Type;
2839+
/* @internal */ getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute): Type | undefined;
28382840
/* @internal */ isContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike): boolean;
28392841

28402842
/**

src/services/completions.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -657,32 +657,46 @@ namespace ts.Completions {
657657
None,
658658
}
659659

660-
function getRecommendedCompletion(currentToken: Node, checker: TypeChecker): Symbol | undefined {
661-
const ty = getContextualType(currentToken, checker);
660+
function getRecommendedCompletion(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Symbol | undefined {
661+
const ty = getContextualType(currentToken, position, sourceFile, checker);
662662
const symbol = ty && ty.symbol;
663663
// Don't include make a recommended completion for an abstract class
664664
return symbol && (symbol.flags & SymbolFlags.Enum || symbol.flags & SymbolFlags.Class && !isAbstractConstructorSymbol(symbol))
665665
? getFirstSymbolInChain(symbol, currentToken, checker)
666666
: undefined;
667667
}
668668

669-
function getContextualType(currentToken: Node, checker: ts.TypeChecker): Type | undefined {
669+
function getContextualType(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined {
670670
const { parent } = currentToken;
671671
switch (currentToken.kind) {
672-
case ts.SyntaxKind.Identifier:
673-
return getContextualTypeFromParent(currentToken as ts.Identifier, checker);
674-
case ts.SyntaxKind.EqualsToken:
675-
return ts.isVariableDeclaration(parent) ? checker.getContextualType(parent.initializer) :
676-
ts.isBinaryExpression(parent) ? checker.getTypeAtLocation(parent.left) : undefined;
677-
case ts.SyntaxKind.NewKeyword:
678-
return checker.getContextualType(parent as ts.Expression);
679-
case ts.SyntaxKind.CaseKeyword:
680-
return getSwitchedType(cast(currentToken.parent, isCaseClause), checker);
672+
case SyntaxKind.Identifier:
673+
return getContextualTypeFromParent(currentToken as Identifier, checker);
674+
case SyntaxKind.EqualsToken:
675+
switch (parent.kind) {
676+
case ts.SyntaxKind.VariableDeclaration:
677+
return checker.getContextualType((parent as VariableDeclaration).initializer);
678+
case ts.SyntaxKind.BinaryExpression:
679+
return checker.getTypeAtLocation((parent as BinaryExpression).left);
680+
case ts.SyntaxKind.JsxAttribute:
681+
return checker.getContextualTypeForJsxAttribute(parent as JsxAttribute);
682+
default:
683+
return undefined;
684+
}
685+
case SyntaxKind.NewKeyword:
686+
return checker.getContextualType(parent as Expression);
687+
case SyntaxKind.CaseKeyword:
688+
return getSwitchedType(cast(parent, isCaseClause), checker);
689+
case SyntaxKind.OpenBraceToken:
690+
return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined;
681691
default:
682-
return isEqualityOperatorKind(currentToken.kind) && ts.isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
692+
const argInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(currentToken, position, sourceFile);
693+
return argInfo
694+
// At `,`, treat this as the next argument after the comma.
695+
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0))
696+
: isEqualityOperatorKind(currentToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
683697
// completion at `x ===/**/` should be for the right side
684698
? checker.getTypeAtLocation(parent.left)
685-
: checker.getContextualType(currentToken as ts.Expression);
699+
: checker.getContextualType(currentToken as Expression);
686700
}
687701
}
688702

@@ -956,7 +970,7 @@ namespace ts.Completions {
956970

957971
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
958972

959-
const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, typeChecker);
973+
const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, position, sourceFile, typeChecker);
960974
return { kind: CompletionDataKind.Data, symbols, completionKind, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer };
961975

962976
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;

src/services/signatureHelp.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ namespace ts.SignatureHelp {
9595
* Returns relevant information for the argument list and the current argument if we are
9696
* in the argument of an invocation; returns undefined otherwise.
9797
*/
98-
export function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo {
98+
export function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined {
9999
if (isCallOrNewExpression(node.parent)) {
100100
const invocation = node.parent;
101101
let list: Node;
@@ -207,8 +207,7 @@ namespace ts.SignatureHelp {
207207
// that trailing comma in the list, and we'll have generated the appropriate
208208
// arg index.
209209
let argumentIndex = 0;
210-
const listChildren = argumentsList.getChildren();
211-
for (const child of listChildren) {
210+
for (const child of argumentsList.getChildren()) {
212211
if (child === node) {
213212
break;
214213
}
@@ -270,9 +269,7 @@ namespace ts.SignatureHelp {
270269

271270
function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number, sourceFile: SourceFile): ArgumentListInfo {
272271
// argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument.
273-
const argumentCount = tagExpression.template.kind === SyntaxKind.NoSubstitutionTemplateLiteral
274-
? 1
275-
: (<TemplateExpression>tagExpression.template).templateSpans.length + 1;
272+
const argumentCount = isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1;
276273

277274
if (argumentIndex !== 0) {
278275
Debug.assertLessThan(argumentIndex, argumentCount);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @jsx: preserve
4+
5+
// @Filename: /a.tsx
6+
////enum E {}
7+
////enum F {}
8+
////function f(e: E, f: F) {}
9+
////f(/*arg0*/, /*arg1*/);
10+
////
11+
////function tag(arr: TemplateStringsArray, x: E) {}
12+
////tag`${/*tag*/}`;
13+
////
14+
////declare function MainButton(props: { e: E }): any;
15+
////<MainButton e={/*jsx*/} />
16+
////<MainButton e=/*jsx2*/ />
17+
18+
recommended("arg0");
19+
recommended("arg1", "F");
20+
recommended("tag");
21+
recommended("jsx");
22+
recommended("jsx2");
23+
24+
function recommended(markerName: string, enumName = "E") {
25+
goTo.marker(markerName);
26+
verify.completionListContains(enumName, `enum ${enumName}`, "", "enum", undefined, undefined , { isRecommended: true });
27+
}

tests/cases/fourslash/signatureHelpIncompleteCalls.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ verify.currentSignatureParameterCountIs(2);
2727
verify.currentSignatureHelpIs("f3(n: number, s: string): string");
2828

2929
verify.currentParameterHelpArgumentNameIs("s");
30-
verify.currentParameterSpanIs("s: string");
30+
verify.currentParameterSpanIs("s: string");
3131

0 commit comments

Comments
 (0)