Skip to content
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

Supports more locations for completions contextual types #21946

Merged
1 commit merged into from
Feb 17, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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) {
Expand Down Expand Up @@ -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
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");