Skip to content

Commit 300a53c

Browse files
Auto-import perf: Do symbol name/flags filtering before cache rehydration (#47678)
* Do symbol name filtering before cache rehydration * Fix typo Co-authored-by: Nathan Shively-Sanders <[email protected]> * Update test * Fix variable clobbered in merge conflict Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent 69d06cb commit 300a53c

File tree

4 files changed

+66
-40
lines changed

4 files changed

+66
-40
lines changed

src/services/completions.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2495,19 +2495,26 @@ namespace ts.Completions {
24952495
preferences,
24962496
!!importCompletionNode,
24972497
context => {
2498-
exportInfo.forEach(sourceFile.path, (info, getSymbolName, isFromAmbientModule, exportMapKey) => {
2499-
const symbolName = getSymbolName(/*preferCapitalized*/ isRightOfOpenTag);
2500-
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return;
2501-
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
2502-
// `targetFlags` should be the same for each `info`
2503-
if (!isTypeOnlyLocation && !importCompletionNode && !(info[0].targetFlags & SymbolFlags.Value)) return;
2504-
if (isTypeOnlyLocation && !(info[0].targetFlags & (SymbolFlags.Module | SymbolFlags.Type))) return;
2505-
// Do not try to auto-import something with a lowercase first letter for a JSX tag
2506-
const firstChar = symbolName.charCodeAt(0);
2507-
if (isRightOfOpenTag && (firstChar < CharacterCodes.A || firstChar > CharacterCodes.Z)) return;
2508-
2509-
const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name));
2510-
if (isCompletionDetailsMatch || !detailsEntryId && charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) {
2498+
exportInfo.search(
2499+
sourceFile.path,
2500+
/*preferCapitalized*/ isRightOfOpenTag,
2501+
(symbolName, targetFlags) => {
2502+
if (!isIdentifierText(symbolName, getEmitScriptTarget(host.getCompilationSettings()))) return false;
2503+
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return false;
2504+
if (!isTypeOnlyLocation && !importCompletionNode && !(targetFlags & SymbolFlags.Value)) return false;
2505+
if (isTypeOnlyLocation && !(targetFlags & (SymbolFlags.Module | SymbolFlags.Type))) return false;
2506+
// Do not try to auto-import something with a lowercase first letter for a JSX tag
2507+
const firstChar = symbolName.charCodeAt(0);
2508+
if (isRightOfOpenTag && (firstChar < CharacterCodes.A || firstChar > CharacterCodes.Z)) return false;
2509+
2510+
if (detailsEntryId) return true;
2511+
return charactersFuzzyMatchInString(symbolName, lowerCaseTokenText);
2512+
},
2513+
(info, symbolName, isFromAmbientModule, exportMapKey) => {
2514+
if (detailsEntryId && !some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name))) {
2515+
return;
2516+
}
2517+
25112518
const defaultExportInfo = find(info, isImportableExportInfo);
25122519
if (!defaultExportInfo) {
25132520
return;
@@ -2531,7 +2538,7 @@ namespace ts.Completions {
25312538
isFromPackageJson: exportInfo.isFromPackageJson,
25322539
});
25332540
}
2534-
});
2541+
);
25352542

25362543
hasUnresolvedAutoImports = context.resolutionLimitExceeded();
25372544
}

src/services/exportInfoMap.ts

Lines changed: 18 additions & 20 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+
capitalizedSymbolName: string | undefined;
3233
symbolTableKey: __String;
3334
moduleName: string;
3435
moduleFile: SourceFile | undefined;
@@ -48,7 +49,7 @@ namespace ts {
4849
clear(): void;
4950
add(importingFile: Path, symbol: Symbol, key: __String, moduleSymbol: Symbol, moduleFile: SourceFile | undefined, exportKind: ExportKind, isFromPackageJson: boolean, checker: TypeChecker): void;
5051
get(importingFile: Path, key: string): readonly SymbolExportInfo[] | undefined;
51-
forEach(importingFile: Path, action: (info: readonly SymbolExportInfo[], getSymbolName: (preferCapitalized?: boolean) => string, isFromAmbientModule: boolean, key: string) => void): void;
52+
search(importingFile: Path, preferCapitalized: boolean, matches: (name: string, targetFlags: SymbolFlags) => boolean, action: (info: readonly SymbolExportInfo[], symbolName: string, isFromAmbientModule: boolean, key: string) => void): void;
5253
releaseSymbols(): void;
5354
isEmpty(): boolean;
5455
/** @returns Whether the change resulted in the cache being cleared */
@@ -121,9 +122,12 @@ namespace ts {
121122
// 3. Otherwise, we have a default/namespace import that can be imported by any name, and
122123
// `symbolTableKey` will be something undesirable like `export=` or `default`, so we try to
123124
// get a better name.
124-
const importedName = exportKind === ExportKind.Named || isExternalModuleSymbol(namedSymbol)
125+
const names = exportKind === ExportKind.Named || isExternalModuleSymbol(namedSymbol)
125126
? unescapeLeadingUnderscores(symbolTableKey)
126-
: getNameForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined);
127+
: getNamesForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined);
128+
129+
const symbolName = typeof names === "string" ? names : names[0];
130+
const capitalizedSymbolName = typeof names === "string" ? undefined : names[1];
127131

128132
const moduleName = stripQuotes(moduleSymbol.name);
129133
const id = exportInfoId++;
@@ -132,10 +136,11 @@ namespace ts {
132136
const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol;
133137
if (!storedSymbol || !storedModuleSymbol) symbols.set(id, [symbol, moduleSymbol]);
134138

135-
exportInfo.add(key(importedName, symbol, isExternalModuleNameRelative(moduleName) ? undefined : moduleName, checker), {
139+
exportInfo.add(key(symbolName, symbol, isExternalModuleNameRelative(moduleName) ? undefined : moduleName, checker), {
136140
id,
137141
symbolTableKey,
138-
symbolName: importedName,
142+
symbolName,
143+
capitalizedSymbolName,
139144
moduleName,
140145
moduleFile,
141146
moduleFileName: moduleFile?.fileName,
@@ -152,24 +157,17 @@ namespace ts {
152157
const result = exportInfo.get(key);
153158
return result?.map(rehydrateCachedInfo);
154159
},
155-
forEach: (importingFile, action) => {
160+
search: (importingFile, preferCapitalized, matches, action) => {
156161
if (importingFile !== usableByFileName) return;
157162
exportInfo.forEach((info, key) => {
158163
const { symbolName, ambientModuleName } = parseKey(key);
159-
const rehydrated = info.map(rehydrateCachedInfo);
160-
const filtered = rehydrated.filter((r, i) => isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName));
161-
if (filtered.length) {
162-
action(
163-
filtered,
164-
preferCapitalized => {
165-
const { symbol, exportKind } = rehydrated[0];
166-
const namedSymbol = exportKind === ExportKind.Default && getLocalSymbolForExportDefault(symbol) || symbol;
167-
return preferCapitalized
168-
? getNameForExportedSymbol(namedSymbol, /*scriptTarget*/ undefined, /*preferCapitalized*/ true)
169-
: symbolName;
170-
},
171-
!!ambientModuleName,
172-
key);
164+
const name = preferCapitalized && info[0].capitalizedSymbolName || symbolName;
165+
if (matches(name, info[0].targetFlags)) {
166+
const rehydrated = info.map(rehydrateCachedInfo);
167+
const filtered = rehydrated.filter((r, i) => isNotShadowedByDeeperNodeModulesPackage(r, info[i].packageName));
168+
if (filtered.length) {
169+
action(filtered, name, !!ambientModuleName, key);
170+
}
173171
}
174172
});
175173
},

src/services/utilities.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3210,13 +3210,34 @@ namespace ts {
32103210
return isArray(valueOrArray) ? first(valueOrArray) : valueOrArray;
32113211
}
32123212

3213+
export function getNamesForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined): string | [lowercase: string, capitalized: string] {
3214+
if (needsNameFromDeclaration(symbol)) {
3215+
const fromDeclaration = getDefaultLikeExportNameFromDeclaration(symbol);
3216+
if (fromDeclaration) return fromDeclaration;
3217+
const fileNameCase = codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*preferCapitalized*/ false);
3218+
const capitalized = codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, /*preferCapitalized*/ true);
3219+
if (fileNameCase === capitalized) return fileNameCase;
3220+
return [fileNameCase, capitalized];
3221+
}
3222+
return symbol.name;
3223+
}
3224+
32133225
export function getNameForExportedSymbol(symbol: Symbol, scriptTarget: ScriptTarget | undefined, preferCapitalized?: boolean) {
3214-
if (!(symbol.flags & SymbolFlags.Transient) && (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default)) {
3226+
if (needsNameFromDeclaration(symbol)) {
32153227
// Name of "export default foo;" is "foo". Name of "export default 0" is the filename converted to camelCase.
3216-
return firstDefined(symbol.declarations, d => isExportAssignment(d) ? tryCast(skipOuterExpressions(d.expression), isIdentifier)?.text : undefined)
3228+
return getDefaultLikeExportNameFromDeclaration(symbol)
32173229
|| codefix.moduleSymbolToValidIdentifier(getSymbolParentOrFail(symbol), scriptTarget, !!preferCapitalized);
32183230
}
32193231
return symbol.name;
3232+
3233+
}
3234+
3235+
function needsNameFromDeclaration(symbol: Symbol) {
3236+
return !(symbol.flags & SymbolFlags.Transient) && (symbol.escapedName === InternalSymbolName.ExportEquals || symbol.escapedName === InternalSymbolName.Default);
3237+
}
3238+
3239+
function getDefaultLikeExportNameFromDeclaration(symbol: Symbol) {
3240+
return firstDefined(symbol.declarations, d => isExportAssignment(d) ? tryCast(skipOuterExpressions(d.expression), isIdentifier)?.text : undefined);
32203241
}
32213242

32223243
function getSymbolParentOrFail(symbol: Symbol) {

src/testRunner/unittests/tsserver/exportMapCache.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ namespace ts.projectSystem {
8787
// transient symbols are recreated with every new checker.
8888
const programBefore = project.getCurrentProgram()!;
8989
let sigintPropBefore: readonly SymbolExportInfo[] | undefined;
90-
exportMapCache.forEach(bTs.path as Path, (info, getSymbolName) => {
91-
if (getSymbolName() === "SIGINT") sigintPropBefore = info;
90+
exportMapCache.search(bTs.path as Path, /*preferCapitalized*/ false, returnTrue, (info, symbolName) => {
91+
if (symbolName === "SIGINT") sigintPropBefore = info;
9292
});
9393
assert.ok(sigintPropBefore);
9494
assert.ok(sigintPropBefore![0].symbol.flags & SymbolFlags.Transient);
@@ -113,8 +113,8 @@ namespace ts.projectSystem {
113113

114114
// Get same info from cache again
115115
let sigintPropAfter: readonly SymbolExportInfo[] | undefined;
116-
exportMapCache.forEach(bTs.path as Path, (info, getSymbolName) => {
117-
if (getSymbolName() === "SIGINT") sigintPropAfter = info;
116+
exportMapCache.search(bTs.path as Path, /*preferCapitalized*/ false, returnTrue, (info, symbolName) => {
117+
if (symbolName === "SIGINT") sigintPropAfter = info;
118118
});
119119
assert.ok(sigintPropAfter);
120120
assert.notEqual(symbolIdBefore, getSymbolId(sigintPropAfter![0].symbol));

0 commit comments

Comments
 (0)