Skip to content

Commit

Permalink
Supports more locations for completions contextual types (#21946)
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy authored Feb 17, 2018
1 parent 9ee51fa commit 69abe49
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 31 deletions.
27 changes: 18 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -14183,14 +14191,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) {
Expand Down Expand Up @@ -14324,7 +14333,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
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down
44 changes: 29 additions & 15 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,32 +657,46 @@ 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))
? getFirstSymbolInChain(symbol, currentToken, checker)
: 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);
}
}

Expand Down Expand Up @@ -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;
Expand Down
9 changes: 3 additions & 6 deletions src/services/signatureHelp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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
: (<TemplateExpression>tagExpression.template).templateSpans.length + 1;
const argumentCount = isNoSubstitutionTemplateLiteral(tagExpression.template) ? 1 : tagExpression.template.templateSpans.length + 1;

if (argumentIndex !== 0) {
Debug.assertLessThan(argumentIndex, argumentCount);
Expand Down
27 changes: 27 additions & 0 deletions tests/cases/fourslash/completionsRecommended_contextualTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/// <reference path="fourslash.ts" />

// @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;
////<MainButton e={/*jsx*/} />
////<MainButton e=/*jsx2*/ />

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 });
}
2 changes: 1 addition & 1 deletion tests/cases/fourslash/signatureHelpIncompleteCalls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

0 comments on commit 69abe49

Please sign in to comment.