Skip to content

Commit 64b8172

Browse files
authored
Auto-imports: fix some exports being incorrectly stored as re-exports of others due to key conflict (#45792)
* Ensure symbol key unique when target is a local symbol exported elsewhere * Add test * Support targets without declarations * Best key yet * A-ha moment * Clean up types * Update API * Update unit test
1 parent f6c0231 commit 64b8172

File tree

8 files changed

+222
-96
lines changed

8 files changed

+222
-96
lines changed

src/services/completions.ts

Lines changed: 86 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -74,23 +74,23 @@ namespace ts.Completions {
7474

7575
interface SymbolOriginInfo {
7676
kind: SymbolOriginInfoKind;
77-
symbolName?: string;
78-
moduleSymbol?: Symbol;
7977
isDefaultExport?: boolean;
8078
isFromPackageJson?: boolean;
81-
exportName?: string;
8279
fileName?: string;
83-
moduleSpecifier?: string;
8480
}
8581

8682
interface SymbolOriginInfoExport extends SymbolOriginInfo {
8783
symbolName: string;
8884
moduleSymbol: Symbol;
8985
isDefaultExport: boolean;
9086
exportName: string;
87+
exportMapKey: string;
9188
}
9289

93-
interface SymbolOriginInfoResolvedExport extends SymbolOriginInfoExport {
90+
interface SymbolOriginInfoResolvedExport extends SymbolOriginInfo {
91+
symbolName: string;
92+
moduleSymbol: Symbol;
93+
exportName: string;
9494
moduleSpecifier: string;
9595
}
9696

@@ -294,6 +294,10 @@ namespace ts.Completions {
294294
}
295295
}
296296

297+
function completionEntryDataIsResolved(data: CompletionEntryDataAutoImport | undefined): data is CompletionEntryDataResolved {
298+
return !!data?.moduleSpecifier;
299+
}
300+
297301
function continuePreviousIncompleteResponse(
298302
cache: IncompleteCompletionsCache,
299303
file: SourceFile,
@@ -308,9 +312,6 @@ namespace ts.Completions {
308312

309313
const lowerCaseTokenText = location.text.toLowerCase();
310314
const exportMap = getExportInfoMap(file, host, program, cancellationToken);
311-
const checker = program.getTypeChecker();
312-
const autoImportProvider = host.getPackageJsonAutoImportProvider?.();
313-
const autoImportProviderChecker = autoImportProvider?.getTypeChecker();
314315
const newEntries = resolvingModuleSpecifiers(
315316
"continuePreviousIncompleteResponse",
316317
host,
@@ -320,7 +321,7 @@ namespace ts.Completions {
320321
/*isForImportStatementCompletion*/ false,
321322
context => {
322323
const entries = mapDefined(previousResponse.entries, entry => {
323-
if (!entry.hasAction || !entry.source || !entry.data || entry.data.moduleSpecifier) {
324+
if (!entry.hasAction || !entry.source || !entry.data || completionEntryDataIsResolved(entry.data)) {
324325
// Not an auto import or already resolved; keep as is
325326
return entry;
326327
}
@@ -329,13 +330,8 @@ namespace ts.Completions {
329330
return undefined;
330331
}
331332

332-
const { symbol, origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host));
333-
const info = exportMap.get(
334-
file.path,
335-
entry.name,
336-
symbol,
337-
origin.moduleSymbol.name,
338-
origin.isFromPackageJson ? autoImportProviderChecker! : checker);
333+
const { origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host));
334+
const info = exportMap.get(file.path, entry.data.exportMapKey);
339335

340336
const result = info && context.tryResolve(info, !isExternalModuleNameRelative(stripQuotes(origin.moduleSymbol.name)));
341337
if (!result) return entry;
@@ -760,14 +756,56 @@ namespace ts.Completions {
760756
return text.replace(/\$/gm, "\\$");
761757
}
762758

763-
function originToCompletionEntryData(origin: SymbolOriginInfoExport): CompletionEntryData | undefined {
764-
return {
759+
function originToCompletionEntryData(origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport): CompletionEntryData | undefined {
760+
const ambientModuleName = origin.fileName ? undefined : stripQuotes(origin.moduleSymbol.name);
761+
const isPackageJsonImport = origin.isFromPackageJson ? true : undefined;
762+
if (originIsResolvedExport(origin)) {
763+
const resolvedData: CompletionEntryDataResolved = {
764+
exportName: origin.exportName,
765+
moduleSpecifier: origin.moduleSpecifier,
766+
ambientModuleName,
767+
fileName: origin.fileName,
768+
isPackageJsonImport,
769+
};
770+
return resolvedData;
771+
}
772+
const unresolvedData: CompletionEntryDataUnresolved = {
765773
exportName: origin.exportName,
774+
exportMapKey: origin.exportMapKey,
766775
fileName: origin.fileName,
767776
ambientModuleName: origin.fileName ? undefined : stripQuotes(origin.moduleSymbol.name),
768777
isPackageJsonImport: origin.isFromPackageJson ? true : undefined,
769-
moduleSpecifier: originIsResolvedExport(origin) ? origin.moduleSpecifier : undefined,
770778
};
779+
return unresolvedData;
780+
}
781+
782+
function completionEntryDataToSymbolOriginInfo(data: CompletionEntryData, completionName: string, moduleSymbol: Symbol): SymbolOriginInfoExport | SymbolOriginInfoResolvedExport {
783+
const isDefaultExport = data.exportName === InternalSymbolName.Default;
784+
const isFromPackageJson = !!data.isPackageJsonImport;
785+
if (completionEntryDataIsResolved(data)) {
786+
const resolvedOrigin: SymbolOriginInfoResolvedExport = {
787+
kind: SymbolOriginInfoKind.ResolvedExport,
788+
exportName: data.exportName,
789+
moduleSpecifier: data.moduleSpecifier,
790+
symbolName: completionName,
791+
fileName: data.fileName,
792+
moduleSymbol,
793+
isDefaultExport,
794+
isFromPackageJson,
795+
};
796+
return resolvedOrigin;
797+
}
798+
const unresolvedOrigin: SymbolOriginInfoExport = {
799+
kind: SymbolOriginInfoKind.Export,
800+
exportName: data.exportName,
801+
exportMapKey: data.exportMapKey,
802+
symbolName: completionName,
803+
fileName: data.fileName,
804+
moduleSymbol,
805+
isDefaultExport,
806+
isFromPackageJson,
807+
};
808+
return unresolvedOrigin;
771809
}
772810

773811
function getInsertTextAndReplacementSpanForImportCompletion(name: string, importCompletionNode: Node, contextToken: Node | undefined, origin: SymbolOriginInfoResolvedExport, useSemicolons: boolean, options: CompilerOptions, preferences: UserPreferences) {
@@ -1762,19 +1800,35 @@ namespace ts.Completions {
17621800
const index = symbols.length;
17631801
symbols.push(firstAccessibleSymbol);
17641802
const moduleSymbol = firstAccessibleSymbol.parent;
1765-
if (!moduleSymbol || !isExternalModuleSymbol(moduleSymbol)) {
1803+
if (!moduleSymbol ||
1804+
!isExternalModuleSymbol(moduleSymbol) ||
1805+
typeChecker.tryGetMemberInModuleExportsAndProperties(firstAccessibleSymbol.name, moduleSymbol) !== firstAccessibleSymbol
1806+
) {
17661807
symbolToOriginInfoMap[index] = { kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberNoExport) };
17671808
}
17681809
else {
1769-
const origin: SymbolOriginInfoExport = {
1770-
kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport),
1810+
const fileName = isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? getSourceFileOfModule(moduleSymbol)?.fileName : undefined;
1811+
const { moduleSpecifier } = codefix.getModuleSpecifierForBestExportInfo([{
1812+
exportKind: ExportKind.Named,
1813+
moduleFileName: fileName,
1814+
isFromPackageJson: false,
17711815
moduleSymbol,
1772-
isDefaultExport: false,
1773-
symbolName: firstAccessibleSymbol.name,
1774-
exportName: firstAccessibleSymbol.name,
1775-
fileName: isExternalModuleNameRelative(stripQuotes(moduleSymbol.name)) ? cast(moduleSymbol.valueDeclaration, isSourceFile).fileName : undefined,
1776-
};
1777-
symbolToOriginInfoMap[index] = origin;
1816+
symbol: firstAccessibleSymbol,
1817+
targetFlags: skipAlias(firstAccessibleSymbol, typeChecker).flags,
1818+
}], sourceFile, program, host, preferences) || {};
1819+
1820+
if (moduleSpecifier) {
1821+
const origin: SymbolOriginInfoResolvedExport = {
1822+
kind: getNullableSymbolOriginInfoKind(SymbolOriginInfoKind.SymbolMemberExport),
1823+
moduleSymbol,
1824+
isDefaultExport: false,
1825+
symbolName: firstAccessibleSymbol.name,
1826+
exportName: firstAccessibleSymbol.name,
1827+
fileName,
1828+
moduleSpecifier,
1829+
};
1830+
symbolToOriginInfoMap[index] = origin;
1831+
}
17781832
}
17791833
}
17801834
else if (preferences.includeCompletionsWithInsertText) {
@@ -2034,7 +2088,7 @@ namespace ts.Completions {
20342088
preferences,
20352089
!!importCompletionNode,
20362090
context => {
2037-
exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => {
2091+
exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule, exportMapKey) => {
20382092
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return;
20392093
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
20402094
// `targetFlags` should be the same for each `info`
@@ -2056,6 +2110,7 @@ namespace ts.Completions {
20562110
kind: moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export,
20572111
moduleSpecifier,
20582112
symbolName,
2113+
exportMapKey,
20592114
exportName: exportInfo.exportKind === ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name,
20602115
fileName: exportInfo.moduleFileName,
20612116
isDefaultExport,
@@ -2974,7 +3029,7 @@ namespace ts.Completions {
29743029
return { contextToken: previousToken as Node, previousToken: previousToken as Node };
29753030
}
29763031

2977-
function getAutoImportSymbolFromCompletionEntryData(name: string, data: CompletionEntryData, program: Program, host: LanguageServiceHost): { symbol: Symbol, origin: SymbolOriginInfoExport } | undefined {
3032+
function getAutoImportSymbolFromCompletionEntryData(name: string, data: CompletionEntryData, program: Program, host: LanguageServiceHost): { symbol: Symbol, origin: SymbolOriginInfoExport | SymbolOriginInfoResolvedExport } | undefined {
29783033
const containingProgram = data.isPackageJsonImport ? host.getPackageJsonAutoImportProvider!()! : program;
29793034
const checker = containingProgram.getTypeChecker();
29803035
const moduleSymbol =
@@ -2989,18 +3044,7 @@ namespace ts.Completions {
29893044
if (!symbol) return undefined;
29903045
const isDefaultExport = data.exportName === InternalSymbolName.Default;
29913046
symbol = isDefaultExport && getLocalSymbolForExportDefault(symbol) || symbol;
2992-
return {
2993-
symbol,
2994-
origin: {
2995-
kind: data.moduleSpecifier ? SymbolOriginInfoKind.ResolvedExport : SymbolOriginInfoKind.Export,
2996-
moduleSymbol,
2997-
symbolName: name,
2998-
isDefaultExport,
2999-
exportName: data.exportName,
3000-
fileName: data.fileName,
3001-
isFromPackageJson: !!data.isPackageJsonImport,
3002-
}
3003-
};
3047+
return { symbol, origin: completionEntryDataToSymbolOriginInfo(data, name, moduleSymbol) };
30043048
}
30053049

30063050
interface CompletionEntryDisplayNameForSymbol {

src/services/exportInfoMap.ts

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ namespace ts {
4646
isUsableByFile(importingFile: Path): boolean;
4747
clear(): void;
4848
add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void;
49-
get(importingFile: Path, importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker): readonly SymbolExportInfo[] | undefined;
50-
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean) => void): void;
49+
get(importingFile: Path, key: string): readonly SymbolExportInfo[] | undefined;
50+
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean, key: string) => void): void;
5151
releaseSymbols(): void;
5252
isEmpty(): boolean;
5353
/** @returns Whether the change resulted in the cache being cleared */
@@ -87,34 +87,35 @@ namespace ts {
8787
: getNameForExportedSymbol(namedSymbol, scriptTarget);
8888
const moduleName = stripQuotes(moduleSymbol.name);
8989
const id = exportInfoId++;
90+
const target = skipAlias(symbol, checker);
9091
const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol;
9192
const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol;
9293
if (!storedSymbol || !storedModuleSymbol) symbols.set(id, [symbol, moduleSymbol]);
9394

94-
exportInfo.add(key(importedName, symbol, moduleName, checker), {
95+
exportInfo.add(key(importedName, symbol, isExternalModuleNameRelative(moduleName) ? undefined : moduleName, checker), {
9596
id,
9697
symbolTableKey,
9798
symbolName: importedName,
9899
moduleName,
99100
moduleFile,
100101
moduleFileName: moduleFile?.fileName,
101102
exportKind,
102-
targetFlags: skipAlias(symbol, checker).flags,
103+
targetFlags: target.flags,
103104
isFromPackageJson,
104105
symbol: storedSymbol,
105106
moduleSymbol: storedModuleSymbol,
106107
});
107108
},
108-
get: (importingFile, importedName, symbol, moduleName, checker) => {
109+
get: (importingFile, key) => {
109110
if (importingFile !== usableByFileName) return;
110-
const result = exportInfo.get(key(importedName, symbol, moduleName, checker));
111+
const result = exportInfo.get(key);
111112
return result?.map(rehydrateCachedInfo);
112113
},
113114
forEach: (importingFile, action) => {
114115
if (importingFile !== usableByFileName) return;
115116
exportInfo.forEach((info, key) => {
116117
const { symbolName, ambientModuleName } = parseKey(key);
117-
action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName);
118+
action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName, key);
118119
});
119120
},
120121
releaseSymbols: () => {
@@ -183,29 +184,18 @@ namespace ts {
183184
};
184185
}
185186

186-
function key(importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker) {
187-
const unquoted = stripQuotes(moduleName);
188-
const moduleKey = isExternalModuleNameRelative(unquoted) ? "/" : unquoted;
189-
const target = skipAlias(symbol, checker);
190-
return `${importedName}|${createSymbolKey(target)}|${moduleKey}`;
187+
function key(importedName: string, symbol: Symbol, ambientModuleName: string | undefined, checker: TypeChecker): string {
188+
const moduleKey = ambientModuleName || "";
189+
return `${importedName}|${getSymbolId(skipAlias(symbol, checker))}|${moduleKey}`;
191190
}
192191

193192
function parseKey(key: string) {
194193
const symbolName = key.substring(0, key.indexOf("|"));
195194
const moduleKey = key.substring(key.lastIndexOf("|") + 1);
196-
const ambientModuleName = moduleKey === "/" ? undefined : moduleKey;
195+
const ambientModuleName = moduleKey === "" ? undefined : moduleKey;
197196
return { symbolName, ambientModuleName };
198197
}
199198

200-
function createSymbolKey(symbol: Symbol) {
201-
let key = symbol.name;
202-
while (symbol.parent) {
203-
key += `,${symbol.parent.name}`;
204-
symbol = symbol.parent;
205-
}
206-
return key;
207-
}
208-
209199
function fileIsGlobalOnly(file: SourceFile) {
210200
return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames;
211201
}

src/services/types.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1187,24 +1187,32 @@ namespace ts {
11871187
entries: CompletionEntry[];
11881188
}
11891189

1190-
export interface CompletionEntryData {
1191-
/** The file name declaring the export's module symbol, if it was an external module */
1192-
fileName?: string;
1193-
/** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */
1194-
ambientModuleName?: string;
1195-
/** True if the export was found in the package.json AutoImportProvider */
1196-
isPackageJsonImport?: true;
1190+
export interface CompletionEntryDataAutoImport {
11971191
/**
11981192
* The name of the property or export in the module's symbol table. Differs from the completion name
11991193
* in the case of InternalSymbolName.ExportEquals and InternalSymbolName.Default.
12001194
*/
12011195
exportName: string;
1202-
/**
1203-
* Set for auto imports with eagerly resolved module specifiers.
1204-
*/
12051196
moduleSpecifier?: string;
1197+
/** The file name declaring the export's module symbol, if it was an external module */
1198+
fileName?: string;
1199+
/** The module name (with quotes stripped) of the export's module symbol, if it was an ambient module */
1200+
ambientModuleName?: string;
1201+
/** True if the export was found in the package.json AutoImportProvider */
1202+
isPackageJsonImport?: true;
12061203
}
12071204

1205+
export interface CompletionEntryDataUnresolved extends CompletionEntryDataAutoImport {
1206+
/** The key in the `ExportMapCache` where the completion entry's `SymbolExportInfo[]` is found */
1207+
exportMapKey: string;
1208+
}
1209+
1210+
export interface CompletionEntryDataResolved extends CompletionEntryDataAutoImport {
1211+
moduleSpecifier: string;
1212+
}
1213+
1214+
export type CompletionEntryData = CompletionEntryDataUnresolved | CompletionEntryDataResolved;
1215+
12081216
// see comments in protocol.ts
12091217
export interface CompletionEntry {
12101218
name: string;

0 commit comments

Comments
 (0)