Skip to content

Commit a71011d

Browse files
author
Andy Hanson
committed
Support completions contextual types in more places
1 parent 60bd262 commit a71011d

9 files changed

+167
-106
lines changed

Diff for: src/compiler/checker.ts

+2-18
Original file line numberDiff line numberDiff line change
@@ -13928,7 +13928,7 @@ namespace ts {
1392813928
// the contextual type of an initializer expression is the type annotation of the containing declaration, if present.
1392913929
function getContextualTypeForInitializerExpression(node: Expression): Type {
1393013930
const declaration = <VariableLikeDeclaration>node.parent;
13931-
if (hasInitializer(declaration) && node === declaration.initializer || node.kind === SyntaxKind.EqualsToken) {
13931+
if (hasInitializer(declaration) && node === declaration.initializer) {
1393213932
const typeNode = getEffectiveTypeAnnotationNode(declaration);
1393313933
if (typeNode) {
1393413934
return getTypeFromTypeNode(typeNode);
@@ -14060,12 +14060,6 @@ namespace ts {
1406014060
case SyntaxKind.AmpersandAmpersandToken:
1406114061
case SyntaxKind.CommaToken:
1406214062
return node === right ? getContextualType(binaryExpression) : undefined;
14063-
case SyntaxKind.EqualsEqualsEqualsToken:
14064-
case SyntaxKind.EqualsEqualsToken:
14065-
case SyntaxKind.ExclamationEqualsEqualsToken:
14066-
case SyntaxKind.ExclamationEqualsToken:
14067-
// For completions after `x === `
14068-
return node === operatorToken ? getTypeOfExpression(binaryExpression.left) : undefined;
1406914063
default:
1407014064
return undefined;
1407114065
}
@@ -14281,12 +14275,8 @@ namespace ts {
1428114275
return getContextualTypeForReturnExpression(node);
1428214276
case SyntaxKind.YieldExpression:
1428314277
return getContextualTypeForYieldOperand(<YieldExpression>parent);
14278+
case SyntaxKind.CallExpression:
1428414279
case SyntaxKind.NewExpression:
14285-
if (node.kind === SyntaxKind.NewKeyword) { // for completions after `new `
14286-
return getContextualType(parent as NewExpression);
14287-
}
14288-
// falls through
14289-
case SyntaxKind.CallExpression:
1429014280
return getContextualTypeForArgument(<CallExpression | NewExpression>parent, node);
1429114281
case SyntaxKind.TypeAssertionExpression:
1429214282
case SyntaxKind.AsExpression:
@@ -14321,12 +14311,6 @@ namespace ts {
1432114311
case SyntaxKind.JsxOpeningElement:
1432214312
case SyntaxKind.JsxSelfClosingElement:
1432314313
return getAttributesTypeFromJsxOpeningLikeElement(<JsxOpeningLikeElement>parent);
14324-
case SyntaxKind.CaseClause: {
14325-
if (node.kind === SyntaxKind.CaseKeyword) { // for completions after `case `
14326-
const switchStatement = (parent as CaseClause).parent.parent;
14327-
return getTypeOfExpression(switchStatement.expression);
14328-
}
14329-
}
1433014314
}
1433114315
return undefined;
1433214316
}

Diff for: src/harness/fourslash.ts

+9-7
Original file line numberDiff line numberDiff line change
@@ -441,12 +441,11 @@ namespace FourSlash {
441441
this.goToPosition(marker.position);
442442
}
443443

444-
public goToEachMarker(action: () => void) {
445-
const markers = this.getMarkers();
444+
public goToEachMarker(markers: ReadonlyArray<Marker>, action: (marker: FourSlash.Marker, index: number) => void) {
446445
assert(markers.length);
447-
for (const marker of markers) {
448-
this.goToMarker(marker);
449-
action();
446+
for (let i = 0; i < markers.length; i++) {
447+
this.goToMarker(markers[i]);
448+
action(markers[i], i);
450449
}
451450
}
452451

@@ -3764,8 +3763,11 @@ namespace FourSlashInterface {
37643763
this.state.goToMarker(name);
37653764
}
37663765

3767-
public eachMarker(action: () => void) {
3768-
this.state.goToEachMarker(action);
3766+
public eachMarker(markers: ReadonlyArray<string>, action: (marker: FourSlash.Marker, index: number) => void): void;
3767+
public eachMarker(action: (marker: FourSlash.Marker, index: number) => void): void;
3768+
public eachMarker(a: ReadonlyArray<string> | ((marker: FourSlash.Marker, index: number) => void), b?: (marker: FourSlash.Marker, index: number) => void): void {
3769+
const markers = typeof a === "function" ? this.state.getMarkers() : a.map(m => this.state.getMarkerByName(m));
3770+
this.state.goToEachMarker(markers, typeof a === "function" ? a : b);
37693771
}
37703772

37713773
public rangeStart(range: FourSlash.Range) {

Diff for: src/services/completions.ts

+60-26
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ namespace ts.Completions {
238238

239239
function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined {
240240
const node = findPrecedingToken(position, sourceFile);
241-
if (!node || node.kind !== SyntaxKind.StringLiteral) {
241+
if (!node || !isStringLiteral(node)) {
242242
return undefined;
243243
}
244244

@@ -280,18 +280,6 @@ namespace ts.Completions {
280280
const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(<StringLiteral>node, compilerOptions, host, typeChecker);
281281
return pathCompletionsInfo(entries);
282282
}
283-
else if (isEqualityExpression(node.parent)) {
284-
// Get completions from the type of the other operand
285-
// i.e. switch (a) {
286-
// case '/*completion position*/'
287-
// }
288-
return getStringLiteralCompletionEntriesFromType(typeChecker.getTypeAtLocation(node.parent.left === node ? node.parent.right : node.parent.left), typeChecker);
289-
}
290-
else if (isCaseOrDefaultClause(node.parent)) {
291-
// Get completions from the type of the switch expression
292-
// i.e. x === '/*completion position'
293-
return getStringLiteralCompletionEntriesFromType(typeChecker.getTypeAtLocation((<SwitchStatement>node.parent.parent.parent).expression), typeChecker);
294-
}
295283
else {
296284
const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile);
297285
if (argumentInfo) {
@@ -303,7 +291,7 @@ namespace ts.Completions {
303291

304292
// Get completion for string literal from string literal type
305293
// i.e. var x: "hi" | "hello" = "/*completion position*/"
306-
return getStringLiteralCompletionEntriesFromType(typeChecker.getContextualType(<StringLiteral>node), typeChecker);
294+
return getStringLiteralCompletionEntriesFromType(getContextualTypeFromParent(node, typeChecker), typeChecker);
307295
}
308296
}
309297

@@ -600,15 +588,60 @@ namespace ts.Completions {
600588
}
601589
type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag };
602590

603-
function getRecommendedCompletion(currentToken: Node, checker: TypeChecker/*, symbolToOriginInfoMap: SymbolOriginInfoMap*/): Symbol | undefined {
604-
const ty = checker.getContextualType(currentToken as Expression);
591+
function getRecommendedCompletion(currentToken: Node, checker: TypeChecker): Symbol | undefined {
592+
const ty = getContextualType(currentToken, checker);
605593
const symbol = ty && ty.symbol;
606594
// Don't include make a recommended completion for an abstract class
607595
return symbol && (symbol.flags & SymbolFlags.Enum || symbol.flags & SymbolFlags.Class && !isAbstractConstructorSymbol(symbol))
608596
? getFirstSymbolInChain(symbol, currentToken, checker)
609597
: undefined;
610598
}
611599

600+
function getContextualType(currentToken: Node, checker: ts.TypeChecker): Type | undefined {
601+
const { parent } = currentToken;
602+
switch (currentToken.kind) {
603+
case ts.SyntaxKind.Identifier:
604+
return getContextualTypeFromParent(currentToken as ts.Identifier, checker);
605+
case ts.SyntaxKind.EqualsToken:
606+
return ts.isVariableDeclaration(parent)
607+
? checker.getContextualType(parent.initializer)
608+
: ts.isBinaryExpression(parent)
609+
? checker.getTypeAtLocation(parent.left)
610+
: undefined;
611+
case ts.SyntaxKind.NewKeyword:
612+
return checker.getContextualType(parent as ts.Expression);
613+
case ts.SyntaxKind.CaseKeyword:
614+
return getSwitchedType(cast(currentToken.parent, isCaseClause), checker);
615+
default:
616+
return isEqualityOperatorKind(currentToken.kind) && ts.isBinaryExpression(parent) && isEqualityOperatorKind(parent.operatorToken.kind)
617+
// completion at `x ===/**/` should be for the right side
618+
? checker.getTypeAtLocation(parent.left)
619+
: checker.getContextualType(currentToken as ts.Expression);
620+
}
621+
}
622+
623+
function getContextualTypeFromParent(node: ts.Expression, checker: ts.TypeChecker): Type | undefined {
624+
const { parent } = node;
625+
switch (parent.kind) {
626+
case ts.SyntaxKind.NewExpression:
627+
return checker.getContextualType(parent as ts.NewExpression);
628+
case ts.SyntaxKind.BinaryExpression: {
629+
const { left, operatorToken, right } = parent as ts.BinaryExpression;
630+
return isEqualityOperatorKind(operatorToken.kind)
631+
? checker.getTypeAtLocation(node === right ? left : right)
632+
: checker.getContextualType(node);
633+
}
634+
case ts.SyntaxKind.CaseClause:
635+
return (parent as ts.CaseClause).expression === node ? getSwitchedType(parent as ts.CaseClause, checker) : undefined;
636+
default:
637+
return checker.getContextualType(node);
638+
}
639+
}
640+
641+
function getSwitchedType(caseClause: ts.CaseClause, checker: ts.TypeChecker): ts.Type {
642+
return checker.getTypeAtLocation(caseClause.parent.parent.expression);
643+
}
644+
612645
function getFirstSymbolInChain(symbol: Symbol, enclosingDeclaration: Node, checker: TypeChecker): Symbol | undefined {
613646
const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ SymbolFlags.All, /*useOnlyExternalAliasing*/ false);
614647
if (chain) return first(chain);
@@ -848,7 +881,7 @@ namespace ts.Completions {
848881

849882
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
850883

851-
const recommendedCompletion = getRecommendedCompletion(previousToken, typeChecker);
884+
const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, typeChecker);
852885
return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters, symbolToOriginInfoMap, recommendedCompletion };
853886

854887
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
@@ -2078,15 +2111,16 @@ namespace ts.Completions {
20782111
return isConstructorParameterCompletionKeyword(stringToToken(text));
20792112
}
20802113

2081-
function isEqualityExpression(node: Node): node is BinaryExpression {
2082-
return isBinaryExpression(node) && isEqualityOperatorKind(node.operatorToken.kind);
2083-
}
2084-
2085-
function isEqualityOperatorKind(kind: SyntaxKind) {
2086-
return kind === SyntaxKind.EqualsEqualsToken ||
2087-
kind === SyntaxKind.ExclamationEqualsToken ||
2088-
kind === SyntaxKind.EqualsEqualsEqualsToken ||
2089-
kind === SyntaxKind.ExclamationEqualsEqualsToken;
2114+
function isEqualityOperatorKind(kind: ts.SyntaxKind): kind is EqualityOperator {
2115+
switch (kind) {
2116+
case ts.SyntaxKind.EqualsEqualsEqualsToken:
2117+
case ts.SyntaxKind.EqualsEqualsToken:
2118+
case ts.SyntaxKind.ExclamationEqualsEqualsToken:
2119+
case ts.SyntaxKind.ExclamationEqualsToken:
2120+
return true;
2121+
default:
2122+
return false;
2123+
}
20902124
}
20912125

20922126
/** Get the corresponding JSDocTag node if the position is in a jsDoc comment */
+7-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
/// <reference path="fourslash.ts" />
22

3-
////enum E {}
4-
////declare const e: E;
5-
////e === /**/
3+
////enum Enu {}
4+
////declare const e: Enu;
5+
////e === /*a*/;
6+
////e === E/*b*/
67

7-
goTo.marker();
8-
verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true });
8+
goTo.eachMarker(["a", "b"], () => {
9+
verify.completionListContains("Enu", "enum Enu", "", "enum", undefined, undefined, { isRecommended: true });
10+
});

Diff for: tests/cases/fourslash/completionsRecommended_import.ts

+25-14
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,36 @@
33
// @noLib: true
44

55
// @Filename: /a.ts
6-
////export class C {}
7-
////export function f(c: C) {}
6+
////export class Cls {}
7+
////export function f(c: Cls) {}
88

99
// @Filename: /b.ts
1010
////import { f } from "./a";
11-
// Here we will recommend a new import of 'C'
12-
////f(new /*b*/);
11+
// Here we will recommend a new import of 'Cls'
12+
////f(new C/*b0*/);
13+
////f(new /*b1*/);
1314

1415
// @Filename: /c.ts
15-
////import * as a from "./a";
16-
// Here we will recommend 'a' because it contains 'C'.
17-
////a.f(new /*c*/);
16+
////import * as alpha from "./a";
17+
// Here we will recommend 'alpha' because it contains 'Cls'.
18+
////alpha.f(new al/*c0*/);
19+
////alpha.f(new /*c1*/);
1820

19-
goTo.marker("b");
20-
verify.completionListContains({ name: "C", source: "/a" }, "class C", "", "class", undefined, /*hasAction*/ true, {
21-
includeExternalModuleExports: true,
22-
isRecommended: true,
23-
sourceDisplay: "./a",
21+
goTo.eachMarker(["b0", "b1"], (_, idx) => {
22+
verify.completionListContains(
23+
{ name: "Cls", source: "/a" },
24+
idx === 0 ? "constructor Cls(): Cls" : "class Cls",
25+
"",
26+
"class",
27+
undefined,
28+
/*hasAction*/ true, {
29+
includeExternalModuleExports: true,
30+
isRecommended: true,
31+
sourceDisplay: "./a",
32+
});
33+
});
34+
35+
goTo.eachMarker(["c0", "c1"], (_, idx) => {
36+
verify.completionListContains("alpha", "import alpha", "", "alias", undefined, undefined, { isRecommended: true })
2437
});
2538

26-
goTo.marker("c");
27-
verify.completionListContains("a", "import a", "", "alias", undefined, undefined, { isRecommended: true });
+32-13
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,37 @@
11
/// <reference path="fourslash.ts" />
22

3-
////enum E {}
4-
////class C {}
5-
////abstract class A {}
6-
////const e: E = /*e*/
7-
////const c: C = new /*c*/
8-
////const a: A = new /*a*/
3+
////enum Enu {}
4+
////class Cls {}
5+
////abstract class Abs {}
6+
////const e: Enu = E/*e0*/;
7+
////const e: Enu = /*e1*/;
8+
////const c: Cls = new C/*c0*/;
9+
////const c: Cls = new /*c1*/;
10+
////const a: Abs = new A/*a0*/;
11+
////const a: Abs = new /*a1*/;
912

10-
goTo.marker("e");
11-
verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true });
13+
// Also works on mutations
14+
////let enu: Enu;
15+
////enu = E/*let0*/;
16+
////enu = E/*let1*/;
1217

13-
goTo.marker("c");
14-
verify.completionListContains("C", "class C", "", "class", undefined, undefined, { isRecommended: true });
18+
goTo.eachMarker(["e0"], () => {//, "e1", "let0", "let1"
19+
verify.completionListContains("Enu", "enum Enu", "", "enum", undefined, undefined, { isRecommended: true });
20+
});
1521

16-
goTo.marker("a");
17-
// Not recommended, because it's an abstract class
18-
verify.completionListContains("A", "class A", "", "class");
22+
goTo.eachMarker(["c0", "c1"], (_, idx) => {
23+
verify.completionListContains(
24+
"Cls",
25+
idx === 0 ? "constructor Cls(): Cls" : "class Cls",
26+
"",
27+
"class",
28+
undefined,
29+
undefined, {
30+
isRecommended: true,
31+
});
32+
});
33+
34+
goTo.eachMarker(["a0", "a1"], (_, idx) => {
35+
// Not recommended, because it's an abstract class
36+
verify.completionListContains("Abs", idx == 0 ? "constructor Abs(): Abs" : "class Abs", "", "class");
37+
});

Diff for: tests/cases/fourslash/completionsRecommended_namespace.ts

+23-17
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,37 @@
33
// @noLib: true
44

55
// @Filename: /a.ts
6-
////export namespace N {
6+
////export namespace Name {
77
//// export class C {}
88
////}
9-
////export function f(c: N.C) {}
10-
////f(new /*a*/);
9+
////export function f(c: Name.C) {}
10+
////f(new N/*a0*/);
11+
////f(new /*a1*/);
1112

1213
// @Filename: /b.ts
1314
////import { f } from "./a";
14-
// Here we will recommend a new import of 'N'
15-
////f(new /*b*/);
15+
// Here we will recommend a new import of 'Name'
16+
////f(new N/*b0*/);
17+
////f(new /*b1*/);
1618

1719
// @Filename: /c.ts
18-
////import * as a from "./a";
19-
// Here we will recommend 'a' because it contains 'N' which contains 'C'.
20-
////a.f(new /*c*/);
20+
////import * as alpha from "./a";
21+
// Here we will recommend 'a' because it contains 'Name' which contains 'C'.
22+
////alpha.f(new a/*c0*/);
23+
////alpha.f(new /*c1*/);
2124

22-
goTo.marker("a");
23-
verify.completionListContains("N", "namespace N", "", "module", undefined, undefined, { isRecommended: true });
25+
goTo.eachMarker(["a0", "a1"], () => {
26+
verify.completionListContains("Name", "namespace Name", "", "module", undefined, undefined, { isRecommended: true });
27+
});
2428

25-
goTo.marker("b");
26-
verify.completionListContains({ name: "N", source: "/a" }, "namespace N", "", "module", undefined, /*hasAction*/ true, {
27-
includeExternalModuleExports: true,
28-
isRecommended: true,
29-
sourceDisplay: "./a",
29+
goTo.eachMarker(["b0", "b1"], () => {
30+
verify.completionListContains({ name: "Name", source: "/a" }, "namespace Name", "", "module", undefined, /*hasAction*/ true, {
31+
includeExternalModuleExports: true,
32+
isRecommended: true,
33+
sourceDisplay: "./a",
34+
});
3035
});
3136

32-
goTo.marker("c");
33-
verify.completionListContains("a", "import a", "", "alias", undefined, undefined, { isRecommended: true });
37+
goTo.eachMarker(["c0", "c1"], () => {
38+
verify.completionListContains("alpha", "import alpha", "", "alias", undefined, undefined, { isRecommended: true });
39+
});

0 commit comments

Comments
 (0)