Skip to content

Commit

Permalink
Supports more types for recommended completions
Browse files Browse the repository at this point in the history
  • Loading branch information
Andy Hanson committed Feb 14, 2018
1 parent 47d84f8 commit 9c561d2
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 32 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 @@ -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
4 changes: 4 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2835,6 +2835,10 @@ 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
43 changes: 28 additions & 15 deletions src/services/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,32 +657,45 @@ 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
? checker.getContextualTypeForArgumentAtIndex(argInfo.invocation, argInfo.argumentIndex)
: 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 +969,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
14 changes: 6 additions & 8 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 @@ -136,7 +136,7 @@ namespace ts.SignatureHelp {

const kind = invocation.typeArguments && invocation.typeArguments.pos === list.pos ? ArgumentListKind.TypeArguments : ArgumentListKind.CallArguments;
const argumentCount = getArgumentCount(list);
if (argumentIndex !== 0) {
if (argumentIndex !== 0 && node.kind !== SyntaxKind.CommaToken) {
Debug.assertLessThan(argumentIndex, argumentCount);
}
const argumentsSpan = getApplicableSpanForArguments(list, sourceFile);
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 All @@ -217,7 +216,8 @@ namespace ts.SignatureHelp {
}
}

return argumentIndex;
// At `,`, treat this as the next argument after the comma.
return node.kind === SyntaxKind.CommaToken ? argumentIndex + 1 : argumentIndex;
}

function getArgumentCount(argumentsList: Node) {
Expand Down Expand Up @@ -270,9 +270,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 });
}

0 comments on commit 9c561d2

Please sign in to comment.