diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1e6a16aa75fd9..25224b025039d 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) => { @@ -8550,7 +8550,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; @@ -16240,11 +16240,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 @@ -16683,13 +16685,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 { @@ -16711,8 +16719,8 @@ namespace ts { } function isValidPropertyAccessWithType( - node: PropertyAccessExpression | QualifiedName, - left: LeftHandSideExpression | QualifiedName, + node: PropertyAccessExpression | QualifiedName | ImportTypeNode, + left: LeftHandSideExpression | QualifiedName | ImportTypeNode, propertyName: __String, type: Type): boolean { @@ -25339,6 +25347,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/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 9fba1a6610b64..eff6c7a20b763 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; @@ -5728,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/harness/tsconfig.json b/src/harness/tsconfig.json index c9521979776d5..fe4b066c5e7db 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 8c34e7e7de622..42e89e5f509d9 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -881,6 +881,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. @@ -1012,10 +1015,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); @@ -1023,7 +1027,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) => typeChecker.isValidPropertyAccess((node.parent), symbol.name); + 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 @@ -1063,7 +1067,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)) { addPropertySymbol(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 new file mode 100644 index 0000000000000..b2bbc0e81b189 --- /dev/null +++ b/tests/cases/fourslash/importTypeMemberCompletions.ts @@ -0,0 +1,51 @@ +/// + +// @Filename: /ns.ts +////export namespace Foo { +//// export namespace Bar { +//// export class Baz {} +//// export interface Bat {} +//// export const a: number; +//// const b: string; +//// } +////} + +// @Filename: /top.ts +////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 +////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*/ +// @Filename: /usage9.ts +////type H = typeof import("./equals")./*9*/ + +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"]); +verify.completionsAt("9", ["prototype", "bar"]);