diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d62e2157b0701..5925a7554d889 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -191,6 +191,14 @@ namespace ts { node = getParseTreeNode(node, isExpression); return node ? getContextualType(node) : undefined; }, + getContextualTypeForArgumentAtIndex: (node, argIndex) => { + node = getParseTreeNode(node, isCallLikeExpression); + return node && getContextualTypeForArgumentAtIndex(node, argIndex); + }, + getContextualTypeForJsxAttribute: (node) => { + node = getParseTreeNode(node, isJsxAttributeLike); + return node && getContextualTypeForJsxAttribute(node); + }, isContextSensitive, getFullyQualifiedName, getResolvedSignature: (node, candidatesOutArray, theArgumentCount) => { @@ -14185,14 +14193,15 @@ namespace ts { // In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter. function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression): Type { const args = getEffectiveCallArguments(callTarget); - const argIndex = args.indexOf(arg); - if (argIndex >= 0) { - // If we're already in the process of resolving the given signature, don't resolve again as - // that could cause infinite recursion. Instead, return anySignature. - const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget); - return getTypeAtPosition(signature, argIndex); - } - return undefined; + const argIndex = args.indexOf(arg); // -1 for e.g. the expression of a CallExpression, or the tag of a TaggedTemplateExpression + return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex); + } + + function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number): Type { + // If we're already in the process of resolving the given signature, don't resolve again as + // that could cause infinite recursion. Instead, return anySignature. + const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget); + return getTypeAtPosition(signature, argIndex); } function getContextualTypeForSubstitutionExpression(template: TemplateExpression, substitutionExpression: Expression) { @@ -14326,7 +14335,7 @@ namespace ts { : undefined; } - function getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute) { + function getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute): Type | undefined { // When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give its attributes a contextual type // which is a type of the parameter of the signature we are trying out. // If there is no contextual type (e.g. we are trying to resolve stateful component), get attributes type from resolving element's tagName diff --git a/src/compiler/types.ts b/src/compiler/types.ts index edfb7868ee71e..21cbb209b2c21 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2835,6 +2835,8 @@ namespace ts { getAugmentedPropertiesOfType(type: Type): Symbol[]; getRootSymbols(symbol: Symbol): Symbol[]; getContextualType(node: Expression): Type | undefined; + /* @internal */ getContextualTypeForArgumentAtIndex(call: CallLikeExpression, argIndex: number): Type; + /* @internal */ getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute): Type | undefined; /* @internal */ isContextSensitive(node: Expression | MethodDeclaration | ObjectLiteralElementLike | JsxAttributeLike): boolean; /** diff --git a/src/services/completions.ts b/src/services/completions.ts index 4e9a9d1485e07..f765677d215f7 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -657,8 +657,8 @@ namespace ts.Completions { None, } - function getRecommendedCompletion(currentToken: Node, checker: TypeChecker): Symbol | undefined { - const ty = getContextualType(currentToken, checker); + function getRecommendedCompletion(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Symbol | undefined { + const ty = getContextualType(currentToken, position, sourceFile, checker); const symbol = ty && ty.symbol; // Don't include make a recommended completion for an abstract class return symbol && (symbol.flags & SymbolFlags.Enum || symbol.flags & SymbolFlags.Class && !isAbstractConstructorSymbol(symbol)) @@ -666,23 +666,37 @@ namespace ts.Completions { : undefined; } - function getContextualType(currentToken: Node, checker: ts.TypeChecker): Type | undefined { + function getContextualType(currentToken: Node, position: number, sourceFile: SourceFile, checker: TypeChecker): Type | undefined { const { parent } = currentToken; switch (currentToken.kind) { - case ts.SyntaxKind.Identifier: - return getContextualTypeFromParent(currentToken as ts.Identifier, checker); - case ts.SyntaxKind.EqualsToken: - return ts.isVariableDeclaration(parent) ? checker.getContextualType(parent.initializer) : - ts.isBinaryExpression(parent) ? checker.getTypeAtLocation(parent.left) : undefined; - case ts.SyntaxKind.NewKeyword: - return checker.getContextualType(parent as ts.Expression); - case ts.SyntaxKind.CaseKeyword: - return getSwitchedType(cast(currentToken.parent, isCaseClause), checker); + case SyntaxKind.Identifier: + return getContextualTypeFromParent(currentToken as Identifier, checker); + case SyntaxKind.EqualsToken: + switch (parent.kind) { + case ts.SyntaxKind.VariableDeclaration: + return checker.getContextualType((parent as VariableDeclaration).initializer); + case ts.SyntaxKind.BinaryExpression: + return checker.getTypeAtLocation((parent as BinaryExpression).left); + case ts.SyntaxKind.JsxAttribute: + return checker.getContextualTypeForJsxAttribute(parent as JsxAttribute); + default: + return undefined; + } + case SyntaxKind.NewKeyword: + return checker.getContextualType(parent as Expression); + case SyntaxKind.CaseKeyword: + return getSwitchedType(cast(parent, isCaseClause), checker); + case SyntaxKind.OpenBraceToken: + return isJsxExpression(parent) && parent.parent.kind !== SyntaxKind.JsxElement ? checker.getContextualTypeForJsxAttribute(parent.parent) : undefined; default: - return isEqualityOperatorKind(currentToken.kind) && ts.isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) + const argInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(currentToken, position, sourceFile); + return argInfo + // At `,`, treat this as the next argument after the comma. + ? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex + (currentToken.kind === SyntaxKind.CommaToken ? 1 : 0)) + : isEqualityOperatorKind(currentToken.kind) && isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind) // completion at `x ===/**/` should be for the right side ? checker.getTypeAtLocation(parent.left) - : checker.getContextualType(currentToken as ts.Expression); + : checker.getContextualType(currentToken as Expression); } } @@ -956,7 +970,7 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, typeChecker); + const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, position, sourceFile, typeChecker); return { kind: CompletionDataKind.Data, symbols, completionKind, propertyAccessToConvert, isNewIdentifierLocation, location, keywordFilters, symbolToOriginInfoMap, recommendedCompletion, previousToken, isJsxInitializer }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index aa30ba0de6fc5..c96d621dd24e6 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -95,7 +95,7 @@ namespace ts.SignatureHelp { * Returns relevant information for the argument list and the current argument if we are * in the argument of an invocation; returns undefined otherwise. */ - export function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo { + export function getImmediatelyContainingArgumentInfo(node: Node, position: number, sourceFile: SourceFile): ArgumentListInfo | undefined { if (isCallOrNewExpression(node.parent)) { const invocation = node.parent; let list: Node; @@ -207,8 +207,7 @@ namespace ts.SignatureHelp { // that trailing comma in the list, and we'll have generated the appropriate // arg index. let argumentIndex = 0; - const listChildren = argumentsList.getChildren(); - for (const child of listChildren) { + for (const child of argumentsList.getChildren()) { if (child === node) { break; } @@ -270,9 +269,7 @@ namespace ts.SignatureHelp { function getArgumentListInfoForTemplate(tagExpression: TaggedTemplateExpression, argumentIndex: number, sourceFile: SourceFile): ArgumentListInfo { // argumentCount is either 1 or (numSpans + 1) to account for the template strings array argument. - const argumentCount = tagExpression.template.kind === SyntaxKind.NoSubstitutionTemplateLiteral - ? 1 - : (tagExpression.template).templateSpans.length + 1; + const argumentCount = isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1; if (argumentIndex !== 0) { Debug.assertLessThan(argumentIndex, argumentCount); diff --git a/tests/cases/fourslash/completionsRecommended_contextualTypes.ts b/tests/cases/fourslash/completionsRecommended_contextualTypes.ts new file mode 100644 index 0000000000000..d5d7f50c30b1e --- /dev/null +++ b/tests/cases/fourslash/completionsRecommended_contextualTypes.ts @@ -0,0 +1,27 @@ +/// + +// @jsx: preserve + +// @Filename: /a.tsx +////enum E {} +////enum F {} +////function f(e: E, f: F) {} +////f(/*arg0*/, /*arg1*/); +//// +////function tag(arr: TemplateStringsArray, x: E) {} +////tag`${/*tag*/}`; +//// +////declare function MainButton(props: { e: E }): any; +//// +//// + +recommended("arg0"); +recommended("arg1", "F"); +recommended("tag"); +recommended("jsx"); +recommended("jsx2"); + +function recommended(markerName: string, enumName = "E") { + goTo.marker(markerName); + verify.completionListContains(enumName, `enum ${enumName}`, "", "enum", undefined, undefined , { isRecommended: true }); +} diff --git a/tests/cases/fourslash/signatureHelpIncompleteCalls.ts b/tests/cases/fourslash/signatureHelpIncompleteCalls.ts index ec4fcccc0fe5c..7403b98733db0 100644 --- a/tests/cases/fourslash/signatureHelpIncompleteCalls.ts +++ b/tests/cases/fourslash/signatureHelpIncompleteCalls.ts @@ -27,5 +27,5 @@ verify.currentSignatureParameterCountIs(2); verify.currentSignatureHelpIs("f3(n: number, s: string): string"); verify.currentParameterHelpArgumentNameIs("s"); -verify.currentParameterSpanIs("s: string"); +verify.currentParameterSpanIs("s: string");