From a71011d574cf261f043b220098b55a09ebfc2a8a Mon Sep 17 00:00:00 2001 From: Andy Hanson Date: Mon, 18 Dec 2017 14:12:07 -0800 Subject: [PATCH] Support completions contextual types in more places --- src/compiler/checker.ts | 20 +---- src/harness/fourslash.ts | 16 ++-- src/services/completions.ts | 86 +++++++++++++------ .../completionsRecommended_equals.ts | 12 +-- .../completionsRecommended_import.ts | 39 ++++++--- .../fourslash/completionsRecommended_local.ts | 45 +++++++--- .../completionsRecommended_namespace.ts | 40 +++++---- .../completionsRecommended_switch.ts | 12 +-- tests/cases/fourslash/fourslash.ts | 3 +- 9 files changed, 167 insertions(+), 106 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d31e9ce570331..54c2a78bd7462 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -13928,7 +13928,7 @@ namespace ts { // the contextual type of an initializer expression is the type annotation of the containing declaration, if present. function getContextualTypeForInitializerExpression(node: Expression): Type { const declaration = node.parent; - if (hasInitializer(declaration) && node === declaration.initializer || node.kind === SyntaxKind.EqualsToken) { + if (hasInitializer(declaration) && node === declaration.initializer) { const typeNode = getEffectiveTypeAnnotationNode(declaration); if (typeNode) { return getTypeFromTypeNode(typeNode); @@ -14060,12 +14060,6 @@ namespace ts { case SyntaxKind.AmpersandAmpersandToken: case SyntaxKind.CommaToken: return node === right ? getContextualType(binaryExpression) : undefined; - case SyntaxKind.EqualsEqualsEqualsToken: - case SyntaxKind.EqualsEqualsToken: - case SyntaxKind.ExclamationEqualsEqualsToken: - case SyntaxKind.ExclamationEqualsToken: - // For completions after `x === ` - return node === operatorToken ? getTypeOfExpression(binaryExpression.left) : undefined; default: return undefined; } @@ -14281,12 +14275,8 @@ namespace ts { return getContextualTypeForReturnExpression(node); case SyntaxKind.YieldExpression: return getContextualTypeForYieldOperand(parent); + case SyntaxKind.CallExpression: case SyntaxKind.NewExpression: - if (node.kind === SyntaxKind.NewKeyword) { // for completions after `new ` - return getContextualType(parent as NewExpression); - } - // falls through - case SyntaxKind.CallExpression: return getContextualTypeForArgument(parent, node); case SyntaxKind.TypeAssertionExpression: case SyntaxKind.AsExpression: @@ -14321,12 +14311,6 @@ namespace ts { case SyntaxKind.JsxOpeningElement: case SyntaxKind.JsxSelfClosingElement: return getAttributesTypeFromJsxOpeningLikeElement(parent); - case SyntaxKind.CaseClause: { - if (node.kind === SyntaxKind.CaseKeyword) { // for completions after `case ` - const switchStatement = (parent as CaseClause).parent.parent; - return getTypeOfExpression(switchStatement.expression); - } - } } return undefined; } diff --git a/src/harness/fourslash.ts b/src/harness/fourslash.ts index fefee7ca3bd94..99d643ec43857 100644 --- a/src/harness/fourslash.ts +++ b/src/harness/fourslash.ts @@ -441,12 +441,11 @@ namespace FourSlash { this.goToPosition(marker.position); } - public goToEachMarker(action: () => void) { - const markers = this.getMarkers(); + public goToEachMarker(markers: ReadonlyArray, action: (marker: FourSlash.Marker, index: number) => void) { assert(markers.length); - for (const marker of markers) { - this.goToMarker(marker); - action(); + for (let i = 0; i < markers.length; i++) { + this.goToMarker(markers[i]); + action(markers[i], i); } } @@ -3764,8 +3763,11 @@ namespace FourSlashInterface { this.state.goToMarker(name); } - public eachMarker(action: () => void) { - this.state.goToEachMarker(action); + public eachMarker(markers: ReadonlyArray, action: (marker: FourSlash.Marker, index: number) => void): void; + public eachMarker(action: (marker: FourSlash.Marker, index: number) => void): void; + public eachMarker(a: ReadonlyArray | ((marker: FourSlash.Marker, index: number) => void), b?: (marker: FourSlash.Marker, index: number) => void): void { + const markers = typeof a === "function" ? this.state.getMarkers() : a.map(m => this.state.getMarkerByName(m)); + this.state.goToEachMarker(markers, typeof a === "function" ? a : b); } public rangeStart(range: FourSlash.Range) { diff --git a/src/services/completions.ts b/src/services/completions.ts index a7020823d2e7d..48142e0b4c68b 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -238,7 +238,7 @@ namespace ts.Completions { function getStringLiteralCompletionEntries(sourceFile: SourceFile, position: number, typeChecker: TypeChecker, compilerOptions: CompilerOptions, host: LanguageServiceHost, log: Log): CompletionInfo | undefined { const node = findPrecedingToken(position, sourceFile); - if (!node || node.kind !== SyntaxKind.StringLiteral) { + if (!node || !isStringLiteral(node)) { return undefined; } @@ -280,18 +280,6 @@ namespace ts.Completions { const entries = PathCompletions.getStringLiteralCompletionsFromModuleNames(node, compilerOptions, host, typeChecker); return pathCompletionsInfo(entries); } - else if (isEqualityExpression(node.parent)) { - // Get completions from the type of the other operand - // i.e. switch (a) { - // case '/*completion position*/' - // } - return getStringLiteralCompletionEntriesFromType(typeChecker.getTypeAtLocation(node.parent.left === node ? node.parent.right : node.parent.left), typeChecker); - } - else if (isCaseOrDefaultClause(node.parent)) { - // Get completions from the type of the switch expression - // i.e. x === '/*completion position' - return getStringLiteralCompletionEntriesFromType(typeChecker.getTypeAtLocation((node.parent.parent.parent).expression), typeChecker); - } else { const argumentInfo = SignatureHelp.getImmediatelyContainingArgumentInfo(node, position, sourceFile); if (argumentInfo) { @@ -303,7 +291,7 @@ namespace ts.Completions { // Get completion for string literal from string literal type // i.e. var x: "hi" | "hello" = "/*completion position*/" - return getStringLiteralCompletionEntriesFromType(typeChecker.getContextualType(node), typeChecker); + return getStringLiteralCompletionEntriesFromType(getContextualTypeFromParent(node, typeChecker), typeChecker); } } @@ -600,8 +588,8 @@ namespace ts.Completions { } type Request = { kind: "JsDocTagName" } | { kind: "JsDocTag" } | { kind: "JsDocParameterName", tag: JSDocParameterTag }; - function getRecommendedCompletion(currentToken: Node, checker: TypeChecker/*, symbolToOriginInfoMap: SymbolOriginInfoMap*/): Symbol | undefined { - const ty = checker.getContextualType(currentToken as Expression); + function getRecommendedCompletion(currentToken: Node, checker: TypeChecker): Symbol | undefined { + const ty = getContextualType(currentToken, 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)) @@ -609,6 +597,51 @@ namespace ts.Completions { : undefined; } + function getContextualType(currentToken: Node, checker: ts.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); + default: + return isEqualityOperatorKind(currentToken.kind) && ts.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); + } + } + + function getContextualTypeFromParent(node: ts.Expression, checker: ts.TypeChecker): Type | undefined { + const { parent } = node; + switch (parent.kind) { + case ts.SyntaxKind.NewExpression: + return checker.getContextualType(parent as ts.NewExpression); + case ts.SyntaxKind.BinaryExpression: { + const { left, operatorToken, right } = parent as ts.BinaryExpression; + return isEqualityOperatorKind(operatorToken.kind) + ? checker.getTypeAtLocation(node === right ? left : right) + : checker.getContextualType(node); + } + case ts.SyntaxKind.CaseClause: + return (parent as ts.CaseClause).expression === node ? getSwitchedType(parent as ts.CaseClause, checker) : undefined; + default: + return checker.getContextualType(node); + } + } + + function getSwitchedType(caseClause: ts.CaseClause, checker: ts.TypeChecker): ts.Type { + return checker.getTypeAtLocation(caseClause.parent.parent.expression); + } + function getFirstSymbolInChain(symbol: Symbol, enclosingDeclaration: Node, checker: TypeChecker): Symbol | undefined { const chain = checker.getAccessibleSymbolChain(symbol, enclosingDeclaration, /*meaning*/ SymbolFlags.All, /*useOnlyExternalAliasing*/ false); if (chain) return first(chain); @@ -848,7 +881,7 @@ namespace ts.Completions { log("getCompletionData: Semantic work: " + (timestamp() - semanticStart)); - const recommendedCompletion = getRecommendedCompletion(previousToken, typeChecker); + const recommendedCompletion = previousToken && getRecommendedCompletion(previousToken, typeChecker); return { symbols, isGlobalCompletion, isMemberCompletion, allowStringLiteral, isNewIdentifierLocation, location, isRightOfDot: (isRightOfDot || isRightOfOpenTag), request, keywordFilters, symbolToOriginInfoMap, recommendedCompletion }; type JSDocTagWithTypeExpression = JSDocParameterTag | JSDocPropertyTag | JSDocReturnTag | JSDocTypeTag | JSDocTypedefTag; @@ -2078,15 +2111,16 @@ namespace ts.Completions { return isConstructorParameterCompletionKeyword(stringToToken(text)); } - function isEqualityExpression(node: Node): node is BinaryExpression { - return isBinaryExpression(node) && isEqualityOperatorKind(node.operatorToken.kind); - } - - function isEqualityOperatorKind(kind: SyntaxKind) { - return kind === SyntaxKind.EqualsEqualsToken || - kind === SyntaxKind.ExclamationEqualsToken || - kind === SyntaxKind.EqualsEqualsEqualsToken || - kind === SyntaxKind.ExclamationEqualsEqualsToken; + function isEqualityOperatorKind(kind: ts.SyntaxKind): kind is EqualityOperator { + switch (kind) { + case ts.SyntaxKind.EqualsEqualsEqualsToken: + case ts.SyntaxKind.EqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsEqualsToken: + case ts.SyntaxKind.ExclamationEqualsToken: + return true; + default: + return false; + } } /** Get the corresponding JSDocTag node if the position is in a jsDoc comment */ diff --git a/tests/cases/fourslash/completionsRecommended_equals.ts b/tests/cases/fourslash/completionsRecommended_equals.ts index 21b17677ccc7e..37e4c9313597d 100644 --- a/tests/cases/fourslash/completionsRecommended_equals.ts +++ b/tests/cases/fourslash/completionsRecommended_equals.ts @@ -1,8 +1,10 @@ /// -////enum E {} -////declare const e: E; -////e === /**/ +////enum Enu {} +////declare const e: Enu; +////e === /*a*/; +////e === E/*b*/ -goTo.marker(); -verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true }); +goTo.eachMarker(["a", "b"], () => { + verify.completionListContains("Enu", "enum Enu", "", "enum", undefined, undefined, { isRecommended: true }); +}); diff --git a/tests/cases/fourslash/completionsRecommended_import.ts b/tests/cases/fourslash/completionsRecommended_import.ts index dc8066d246e6a..158221f03a8c8 100644 --- a/tests/cases/fourslash/completionsRecommended_import.ts +++ b/tests/cases/fourslash/completionsRecommended_import.ts @@ -3,25 +3,36 @@ // @noLib: true // @Filename: /a.ts -////export class C {} -////export function f(c: C) {} +////export class Cls {} +////export function f(c: Cls) {} // @Filename: /b.ts ////import { f } from "./a"; -// Here we will recommend a new import of 'C' -////f(new /*b*/); +// Here we will recommend a new import of 'Cls' +////f(new C/*b0*/); +////f(new /*b1*/); // @Filename: /c.ts -////import * as a from "./a"; -// Here we will recommend 'a' because it contains 'C'. -////a.f(new /*c*/); +////import * as alpha from "./a"; +// Here we will recommend 'alpha' because it contains 'Cls'. +////alpha.f(new al/*c0*/); +////alpha.f(new /*c1*/); -goTo.marker("b"); -verify.completionListContains({ name: "C", source: "/a" }, "class C", "", "class", undefined, /*hasAction*/ true, { - includeExternalModuleExports: true, - isRecommended: true, - sourceDisplay: "./a", +goTo.eachMarker(["b0", "b1"], (_, idx) => { + verify.completionListContains( + { name: "Cls", source: "/a" }, + idx === 0 ? "constructor Cls(): Cls" : "class Cls", + "", + "class", + undefined, + /*hasAction*/ true, { + includeExternalModuleExports: true, + isRecommended: true, + sourceDisplay: "./a", + }); +}); + +goTo.eachMarker(["c0", "c1"], (_, idx) => { + verify.completionListContains("alpha", "import alpha", "", "alias", undefined, undefined, { isRecommended: true }) }); -goTo.marker("c"); -verify.completionListContains("a", "import a", "", "alias", undefined, undefined, { isRecommended: true }); diff --git a/tests/cases/fourslash/completionsRecommended_local.ts b/tests/cases/fourslash/completionsRecommended_local.ts index fad660761fa92..768db8c0c9120 100644 --- a/tests/cases/fourslash/completionsRecommended_local.ts +++ b/tests/cases/fourslash/completionsRecommended_local.ts @@ -1,18 +1,37 @@ /// -////enum E {} -////class C {} -////abstract class A {} -////const e: E = /*e*/ -////const c: C = new /*c*/ -////const a: A = new /*a*/ +////enum Enu {} +////class Cls {} +////abstract class Abs {} +////const e: Enu = E/*e0*/; +////const e: Enu = /*e1*/; +////const c: Cls = new C/*c0*/; +////const c: Cls = new /*c1*/; +////const a: Abs = new A/*a0*/; +////const a: Abs = new /*a1*/; -goTo.marker("e"); -verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true }); +// Also works on mutations +////let enu: Enu; +////enu = E/*let0*/; +////enu = E/*let1*/; -goTo.marker("c"); -verify.completionListContains("C", "class C", "", "class", undefined, undefined, { isRecommended: true }); +goTo.eachMarker(["e0"], () => {//, "e1", "let0", "let1" + verify.completionListContains("Enu", "enum Enu", "", "enum", undefined, undefined, { isRecommended: true }); +}); -goTo.marker("a"); -// Not recommended, because it's an abstract class -verify.completionListContains("A", "class A", "", "class"); +goTo.eachMarker(["c0", "c1"], (_, idx) => { + verify.completionListContains( + "Cls", + idx === 0 ? "constructor Cls(): Cls" : "class Cls", + "", + "class", + undefined, + undefined, { + isRecommended: true, + }); +}); + +goTo.eachMarker(["a0", "a1"], (_, idx) => { + // Not recommended, because it's an abstract class + verify.completionListContains("Abs", idx == 0 ? "constructor Abs(): Abs" : "class Abs", "", "class"); +}); diff --git a/tests/cases/fourslash/completionsRecommended_namespace.ts b/tests/cases/fourslash/completionsRecommended_namespace.ts index 3ea8d597210d7..d3fe9c2a54b2e 100644 --- a/tests/cases/fourslash/completionsRecommended_namespace.ts +++ b/tests/cases/fourslash/completionsRecommended_namespace.ts @@ -3,31 +3,37 @@ // @noLib: true // @Filename: /a.ts -////export namespace N { +////export namespace Name { //// export class C {} ////} -////export function f(c: N.C) {} -////f(new /*a*/); +////export function f(c: Name.C) {} +////f(new N/*a0*/); +////f(new /*a1*/); // @Filename: /b.ts ////import { f } from "./a"; -// Here we will recommend a new import of 'N' -////f(new /*b*/); +// Here we will recommend a new import of 'Name' +////f(new N/*b0*/); +////f(new /*b1*/); // @Filename: /c.ts -////import * as a from "./a"; -// Here we will recommend 'a' because it contains 'N' which contains 'C'. -////a.f(new /*c*/); +////import * as alpha from "./a"; +// Here we will recommend 'a' because it contains 'Name' which contains 'C'. +////alpha.f(new a/*c0*/); +////alpha.f(new /*c1*/); -goTo.marker("a"); -verify.completionListContains("N", "namespace N", "", "module", undefined, undefined, { isRecommended: true }); +goTo.eachMarker(["a0", "a1"], () => { + verify.completionListContains("Name", "namespace Name", "", "module", undefined, undefined, { isRecommended: true }); +}); -goTo.marker("b"); -verify.completionListContains({ name: "N", source: "/a" }, "namespace N", "", "module", undefined, /*hasAction*/ true, { - includeExternalModuleExports: true, - isRecommended: true, - sourceDisplay: "./a", +goTo.eachMarker(["b0", "b1"], () => { + verify.completionListContains({ name: "Name", source: "/a" }, "namespace Name", "", "module", undefined, /*hasAction*/ true, { + includeExternalModuleExports: true, + isRecommended: true, + sourceDisplay: "./a", + }); }); -goTo.marker("c"); -verify.completionListContains("a", "import a", "", "alias", undefined, undefined, { isRecommended: true }); +goTo.eachMarker(["c0", "c1"], () => { + verify.completionListContains("alpha", "import alpha", "", "alias", undefined, undefined, { isRecommended: true }); +}); diff --git a/tests/cases/fourslash/completionsRecommended_switch.ts b/tests/cases/fourslash/completionsRecommended_switch.ts index b73d0be632ba6..a8941ac53eae5 100644 --- a/tests/cases/fourslash/completionsRecommended_switch.ts +++ b/tests/cases/fourslash/completionsRecommended_switch.ts @@ -1,10 +1,12 @@ /// -////enum E {} -////declare const e: E; +////enum Enu {} +////declare const e: Enu; ////switch (e) { -//// case /**/ +//// case E/*0*/: +//// case /*1*/: ////} -goTo.marker(); -verify.completionListContains("E", "enum E", "", "enum", undefined, undefined, { isRecommended: true }); +goTo.eachMarker((_, idx) => { + verify.completionListContains("Enu", "enum Enu", "", "enum", undefined, undefined, { isRecommended: true }); +}); diff --git a/tests/cases/fourslash/fourslash.ts b/tests/cases/fourslash/fourslash.ts index acebb9dfc0545..8b581e62e6c9c 100644 --- a/tests/cases/fourslash/fourslash.ts +++ b/tests/cases/fourslash/fourslash.ts @@ -122,7 +122,8 @@ declare namespace FourSlashInterface { } class goTo { marker(name?: string | Marker): void; - eachMarker(action: () => void): void; + eachMarker(markers: ReadonlyArray, action: (marker: Marker, index: number) => void): void; + eachMarker(action: (marker: Marker, index: number) => void): void; rangeStart(range: Range): void; eachRange(action: () => void): void; bof(): void;