From 6d6d4431a518b97b669f6152c56521850f24c818 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 2 Apr 2018 13:11:54 -0700 Subject: [PATCH 1/5] Enable member completions for import types --- src/compiler/checker.ts | 2 + src/compiler/utilities.ts | 3 ++ src/harness/tsconfig.json | 2 + src/services/completions.ts | 18 +++++++-- .../fourslash/importTypeMemberCompletions.ts | 40 +++++++++++++++++++ 5 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 tests/cases/fourslash/importTypeMemberCompletions.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3c48319273abc..2a0849bcbdfcc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -25336,6 +25336,8 @@ namespace ts { case SyntaxKind.FunctionKeyword: case SyntaxKind.EqualsGreaterThanToken: return getSymbolOfNode(node.parent); + case SyntaxKind.ImportTypeNode: + return isLiteralImportTypeNode(node) ? getSymbolAtLocation(node.argument.literal) : undefined; default: return undefined; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 9fba1a6610b64..c71cdcd005a2e 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -811,6 +811,9 @@ namespace ts { if (parent.kind === SyntaxKind.TypeQuery) { return false; } + if (parent.kind === SyntaxKind.ImportTypeNode) { + return !(parent as ImportTypeNode).isTypeOf; + } // Do not recursively call isPartOfTypeNode on the parent. In the example: // // let a: A.B.C; diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index 54766ed02eca4..8aad22e725ffd 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -54,6 +54,8 @@ "../services/navigationBar.ts", "../services/outliningElementsCollector.ts", "../services/patternMatcher.ts", + "../services/pathCompletions.ts", + "../services/completions.ts", "../services/services.ts", "../services/shims.ts", "../services/signatureHelp.ts", diff --git a/src/services/completions.ts b/src/services/completions.ts index 1fcd947e5bb79..62f12d27376d7 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -865,6 +865,9 @@ namespace ts.Completions { case SyntaxKind.QualifiedName: node = (parent as QualifiedName).left; break; + case SyntaxKind.ImportTypeNode: + node = parent; + break; default: // There is nothing that precedes the dot, so this likely just a stray character // or leading into a '...' token. Just bail out instead. @@ -996,10 +999,11 @@ namespace ts.Completions { completionKind = CompletionKind.PropertyAccess; // Since this is qualified name check its a type node location - const isTypeLocation = insideJsDocTagTypeExpression || isPartOfTypeNode(node.parent); + const isImportType = isLiteralImportTypeNode(node); + const isTypeLocation = insideJsDocTagTypeExpression || (isImportType && !(node as ImportTypeNode).isTypeOf) || isPartOfTypeNode(node.parent); const isRhsOfImportDeclaration = isInRightSideOfInternalImportEqualsDeclaration(node); const allowTypeOrValue = isRhsOfImportDeclaration || (!isTypeLocation && isPossiblyTypeArgumentPosition(contextToken, sourceFile)); - if (isEntityName(node)) { + if (isEntityName(node) || isImportType) { let symbol = typeChecker.getSymbolAtLocation(node); if (symbol) { symbol = skipAlias(symbol, typeChecker); @@ -1007,7 +1011,15 @@ namespace ts.Completions { if (symbol.flags & (SymbolFlags.Module | SymbolFlags.Enum)) { // Extract module or enum members const exportedSymbols = Debug.assertEachDefined(typeChecker.getExportsOfModule(symbol), "getExportsOfModule() should all be defined"); - const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess((node.parent), symbol.name); + const isValidValueAccess = (symbol: Symbol) => { + if (!isImportType) { + return typeChecker.isValidPropertyAccess((node.parent), symbol.name); + } + else { + if (!(node as ImportTypeNode).isTypeOf) return false; + return !!(symbol.flags & SymbolFlags.Value); + } + } const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); const isValidAccess = allowTypeOrValue ? // Any kind is allowed when dotting off namespace in internal import equals declaration diff --git a/tests/cases/fourslash/importTypeMemberCompletions.ts b/tests/cases/fourslash/importTypeMemberCompletions.ts new file mode 100644 index 0000000000000..7c9026674a975 --- /dev/null +++ b/tests/cases/fourslash/importTypeMemberCompletions.ts @@ -0,0 +1,40 @@ +/// + +// @Filename: /ns.ts +////export namespace Foo { +//// export namespace Bar { +//// export class Baz {} +//// export interface Bat {} +//// export const a: number; +//// } +////} + +// @Filename: /top.ts +////export interface Bat {} +////export const a: number; + +// @Filename: /usage1.ts +////type A = typeof import("./ns")./*1*/ +// @Filename: /usage2.ts +////type B = typeof import("./ns").Foo./*2*/ +// @Filename: /usage3.ts +////type C = typeof import("./ns").Foo.Bar./*3*/ +// @Filename: /usage4.ts +////type D = import("./ns")./*4*/ +// @Filename: /usage5.ts +////type E = import("./ns").Foo./*5*/ +// @Filename: /usage6.ts +////type F = import("./ns").Foo.Bar./*6*/ +// @Filename: /usage7.ts +////type G = typeof import("./top")./*7*/ +// @Filename: /usage8.ts +////type H = import("./top")./*8*/ + +verify.completionsAt("1", ["Foo"]); +verify.completionsAt("2", ["Bar"]); +verify.completionsAt("3", ["Baz", "a"]); +verify.completionsAt("4", ["Foo"]); +verify.completionsAt("5", ["Bar"]); +verify.completionsAt("6", ["Baz", "Bat"]); +verify.completionsAt("7", ["a"]); +verify.completionsAt("8", ["Bat"]); From 72066e94c6e94464c781ab7c6256daf4b798fa66 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 2 Apr 2018 13:17:06 -0700 Subject: [PATCH 2/5] Add missing semicolon --- src/services/completions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 62f12d27376d7..29ba18141f1d7 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1019,7 +1019,7 @@ namespace ts.Completions { if (!(node as ImportTypeNode).isTypeOf) return false; return !!(symbol.flags & SymbolFlags.Value); } - } + }; const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); const isValidAccess = allowTypeOrValue ? // Any kind is allowed when dotting off namespace in internal import equals declaration From 27400a7a1bc4c2e344f18c9f83bf3702ece03e73 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 2 Apr 2018 15:55:12 -0700 Subject: [PATCH 3/5] Compact statements --- src/services/completions.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 29ba18141f1d7..f6d3a78ee3266 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1016,8 +1016,7 @@ namespace ts.Completions { return typeChecker.isValidPropertyAccess((node.parent), symbol.name); } else { - if (!(node as ImportTypeNode).isTypeOf) return false; - return !!(symbol.flags & SymbolFlags.Value); + return !(node as ImportTypeNode).isTypeOf && !!(symbol.flags & SymbolFlags.Value); } }; const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); From 83dd753a918fe10817578804704227df5bb34c79 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 2 Apr 2018 16:08:08 -0700 Subject: [PATCH 4/5] Ye doth compacteth too much --- src/services/completions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index f6d3a78ee3266..d56eb0a6a3edc 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1016,7 +1016,7 @@ namespace ts.Completions { return typeChecker.isValidPropertyAccess((node.parent), symbol.name); } else { - return !(node as ImportTypeNode).isTypeOf && !!(symbol.flags & SymbolFlags.Value); + return !!(node as ImportTypeNode).isTypeOf && !!(symbol.flags & SymbolFlags.Value); } }; const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); From 895be38dd54e105dc7544f4e31ad73cafa8d3d0d Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 2 Apr 2018 18:11:26 -0700 Subject: [PATCH 5/5] Handle class accesibility --- src/compiler/checker.ts | 30 ++++++++++++------- src/compiler/types.ts | 4 +-- src/compiler/utilities.ts | 8 +++++ src/services/completions.ts | 11 ++----- .../reference/api/tsserverlibrary.d.ts | 2 +- tests/baselines/reference/api/typescript.d.ts | 2 +- .../fourslash/importTypeMemberCompletions.ts | 11 +++++++ 7 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2a0849bcbdfcc..ed46e94bad0ca 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -213,7 +213,7 @@ namespace ts { return node ? getConstantValue(node) : undefined; }, isValidPropertyAccess: (node, propertyName) => { - node = getParseTreeNode(node, isPropertyAccessOrQualifiedName); + node = getParseTreeNode(node, isPropertyAccessOrQualifiedNameOrImportTypeNode); return !!node && isValidPropertyAccess(node, escapeLeadingUnderscores(propertyName)); }, isValidPropertyAccessForCompletions: (node, type, property) => { @@ -8547,7 +8547,7 @@ namespace ts { return links.resolvedType = unknownType; } const moduleSymbol = resolveExternalModuleSymbol(innerModuleSymbol, /*dontResolveAlias*/ false); - if (node.qualifier) { + if (!nodeIsMissing(node.qualifier)) { const nameStack: Identifier[] = getIdentifierChain(node.qualifier); let currentNamespace = moduleSymbol; let current: Identifier | undefined; @@ -16237,11 +16237,13 @@ namespace ts { * @param type The type of left. * @param prop The symbol for the right hand side of the property access. */ - function checkPropertyAccessibility(node: PropertyAccessExpression | QualifiedName | VariableLikeDeclaration, left: Expression | QualifiedName, type: Type, prop: Symbol): boolean { + function checkPropertyAccessibility(node: PropertyAccessExpression | QualifiedName | VariableLikeDeclaration | ImportTypeNode, left: Expression | QualifiedName | ImportTypeNode, type: Type, prop: Symbol): boolean { const flags = getDeclarationModifierFlagsFromSymbol(prop); const errorNode = node.kind === SyntaxKind.PropertyAccessExpression || node.kind === SyntaxKind.VariableDeclaration ? node.name : - (node).right; + node.kind === SyntaxKind.ImportTypeNode ? + node : + (node).right; if (getCheckFlags(prop) & CheckFlags.ContainsPrivate) { // Synthetic property with private constituent property @@ -16680,13 +16682,19 @@ namespace ts { (getCheckFlags(prop) & CheckFlags.Instantiated ? getSymbolLinks(prop).target : prop).isReferenced = SymbolFlags.All; } - function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: __String): boolean { - const left = node.kind === SyntaxKind.PropertyAccessExpression ? node.expression : node.left; - return isValidPropertyAccessWithType(node, left, propertyName, getWidenedType(checkExpression(left))); + function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: __String): boolean { + switch (node.kind) { + case SyntaxKind.PropertyAccessExpression: + return isValidPropertyAccessWithType(node, node.expression, propertyName, getWidenedType(checkExpression(node.expression))); + case SyntaxKind.QualifiedName: + return isValidPropertyAccessWithType(node, node.left, propertyName, getWidenedType(checkExpression(node.left))); + case SyntaxKind.ImportTypeNode: + return isValidPropertyAccessWithType(node, node, propertyName, getTypeFromTypeNode(node)); + } } - function isValidPropertyAccessForCompletions(node: PropertyAccessExpression, type: Type, property: Symbol): boolean { - return isValidPropertyAccessWithType(node, node.expression, property.escapedName, type) + function isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode, type: Type, property: Symbol): boolean { + return isValidPropertyAccessWithType(node, node.kind === SyntaxKind.ImportTypeNode ? node : node.expression, property.escapedName, type) && (!(property.flags & SymbolFlags.Method) || isValidMethodAccess(property, type)); } function isValidMethodAccess(method: Symbol, actualThisType: Type): boolean { @@ -16708,8 +16716,8 @@ namespace ts { } function isValidPropertyAccessWithType( - node: PropertyAccessExpression | QualifiedName, - left: LeftHandSideExpression | QualifiedName, + node: PropertyAccessExpression | QualifiedName | ImportTypeNode, + left: LeftHandSideExpression | QualifiedName | ImportTypeNode, propertyName: __String, type: Type): boolean { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4edfc29a2a14c..89547f43cbe55 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -2890,9 +2890,9 @@ namespace ts { /* @internal */ getMergedSymbol(symbol: Symbol): Symbol; getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined; - isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean; + isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: string): boolean; /** Exclude accesses to private properties or methods with a `this` parameter that `type` doesn't satisfy. */ - /* @internal */ isValidPropertyAccessForCompletions(node: PropertyAccessExpression, type: Type, property: Symbol): boolean; + /* @internal */ isValidPropertyAccessForCompletions(node: PropertyAccessExpression | ImportTypeNode, type: Type, property: Symbol): boolean; /** Follow all aliases to get the original symbol. */ getAliasedSymbol(symbol: Symbol): Symbol; /** Follow a *single* alias to get the immediately aliased symbol. */ diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index c71cdcd005a2e..eff6c7a20b763 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -5731,6 +5731,14 @@ namespace ts { return false; } + /* @internal */ + export function isPropertyAccessOrQualifiedNameOrImportTypeNode(node: Node): node is PropertyAccessExpression | QualifiedName | ImportTypeNode { + const kind = node.kind; + return kind === SyntaxKind.PropertyAccessExpression + || kind === SyntaxKind.QualifiedName + || kind === SyntaxKind.ImportTypeNode; + } + // Expression export function isPropertyAccessOrQualifiedName(node: Node): node is PropertyAccessExpression | QualifiedName { diff --git a/src/services/completions.ts b/src/services/completions.ts index d56eb0a6a3edc..fbb87c3128ad1 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1011,14 +1011,7 @@ namespace ts.Completions { if (symbol.flags & (SymbolFlags.Module | SymbolFlags.Enum)) { // Extract module or enum members const exportedSymbols = Debug.assertEachDefined(typeChecker.getExportsOfModule(symbol), "getExportsOfModule() should all be defined"); - const isValidValueAccess = (symbol: Symbol) => { - if (!isImportType) { - return typeChecker.isValidPropertyAccess((node.parent), symbol.name); - } - else { - return !!(node as ImportTypeNode).isTypeOf && !!(symbol.flags & SymbolFlags.Value); - } - }; + const isValidValueAccess = (symbol: Symbol) => typeChecker.isValidPropertyAccess(isImportType ? node : (node.parent), symbol.name); const isValidTypeAccess = (symbol: Symbol) => symbolCanBeReferencedAtTypeLocation(symbol); const isValidAccess = allowTypeOrValue ? // Any kind is allowed when dotting off namespace in internal import equals declaration @@ -1058,7 +1051,7 @@ namespace ts.Completions { } else { for (const symbol of type.getApparentProperties()) { - if (typeChecker.isValidPropertyAccessForCompletions((node.parent), type, symbol)) { + if (typeChecker.isValidPropertyAccessForCompletions(node.kind === SyntaxKind.ImportTypeNode ? node : (node.parent), type, symbol)) { symbols.push(symbol); } } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index aca4e95618135..2046d3fa1d1af 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -1821,7 +1821,7 @@ declare namespace ts { isArgumentsSymbol(symbol: Symbol): boolean; isUnknownSymbol(symbol: Symbol): boolean; getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined; - isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean; + isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: string): boolean; /** Follow all aliases to get the original symbol. */ getAliasedSymbol(symbol: Symbol): Symbol; getExportsOfModule(moduleSymbol: Symbol): Symbol[]; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index aa77372db4576..576853acf2431 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -1821,7 +1821,7 @@ declare namespace ts { isArgumentsSymbol(symbol: Symbol): boolean; isUnknownSymbol(symbol: Symbol): boolean; getConstantValue(node: EnumMember | PropertyAccessExpression | ElementAccessExpression): string | number | undefined; - isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean; + isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName | ImportTypeNode, propertyName: string): boolean; /** Follow all aliases to get the original symbol. */ getAliasedSymbol(symbol: Symbol): Symbol; getExportsOfModule(moduleSymbol: Symbol): Symbol[]; diff --git a/tests/cases/fourslash/importTypeMemberCompletions.ts b/tests/cases/fourslash/importTypeMemberCompletions.ts index 7c9026674a975..b2bbc0e81b189 100644 --- a/tests/cases/fourslash/importTypeMemberCompletions.ts +++ b/tests/cases/fourslash/importTypeMemberCompletions.ts @@ -6,6 +6,7 @@ //// export class Baz {} //// export interface Bat {} //// export const a: number; +//// const b: string; //// } ////} @@ -13,6 +14,13 @@ ////export interface Bat {} ////export const a: number; +// @Filename: /equals.ts +////class Foo { +//// public static bar: string; +//// private static baz: number; +////} +////export = Foo; + // @Filename: /usage1.ts ////type A = typeof import("./ns")./*1*/ // @Filename: /usage2.ts @@ -29,6 +37,8 @@ ////type G = typeof import("./top")./*7*/ // @Filename: /usage8.ts ////type H = import("./top")./*8*/ +// @Filename: /usage9.ts +////type H = typeof import("./equals")./*9*/ verify.completionsAt("1", ["Foo"]); verify.completionsAt("2", ["Bar"]); @@ -38,3 +48,4 @@ verify.completionsAt("5", ["Bar"]); verify.completionsAt("6", ["Baz", "Bat"]); verify.completionsAt("7", ["a"]); verify.completionsAt("8", ["Bat"]); +verify.completionsAt("9", ["prototype", "bar"]);