From fef7e8cf2af9cd84a869f7ead4e710a7fc545ebe Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 8 Oct 2021 11:15:47 -0700 Subject: [PATCH] A-ha moment --- src/services/completions.ts | 53 ++++++++++++------- src/services/exportInfoMap.ts | 40 +++++--------- src/services/types.ts | 2 + .../completionsImport_reexportTransient.ts | 33 ++++++++++++ 4 files changed, 80 insertions(+), 48 deletions(-) create mode 100644 tests/cases/fourslash/completionsImport_reexportTransient.ts diff --git a/src/services/completions.ts b/src/services/completions.ts index ce8c310b771a4..9bd3141641835 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -88,6 +88,7 @@ namespace ts.Completions { moduleSymbol: Symbol; isDefaultExport: boolean; exportName: string; + exportMapKey: string; } interface SymbolOriginInfoResolvedExport extends SymbolOriginInfoExport { @@ -308,9 +309,6 @@ namespace ts.Completions { const lowerCaseTokenText = location.text.toLowerCase(); const exportMap = getExportInfoMap(file, host, program, cancellationToken); - const checker = program.getTypeChecker(); - const autoImportProvider = host.getPackageJsonAutoImportProvider?.(); - const autoImportProviderChecker = autoImportProvider?.getTypeChecker(); const newEntries = resolvingModuleSpecifiers( "continuePreviousIncompleteResponse", host, @@ -329,13 +327,8 @@ namespace ts.Completions { return undefined; } - const { symbol, origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host)); - const info = exportMap.get( - file.path, - entry.name, - symbol, - origin.moduleSymbol.name, - origin.isFromPackageJson ? autoImportProviderChecker! : checker); + const { origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host)); + const info = exportMap.get(file.path, entry.data.exportMapKey); const result = info && context.tryResolve(info, !isExternalModuleNameRelative(stripQuotes(origin.moduleSymbol.name))); if (!result) return entry; @@ -763,6 +756,7 @@ namespace ts.Completions { function originToCompletionEntryData(origin: SymbolOriginInfoExport): CompletionEntryData | undefined { return { exportName: origin.exportName, + exportMapKey: origin.exportMapKey, fileName: origin.fileName, ambientModuleName: origin.fileName ? undefined : stripQuotes(origin.moduleSymbol.name), isPackageJsonImport: origin.isFromPackageJson ? true : undefined, @@ -1762,19 +1756,36 @@ namespace ts.Completions { const index = symbols.length; symbols.push(firstAccessibleSymbol); const moduleSymbol = firstAccessibleSymbol.parent; - if (!moduleSymbol || !isExternalModuleSymbol(moduleSymbol)) { + if (!moduleSymbol || + !isExternalModuleSymbol(moduleSymbol) || + typeChecker.tryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.name, moduleSymbol) !== firstAccessibleSymbol + ) { symbolToOriginInfoMap[index] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) }; } else { - const origin: SymbolOriginInfoExport = { - kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), + const fileName = isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? getSourceFileOfModule(moduleSymbol)?.fileName : undefined; + const { moduleSpecifier } = codefix.getModuleSpecifierForBestExportInfo([{ + exportKind: ExportKind.Named, + moduleFileName: fileName, + isFromPackageJson: false, moduleSymbol, - isDefaultExport: false, - symbolName: firstAccessibleSymbol.name, - exportName: firstAccessibleSymbol.name, - fileName: isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? cast(moduleSymbol.valueDeclaration, isSourceFile).fileName : undefined, - }; - symbolToOriginInfoMap[index] = origin; + symbol: firstAccessibleSymbol, + targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags, + }], sourceFile, program, host, preferences) || {}; + + if (moduleSpecifier) { + const origin: SymbolOriginInfoResolvedExport = { + kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport), + moduleSymbol, + isDefaultExport: false, + symbolName: firstAccessibleSymbol.name, + exportName: firstAccessibleSymbol.name, + fileName, + moduleSpecifier, + exportMapKey: "", // gross, but this will never be inspected since it includes the module specifier already + }; + symbolToOriginInfoMap[index] = origin; + } } } else if (preferences.includeCompletionsWithInsertText) { @@ -2034,7 +2045,7 @@ namespace ts.Completions { preferences, !!importCompletionNode, context => { - exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => { + exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule, exportMapKey) => { if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return; if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; // `targetFlags` should be the same for each `info` @@ -2056,6 +2067,7 @@ namespace ts.Completions { kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export, moduleSpecifier, symbolName, + exportMapKey, exportName: exportInfo.exportKind === ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name, fileName: exportInfo.moduleFileName, isDefaultExport, @@ -2997,6 +3009,7 @@ namespace ts.Completions { symbolName: name, isDefaultExport, exportName: data.exportName, + exportMapKey: data.exportMapKey, fileName: data.fileName, isFromPackageJson: !!data.isPackageJsonImport, } diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 049cf66789f8a..4d49f63117434 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -46,8 +46,8 @@ namespace ts { isUsableByFile(importingFile: Path): boolean; clear(): void; add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void; - get(importingFile: Path, importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker): readonly SymbolExportInfo[] | undefined; - forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean) => void): void; + get(importingFile: Path, key: string): readonly SymbolExportInfo[] | undefined; + forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean, key: string) => void): void; releaseSymbols(): void; isEmpty(): boolean; /** @returns Whether the change resulted in the cache being cleared */ @@ -87,11 +87,12 @@ namespace ts { : getNameForExportedSymbol(namedSymbol, scriptTarget); const moduleName = stripQuotes(moduleSymbol.name); const id = exportInfoId++; + const target = skipAlias(symbol, checker); const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol; const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol; if (!storedSymbol || !storedModuleSymbol) symbols.set(id, [symbol, moduleSymbol]); - exportInfo.add(key(importedName, symbol, moduleName, checker), { + exportInfo.add(key(importedName, symbol, isExternalModuleNameRelative(moduleName) ? undefined : moduleName, checker), { id, symbolTableKey, symbolName: importedName, @@ -99,22 +100,22 @@ namespace ts { moduleFile, moduleFileName: moduleFile?.fileName, exportKind, - targetFlags: skipAlias(symbol, checker).flags, + targetFlags: target.flags, isFromPackageJson, symbol: storedSymbol, moduleSymbol: storedModuleSymbol, }); }, - get: (importingFile, importedName, symbol, moduleName, checker) => { + get: (importingFile, key) => { if (importingFile !== usableByFileName) return; - const result = exportInfo.get(key(importedName, symbol, moduleName, checker)); + const result = exportInfo.get(key); return result?.map(rehydrateCachedInfo); }, forEach: (importingFile, action) => { if (importingFile !== usableByFileName) return; exportInfo.forEach((info, key) => { const { symbolName, ambientModuleName } = parseKey(key); - action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName); + action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName, key); }); }, releaseSymbols: () => { @@ -183,35 +184,18 @@ namespace ts { }; } - function key(importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker) { - const unquoted = stripQuotes(moduleName); - const moduleKey = isExternalModuleNameRelative(unquoted) ? "/" : unquoted; - return `${importedName}|${createSymbolKey(skipAlias(symbol, checker), unquoted)}|${moduleKey}`; + function key(importedName: string, symbol: Symbol, ambientModuleName: string | undefined, checker: TypeChecker): string { + const moduleKey = ambientModuleName || ""; + return `${importedName}|${getSymbolId(skipAlias(symbol, checker))}|${moduleKey}`; } function parseKey(key: string) { const symbolName = key.substring(0, key.indexOf("|")); const moduleKey = key.substring(key.lastIndexOf("|") + 1); - const ambientModuleName = moduleKey === "/" ? undefined : moduleKey; + const ambientModuleName = moduleKey === "" ? undefined : moduleKey; return { symbolName, ambientModuleName }; } - function createSymbolKey(symbol: Symbol, sourceModuleName: string) { - let key = symbol.name; - let seenModule = false; - while (symbol.parent) { - seenModule = isExternalModuleSymbol(symbol.parent); - key += `,${symbol.parent.name}`; - symbol = symbol.parent; - } - if (!seenModule) { - const decl = symbol.declarations?.[0]; - const fileName = decl && getSourceFileOfNode(decl).fileName; - key += fileName || sourceModuleName; - } - return key; - } - function fileIsGlobalOnly(file: SourceFile) { return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames; } diff --git a/src/services/types.ts b/src/services/types.ts index 816c83771b0a5..08a5eb17b1f09 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -1199,6 +1199,8 @@ namespace ts { * in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default. */ exportName: string; + /** The key in the `ExportMapCache` where the completion entry's `SymbolExportInfo[]` is found */ + exportMapKey: string; /** * Set for auto imports with eagerly resolved module specifiers. */ diff --git a/tests/cases/fourslash/completionsImport_reexportTransient.ts b/tests/cases/fourslash/completionsImport_reexportTransient.ts new file mode 100644 index 0000000000000..30b16c9e853f8 --- /dev/null +++ b/tests/cases/fourslash/completionsImport_reexportTransient.ts @@ -0,0 +1,33 @@ +/// + +// @esModuleInterop: true + +// @Filename: /transient.d.ts +//// declare const map: { [K in "one"]: number }; +//// export = map; + +// @Filename: /r1.ts +//// export { one } from "./transient"; + +// @Filename: /r2.ts +//// export { one } from "./r1"; + +// @Filename: /index.ts +//// one/**/ + +goTo.marker(""); + +verify.completions({ + marker: "", + exact: completion.globalsPlus([{ + name: "one", + source: "./transient", + sourceDisplay: "./transient", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions, + }]), + preferences: { + includeCompletionsForModuleExports: true, + allowIncompleteCompletions: true, + } +});