Skip to content

Commit f80bc3f

Browse files
authored
Store symbol table map key in CachedSymbolExportInfo (#45289)
* Store symbol table map key in CachedSymbolExportInfo * Remove debug assertion * Filter out known symbols (again) and private identifiers
1 parent 2bae169 commit f80bc3f

File tree

6 files changed

+61
-21
lines changed

6 files changed

+61
-21
lines changed

src/compiler/checker.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,7 @@ namespace ts {
581581
getEmitResolver,
582582
getExportsOfModule: getExportsOfModuleAsArray,
583583
getExportsAndPropertiesOfModule,
584+
forEachExportAndPropertyOfModule,
584585
getSymbolWalker: createGetSymbolWalker(
585586
getRestTypeOfSignature,
586587
getTypePredicateOfSignature,
@@ -3532,6 +3533,24 @@ namespace ts {
35323533
return exports;
35333534
}
35343535

3536+
function forEachExportAndPropertyOfModule(moduleSymbol: Symbol, cb: (symbol: Symbol, key: __String) => void): void {
3537+
const exports = getExportsOfModule(moduleSymbol);
3538+
exports.forEach((symbol, key) => {
3539+
if (!isReservedMemberName(key)) {
3540+
cb(symbol, key);
3541+
}
3542+
});
3543+
const exportEquals = resolveExternalModuleSymbol(moduleSymbol);
3544+
if (exportEquals !== moduleSymbol) {
3545+
const type = getTypeOfSymbol(exportEquals);
3546+
if (shouldTreatPropertiesOfExternalModuleAsExports(type)) {
3547+
getPropertiesOfType(type).forEach(symbol => {
3548+
cb(symbol, symbol.escapedName);
3549+
});
3550+
}
3551+
}
3552+
}
3553+
35353554
function tryGetMemberInModuleExports(memberName: __String, moduleSymbol: Symbol): Symbol | undefined {
35363555
const symbolTable = getExportsOfModule(moduleSymbol);
35373556
if (symbolTable) {

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4225,6 +4225,7 @@ namespace ts {
42254225
getExportsOfModule(moduleSymbol: Symbol): Symbol[];
42264226
/** Unlike `getExportsOfModule`, this includes properties of an `export =` value. */
42274227
/* @internal */ getExportsAndPropertiesOfModule(moduleSymbol: Symbol): Symbol[];
4228+
/* @internal */ forEachExportAndPropertyOfModule(moduleSymbol: Symbol, cb: (symbol: Symbol, key: __String) => void): void;
42284229
getJsxIntrinsicTagNamesAt(location: Node): Symbol[];
42294230
isOptionalParameter(node: ParameterDeclaration): boolean;
42304231
getAmbientModules(): Symbol[];

src/compiler/utilities.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3289,6 +3289,10 @@ namespace ts {
32893289
return startsWith(symbol.escapedName as string, "__@");
32903290
}
32913291

3292+
export function isPrivateIdentifierSymbol(symbol: Symbol): boolean {
3293+
return startsWith(symbol.escapedName as string, "__#");
3294+
}
3295+
32923296
/**
32933297
* Includes the word "Symbol" with unicode escapes
32943298
*/

src/services/completions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,6 +1934,7 @@ namespace ts.Completions {
19341934
!!importCompletionNode,
19351935
context => {
19361936
exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => {
1937+
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return;
19371938
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
19381939
// `targetFlags` should be the same for each `info`
19391940
if (!isTypeOnlyLocation && !importCompletionNode && !(info[0].targetFlags & SymbolFlags.Value)) return;

src/services/exportInfoMap.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ namespace ts {
2929
// Used to rehydrate `symbol` and `moduleSymbol` when transient
3030
id: number;
3131
symbolName: string;
32+
symbolTableKey: __String;
3233
moduleName: string;
3334
moduleFile: SourceFile | undefined;
3435

@@ -44,7 +45,7 @@ namespace ts {
4445
export interface ExportInfoMap {
4546
isUsableByFile(importingFile: Path): boolean;
4647
clear(): void;
47-
add(importingFile: Path, symbol: Symbol, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void;
48+
add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void;
4849
get(importingFile: Path, importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker): readonly SymbolExportInfo[] | undefined;
4950
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean) => void): void;
5051
releaseSymbols(): void;
@@ -71,13 +72,19 @@ namespace ts {
7172
symbols.clear();
7273
usableByFileName = undefined;
7374
},
74-
add: (importingFile, symbol, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => {
75+
add: (importingFile, symbol, symbolTableKey, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => {
7576
if (importingFile !== usableByFileName) {
7677
cache.clear();
7778
usableByFileName = importingFile;
7879
}
7980
const isDefault = exportKind === ExportKind.Default;
80-
const importedName = getNameForExportedSymbol(isDefault && getLocalSymbolForExportDefault(symbol) || symbol, scriptTarget);
81+
const namedSymbol = isDefault && getLocalSymbolForExportDefault(symbol) || symbol;
82+
// A re-export merged with an export from a module augmentation can result in `symbol`
83+
// being an external module symbol; the name it is re-exported by will be `symbolTableKey`
84+
// (which comes from the keys of `moduleSymbol.exports`.)
85+
const importedName = isExternalModuleSymbol(namedSymbol)
86+
? unescapeLeadingUnderscores(symbolTableKey)
87+
: getNameForExportedSymbol(namedSymbol, scriptTarget);
8188
const moduleName = stripQuotes(moduleSymbol.name);
8289
const id = exportInfoId++;
8390
const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol;
@@ -86,6 +93,7 @@ namespace ts {
8693

8794
exportInfo.add(key(importedName, symbol, moduleName, checker), {
8895
id,
96+
symbolTableKey,
8997
symbolName: importedName,
9098
moduleName,
9199
moduleFile,
@@ -160,12 +168,10 @@ namespace ts {
160168
const moduleSymbol = info.moduleSymbol || cachedModuleSymbol || Debug.checkDefined(info.moduleFile
161169
? checker.getMergedSymbol(info.moduleFile.symbol)
162170
: checker.tryFindAmbientModule(info.moduleName));
163-
const symbolName = exportKind === ExportKind.Default
164-
? InternalSymbolName.Default
165-
: info.symbolName;
166171
const symbol = info.symbol || cachedSymbol || Debug.checkDefined(exportKind === ExportKind.ExportEquals
167172
? checker.resolveExternalModuleSymbol(moduleSymbol)
168-
: checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol));
173+
: checker.tryGetMemberInModuleExportsAndProperties(unescapeLeadingUnderscores(info.symbolTableKey), moduleSymbol),
174+
`Could not find symbol '${info.symbolName}' by key '${info.symbolTableKey}' in module ${moduleSymbol.name}`);
169175
symbols.set(id, [symbol, moduleSymbol]);
170176
return {
171177
symbol,
@@ -330,30 +336,32 @@ namespace ts {
330336
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
331337
// Note: I think we shouldn't actually see resolved module symbols here, but weird merges
332338
// can cause it to happen: see 'completionsImport_mergedReExport.ts'
333-
if (defaultInfo && !checker.isUndefinedSymbol(defaultInfo.symbol) && !isExternalModuleSymbol(defaultInfo.symbol)) {
339+
if (defaultInfo && isImportableSymbol(defaultInfo.symbol, checker)) {
334340
cache.add(
335341
importingFile.path,
336342
defaultInfo.symbol,
343+
defaultInfo.exportKind === ExportKind.Default ? InternalSymbolName.Default : InternalSymbolName.ExportEquals,
337344
moduleSymbol,
338345
moduleFile,
339346
defaultInfo.exportKind,
340347
isFromPackageJson,
341348
scriptTarget,
342349
checker);
343350
}
344-
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
345-
if (exported !== defaultInfo?.symbol && !isKnownSymbol(exported) && !isExternalModuleSymbol(exported) && addToSeen(seenExports, exported)) {
351+
checker.forEachExportAndPropertyOfModule(moduleSymbol, (exported, key) => {
352+
if (exported !== defaultInfo?.symbol && isImportableSymbol(exported, checker) && addToSeen(seenExports, exported)) {
346353
cache.add(
347354
importingFile.path,
348355
exported,
356+
key,
349357
moduleSymbol,
350358
moduleFile,
351359
ExportKind.Named,
352360
isFromPackageJson,
353361
scriptTarget,
354362
checker);
355363
}
356-
}
364+
});
357365
});
358366

359367
host.log?.(`getExportInfoMap: done in ${timestamp() - start} ms`);
@@ -368,6 +376,10 @@ namespace ts {
368376
return info && { symbol, exportKind, ...info };
369377
}
370378

379+
function isImportableSymbol(symbol: Symbol, checker: TypeChecker) {
380+
return !checker.isUndefinedSymbol(symbol) && !checker.isUnknownSymbol(symbol) && !isKnownSymbol(symbol) && !isPrivateIdentifierSymbol(symbol);
381+
}
382+
371383
function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined {
372384
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
373385
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };

tests/cases/fourslash/server/completionsImport_mergedReExport.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,26 +30,29 @@
3030

3131
verify.completions({
3232
marker: "",
33+
includes: [{
34+
name: "Config",
35+
source: "/node_modules/@jest/types/index",
36+
hasAction: true,
37+
sortText: completion.SortText.AutoImportSuggestions,
38+
}],
3339
preferences: {
3440
includeCompletionsForModuleExports: true,
3541
},
3642
});
3743

3844
edit.insert("o");
3945

40-
// Should not crash
4146
verify.completions({
4247
marker: "",
48+
includes: [{
49+
name: "Config",
50+
source: "@jest/types",
51+
hasAction: true,
52+
sortText: completion.SortText.AutoImportSuggestions,
53+
}],
4354
preferences: {
4455
includeCompletionsForModuleExports: true,
56+
allowIncompleteCompletions: true,
4557
},
4658
});
47-
48-
// Because of the way `Config` is merged, we are actually not including it
49-
// in completions here, though it would be better if we could. The `exports`
50-
// of "@jest/types/index" would contain an alias symbol named `Config` without
51-
// the merge from ts-jest, but with the merge, the `exports` contains the merge
52-
// of `namespace Config` and the "@jest/types/Config" module symbol. This is
53-
// unexpected (to me) and difficult to work with, and might be wrong? My
54-
// expectation would have been to preserve the export alias symbol, but let it
55-
// *resolve* to the merge of the SourceFile and the namespace declaration.

0 commit comments

Comments
 (0)