Skip to content

Commit a33f229

Browse files
author
Andy
authored
Support completions contextual types in more places (#20768)
* Support completions contextual types in more places * Adjust formatting
1 parent dde7f03 commit a33f229

10 files changed

+166
-108
lines changed

Diff for: src/compiler/checker.ts

+2-18
Original file line numberDiff line numberDiff line change
@@ -13943,7 +13943,7 @@ namespace ts {
1394313943
// the contextual type of an initializer expression is the type annotation of the containing declaration, if present.
1394413944
function getContextualTypeForInitializerExpression(node: Expression): Type {
1394513945
const declaration = <VariableLikeDeclaration>node.parent;
13946-
if (hasInitializer(declaration) && node === declaration.initializer || node.kind === SyntaxKind.EqualsToken) {
13946+
if (hasInitializer(declaration) && node === declaration.initializer) {
1394713947
const typeNode = getEffectiveTypeAnnotationNode(declaration);
1394813948
if (typeNode) {
1394913949
return getTypeFromTypeNode(typeNode);
@@ -14075,12 +14075,6 @@ namespace ts {
1407514075
case SyntaxKind.AmpersandAmpersandToken:
1407614076
case SyntaxKind.CommaToken:
1407714077
return node === right ? getContextualType(binaryExpression) : undefined;
14078-
case SyntaxKind.EqualsEqualsEqualsToken:
14079-
case SyntaxKind.EqualsEqualsToken:
14080-
case SyntaxKind.ExclamationEqualsEqualsToken:
14081-
case SyntaxKind.ExclamationEqualsToken:
14082-
// For completions after `x === `
14083-
return node === operatorToken ? getTypeOfExpression(binaryExpression.left) : undefined;
1408414078
default:
1408514079
return undefined;
1408614080
}
@@ -14296,12 +14290,8 @@ namespace ts {
1429614290
return getContextualTypeForReturnExpression(node);
1429714291
case SyntaxKind.YieldExpression:
1429814292
return getContextualTypeForYieldOperand(<YieldExpression>parent);
14293+
case SyntaxKind.CallExpression:
1429914294
case SyntaxKind.NewExpression:
14300-
if (node.kind === SyntaxKind.NewKeyword) { // for completions after `new `
14301-
return getContextualType(parent as NewExpression);
14302-
}
14303-
// falls through
14304-
case SyntaxKind.CallExpression:
1430514295
return getContextualTypeForArgument(<CallExpression | NewExpression>parent, node);
1430614296
case SyntaxKind.TypeAssertionExpression:
1430714297
case SyntaxKind.AsExpression:
@@ -14336,12 +14326,6 @@ namespace ts {
1433614326
case SyntaxKind.JsxOpeningElement:
1433714327
case SyntaxKind.JsxSelfClosingElement:
1433814328
return getAttributesTypeFromJsxOpeningLikeElement(<JsxOpeningLikeElement>parent);
14339-
case SyntaxKind.CaseClause: {
14340-
if (node.kind === SyntaxKind.CaseKeyword) { // for completions after `case `
14341-
const switchStatement = (parent as CaseClause).parent.parent;
14342-
return getTypeOfExpression(switchStatement.expression);
14343-
}
14344-
}
1434514329
}
1434614330
return undefined;
1434714331
}

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

+58-27
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 && node.kind !== SyntaxKind.NoSubstitutionTemplateLiteral)) {
241+
if (!node || !isStringLiteral(node) && !isNoSubstitutionTemplateLiteral(node)) {
242242
return undefined;
243243
}
244244

@@ -277,21 +277,9 @@ namespace ts.Completions {
277277
// import x = require("/*completion position*/");
278278
// var y = require("/*completion position*/");
279279
// export * from "/*completion position*/";
280-
const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(<StringLiteral>node, compilerOptions, host, typeChecker);
280+
const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(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(<LiteralExpression>node), typeChecker);
294+
return getStringLiteralCompletionEntriesFromType(getContextualTypeFromParent(node, typeChecker), typeChecker);
307295
}
308296
}
309297

@@ -602,15 +590,57 @@ namespace ts.Completions {
602590
}
603591
type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag };
604592

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

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

852882
log("getCompletionData: Semantic work: " + (timestamp() - semanticStart));
853883

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

857887
type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag;
@@ -2081,15 +2111,16 @@ namespace ts.Completions {
20812111
return isConstructorParameterCompletionKeyword(stringToToken(text));
20822112
}
20832113

2084-
function isEqualityExpression(node: Node): node is BinaryExpression {
2085-
return isBinaryExpression(node) && isEqualityOperatorKind(node.operatorToken.kind);
2086-
}
2087-
2088-
function isEqualityOperatorKind(kind: SyntaxKind) {
2089-
return kind === SyntaxKind.EqualsEqualsToken ||
2090-
kind === SyntaxKind.ExclamationEqualsToken ||
2091-
kind === SyntaxKind.EqualsEqualsEqualsToken ||
2092-
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+
}
20932124
}
20942125

20952126
/** Get the corresponding JSDocTag node if the position is in a jsDoc comment */

Diff for: src/services/pathCompletions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* @internal */
22
namespace ts.Completions.PathCompletions {
3-
export function getStringLiteralCompletionsFromModuleNames(node: StringLiteral, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
3+
export function getStringLiteralCompletionsFromModuleNames(node: LiteralExpression, compilerOptions: CompilerOptions, host: LanguageServiceHost, typeChecker: TypeChecker): CompletionEntry[] {
44
const literalValue = normalizeSlashes(node.text);
55

66
const scriptPath = node.getSourceFile().path;
+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)