Refactor export map cache to not store transient symbols (#44816)

* Add some failing tests around transient symbols

* Working, but slower

* A class is much faster, apparently

* This is probably best?

* Back to multimap

* Go back to single symbol cache

* Revert now-unnecessary generics

* Rename and reorganize

* Fix weird compound condition

* Clean up
This commit is contained in:
Andrew Branch authored and GitHub committed 2021-07-06 11:04:59 -06:00
1 parent fc5c765f81
commit 1da18c60d6
11 files changed
+641 -491

No files matched your search

+95
View File
@@ -0,0 +1,95 @@
/*@internal*/
namespace ts.server {
export interface ModuleSpecifierResolutionCacheHost {
watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher;
}
export function createModuleSpecifierCache(host: ModuleSpecifierResolutionCacheHost): ModuleSpecifierCache {
let containedNodeModulesWatchers: ESMap<string, FileWatcher> | undefined;
let cache: ESMap<Path, ResolvedModuleSpecifierInfo> | undefined;
let currentKey: string | undefined;
const result: ModuleSpecifierCache = {
get(fromFileName, toFileName, preferences) {
if (!cache || currentKey !== key(fromFileName, preferences)) return undefined;
return cache.get(toFileName);
},
set(fromFileName, toFileName, preferences, modulePaths, moduleSpecifiers) {
ensureCache(fromFileName, preferences).set(toFileName, createInfo(modulePaths, moduleSpecifiers, /*isAutoImportable*/ true));
// If any module specifiers were generated based off paths in node_modules,
// a package.json file in that package was read and is an input to the cached.
// Instead of watching each individual package.json file, set up a wildcard
// directory watcher for any node_modules referenced and clear the cache when
// it sees any changes.
if (moduleSpecifiers) {
for (const p of modulePaths) {
if (p.isInNodeModules) {
// No trailing slash
const nodeModulesPath = p.path.substring(0, p.path.indexOf(nodeModulesPathPart) + nodeModulesPathPart.length - 1);
if (!containedNodeModulesWatchers?.has(nodeModulesPath)) {
(containedNodeModulesWatchers ||= new Map()).set(
nodeModulesPath,
host.watchNodeModulesForPackageJsonChanges(nodeModulesPath),
);
}
}
}
}
},
setModulePaths(fromFileName, toFileName, preferences, modulePaths) {
const cache = ensureCache(fromFileName, preferences);
const info = cache.get(toFileName);
if (info) {
info.modulePaths = modulePaths;
}
else {
cache.set(toFileName, createInfo(modulePaths, /*moduleSpecifiers*/ undefined, /*isAutoImportable*/ undefined));
}
},
setIsAutoImportable(fromFileName, toFileName, preferences, isAutoImportable) {
const cache = ensureCache(fromFileName, preferences);
const info = cache.get(toFileName);
if (info) {
info.isAutoImportable = isAutoImportable;
}
else {
cache.set(toFileName, createInfo(/*modulePaths*/ undefined, /*moduleSpecifiers*/ undefined, isAutoImportable));
}
},
clear() {
containedNodeModulesWatchers?.forEach(watcher => watcher.close());
cache?.clear();
containedNodeModulesWatchers?.clear();
currentKey = undefined;
},
count() {
return cache ? cache.size : 0;
}
};
if (Debug.isDebugging) {
Object.defineProperty(result, "__cache", { get: () => cache });
}
return result;
function ensureCache(fromFileName: Path, preferences: UserPreferences) {
const newKey = key(fromFileName, preferences);
if (cache && (currentKey !== newKey)) {
result.clear();
}
currentKey = newKey;
return cache ||= new Map();
}
function key(fromFileName: Path, preferences: UserPreferences) {
return `${fromFileName},${preferences.importModuleSpecifierEnding},${preferences.importModuleSpecifierPreference}`;
}
function createInfo(
modulePaths: readonly ModulePath[] | undefined,
moduleSpecifiers: readonly string[] | undefined,
isAutoImportable: boolean | undefined,
): ResolvedModuleSpecifierInfo {
return { modulePaths, moduleSpecifiers, isAutoImportable };
}
}
}
+15 -8
View File
@@ -251,7 +251,7 @@ namespace ts.server {
public readonly getCanonicalFileName: GetCanonicalFileName;
/*@internal*/
private exportMapCache = createExportMapCache();
private exportMapCache: ExportInfoMap | undefined;
/*@internal*/
private changedFilesForExportMapCache: Set<Path> | undefined;
/*@internal*/
@@ -793,6 +793,7 @@ namespace ts.server {
this.cachedUnresolvedImportsPerFile = undefined!;
this.moduleSpecifierCache = undefined!;
this.directoryStructureHost = undefined!;
this.exportMapCache = undefined;
this.projectErrors = undefined;
// Clean up file watchers waiting for missing files
@@ -990,7 +991,7 @@ namespace ts.server {
/*@internal*/
markFileAsDirty(changedFile: Path) {
this.markAsDirty();
if (!this.exportMapCache.isEmpty()) {
if (this.exportMapCache && !this.exportMapCache.isEmpty()) {
(this.changedFilesForExportMapCache ||= new Set()).add(changedFile);
}
}
@@ -1192,7 +1193,8 @@ namespace ts.server {
}
}
if (!this.exportMapCache.isEmpty()) {
if (this.exportMapCache && !this.exportMapCache.isEmpty()) {
this.exportMapCache.releaseSymbols();
if (this.hasAddedorRemovedFiles || oldProgram && !this.program!.structureIsReused) {
this.exportMapCache.clear();
}
@@ -1201,10 +1203,10 @@ namespace ts.server {
const oldSourceFile = oldProgram.getSourceFileByPath(fileName);
const sourceFile = this.program!.getSourceFileByPath(fileName);
if (!oldSourceFile || !sourceFile) {
this.exportMapCache.clear();
this.exportMapCache!.clear();
return true;
}
return this.exportMapCache.onFileChanged(oldSourceFile, sourceFile, !!this.getTypeAcquisition().enable);
return this.exportMapCache!.onFileChanged(oldSourceFile, sourceFile, !!this.getTypeAcquisition().enable);
});
}
}
@@ -1666,8 +1668,13 @@ namespace ts.server {
}
/*@internal*/
getExportMapCache() {
return this.exportMapCache;
getCachedExportInfoMap() {
return this.exportMapCache ||= createCacheableExportInfoMap(this);
}
/*@internal*/
clearCachedExportInfoMap() {
this.exportMapCache?.clear();
}
/*@internal*/
@@ -2017,7 +2024,7 @@ namespace ts.server {
const oldProgram = this.getCurrentProgram();
const hasSameSetOfFiles = super.updateGraph();
if (oldProgram && oldProgram !== this.getCurrentProgram()) {
this.hostProject.getExportMapCache().clear();
this.hostProject.clearCachedExportInfoMap();
}
return hasSameSetOfFiles;
}
+1
View File
@@ -23,6 +23,7 @@
"typingsCache.ts",
"project.ts",
"editorServices.ts",
"moduleSpecifierCache.ts",
"packageJsonCache.ts",
"session.ts",
"scriptVersionCache.ts"
+14 -200
View File
@@ -231,11 +231,11 @@ namespace ts.codefix {
function getInfoWithChecker(checker: TypeChecker, isFromPackageJson: boolean): SymbolExportInfo | undefined {
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && skipAlias(defaultInfo.symbol, checker) === symbol) {
return { symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson };
return { symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: undefined, exportKind: defaultInfo.exportKind, isTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson };
}
const named = checker.tryGetMemberInModuleExportsAndProperties(symbol.name, moduleSymbol);
if (named && skipAlias(named, checker) === symbol) {
return { symbol: named, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson };
return { symbol: named, moduleSymbol, moduleFileName: undefined, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson };
}
}
}
@@ -256,12 +256,12 @@ namespace ts.codefix {
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && (defaultInfo.name === symbolName || moduleSymbolToValidIdentifier(moduleSymbol, compilerOptions.target) === symbolName) && skipAlias(defaultInfo.symbol, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
result.push({ symbol: defaultInfo.symbol, moduleSymbol, exportKind: defaultInfo.exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson });
result.push({ symbol: defaultInfo.symbol, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: defaultInfo.exportKind, isTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson });
}
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
if (exported.name === symbolName && skipAlias(exported, checker) === exportedSymbol && isImportable(program, moduleFile, isFromPackageJson)) {
result.push({ symbol: exported, moduleSymbol, exportKind: ExportKind.Named, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson });
result.push({ symbol: exported, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson });
}
}
});
@@ -272,7 +272,8 @@ namespace ts.codefix {
}
}
export function getModuleSpecifierForBestExportInfo(exportInfo: readonly SymbolExportInfo[],
export function getModuleSpecifierForBestExportInfo(
exportInfo: readonly SymbolExportInfo[],
importingFile: SourceFile,
program: Program,
host: LanguageServiceHost,
@@ -293,114 +294,6 @@ namespace ts.codefix {
return result && { ...result, computedWithoutCacheCount };
}
export interface SymbolToExportInfoMap {
get(importedName: string, symbol: Symbol, moduleSymbol: Symbol, checker: TypeChecker): readonly SymbolExportInfo[] | undefined;
forEach(getChecker: (isFromPackageJson: boolean) => TypeChecker, action: (info: readonly SymbolExportInfo[], name: string, isFromAmbientModule: boolean) => void): void;
}
export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program): SymbolToExportInfoMap {
const start = timestamp();
// Pulling the AutoImportProvider project will trigger its updateGraph if pending,
// which will invalidate the export map cache if things change, so pull it before
// checking the cache.
host.getPackageJsonAutoImportProvider?.();
const cache = host.getExportMapCache?.();
if (cache) {
const cached = cache.get(importingFile.path, program.getTypeChecker());
if (cached) {
host.log?.("getSymbolToExportInfoMap: cache hit");
const projectVersion = host.getProjectVersion?.();
return wrapMultiMap(cached, !projectVersion || cache.getProjectVersion() !== projectVersion);
}
else {
host.log?.("getSymbolToExportInfoMap: cache miss or empty; calculating new results");
}
}
const result: MultiMap<string, SymbolExportInfo> = createMultiMap();
const compilerOptions = program.getCompilerOptions();
const target = getEmitScriptTarget(compilerOptions);
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, _moduleFile, program, isFromPackageJson) => {
const checker = program.getTypeChecker();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && !checker.isUndefinedSymbol(defaultInfo.symbol)) {
const name = getNameForExportedSymbol(getLocalSymbolForExportDefault(defaultInfo.symbol) || defaultInfo.symbol, target);
result.add(key(name, defaultInfo.symbol, moduleSymbol, checker), {
symbol: defaultInfo.symbol,
moduleSymbol,
exportKind: defaultInfo.exportKind,
exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker),
isFromPackageJson,
});
}
const seenExports = new Map<Symbol, true>();
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
if (exported !== defaultInfo?.symbol && addToSeen(seenExports, exported)) {
result.add(key(getNameForExportedSymbol(exported, target), exported, moduleSymbol, checker), {
symbol: exported,
moduleSymbol,
exportKind: ExportKind.Named,
exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker),
isFromPackageJson,
});
}
}
});
if (cache) {
host.log?.("getSymbolToExportInfoMap: caching results");
cache.set(result, host.getProjectVersion?.());
}
host.log?.(`getSymbolToExportInfoMap: done in ${timestamp() - start} ms`);
return wrapMultiMap(result, /*isFromPreviousProjectVersion*/ false);
function key(importedName: string, alias: Symbol, moduleSymbol: Symbol, checker: TypeChecker) {
const moduleName = stripQuotes(moduleSymbol.name);
const moduleKey = isExternalModuleNameRelative(moduleName) ? "/" : moduleName;
const original = skipAlias(alias, checker);
return `${importedName}|${getSymbolId(original.declarations?.[0].symbol || original)}|${moduleKey}`;
}
function parseKey(key: string) {
const symbolName = key.substring(0, key.indexOf("|"));
const moduleKey = key.substring(key.lastIndexOf("|") + 1);
const ambientModuleName = moduleKey === "/" ? undefined : moduleKey;
return { symbolName, ambientModuleName };
}
function wrapMultiMap(map: MultiMap<string, SymbolExportInfo>, isFromPreviousProjectVersion: boolean): SymbolToExportInfoMap {
const wrapped: SymbolToExportInfoMap = {
get: (importedName, symbol, moduleSymbol, checker) => {
const info = map.get(key(importedName, symbol, moduleSymbol, checker));
return isFromPreviousProjectVersion ? info?.map(info => replaceTransientSymbols(info, checker)) : info;
},
forEach: (getChecker, action) => {
map.forEach((info, key) => {
const { symbolName, ambientModuleName } = parseKey(key);
action(
isFromPreviousProjectVersion ? info.map(i => replaceTransientSymbols(i, getChecker(i.isFromPackageJson))) : info,
symbolName,
!!ambientModuleName);
});
},
};
if (Debug.isDebugging) {
Object.defineProperty(wrapped, "__cache", { get: () => map });
}
return wrapped;
function replaceTransientSymbols(info: SymbolExportInfo, checker: TypeChecker) {
if (info.symbol.flags & SymbolFlags.Transient) {
info.symbol = checker.getMergedSymbol(info.symbol.declarations?.[0]?.symbol || info.symbol);
}
if (info.moduleSymbol.flags & SymbolFlags.Transient) {
info.moduleSymbol = checker.getMergedSymbol(info.moduleSymbol.declarations?.[0]?.symbol || info.moduleSymbol);
}
return info;
}
}
}
function isTypeOnlySymbol(s: Symbol, checker: TypeChecker): boolean {
return !(skipAlias(s, checker).flags & SymbolFlags.Value);
}
@@ -502,7 +395,7 @@ namespace ts.codefix {
});
}
function getExistingImportDeclarations({ moduleSymbol, exportKind, exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] {
function getExistingImportDeclarations({ moduleSymbol, exportKind, isTypeOnly: exportedSymbolIsTypeOnly }: SymbolExportInfo, checker: TypeChecker, importingFile: SourceFile, compilerOptions: CompilerOptions): readonly FixAddToExistingImportInfo[] {
// Can't use an es6 import for a type in JS.
if (exportedSymbolIsTypeOnly && isSourceFileJS(importingFile)) return emptyArray;
const importKind = getImportKind(importingFile, exportKind, compilerOptions);
@@ -566,10 +459,10 @@ namespace ts.codefix {
let computedWithoutCacheCount = 0;
const fixes = flatMap(moduleSymbols, exportInfo => {
const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo.moduleSymbol);
computedWithoutCacheCount += Number(computedWithoutCache);
computedWithoutCacheCount += computedWithoutCache ? 1 : 0;
return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixUseImportType =>
// `position` should only be undefined at a missing jsx namespace, in which case we shouldn't be looking for pure types.
exportInfo.exportedSymbolIsTypeOnly && isJs && position !== undefined
exportInfo.isTypeOnly && isJs && position !== undefined
? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo }
: {
kind: ImportFixKind.AddNew,
@@ -578,8 +471,9 @@ namespace ts.codefix {
useRequire,
typeOnly: preferTypeOnlyImport,
exportInfo,
});
});
}
);
});
return { computedWithoutCacheCount, fixes };
}
@@ -646,7 +540,7 @@ namespace ts.codefix {
if (!umdSymbol) return undefined;
const symbol = checker.getAliasedSymbol(umdSymbol);
const symbolName = umdSymbol.name;
const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, exportKind: ExportKind.UMD, exportedSymbolIsTypeOnly: false, isFromPackageJson: false }];
const exportInfos: readonly SymbolExportInfo[] = [{ symbol: umdSymbol, moduleSymbol: symbol, moduleFileName: undefined, exportKind: ExportKind.UMD, isTypeOnly: false, isFromPackageJson: false }];
const useRequire = shouldUseRequire(sourceFile, program);
const fixes = getImportFixes(exportInfos, symbolName, isIdentifier(token) ? token.getStart(sourceFile) : undefined, /*preferTypeOnlyImport*/ false, useRequire, program, sourceFile, host, preferences);
return { fixes, symbolName };
@@ -752,7 +646,7 @@ namespace ts.codefix {
!toFile && packageJsonFilter.allowsImportingAmbientModule(moduleSymbol, moduleSpecifierResolutionHost)
) {
const checker = program.getTypeChecker();
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, exportKind, exportedSymbolIsTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson });
originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, moduleFileName: toFile?.fileName, exportKind, isTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson });
}
}
forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => {
@@ -774,21 +668,6 @@ namespace ts.codefix {
return originalSymbolToExportInfos;
}
function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) {
const exported = getDefaultLikeExportWorker(moduleSymbol, checker);
if (!exported) return undefined;
const { symbol, exportKind } = exported;
const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions);
return info && { symbol, exportKind, ...info };
}
function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined {
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
}
function getExportEqualsImportKind(importingFile: SourceFile, compilerOptions: CompilerOptions): ImportKind {
const allowSyntheticDefaults = getAllowSyntheticDefaultImports(compilerOptions);
// 1. 'import =' will not work in es2015+, so the decision is between a default
@@ -815,43 +694,6 @@ namespace ts.codefix {
return allowSyntheticDefaults ? ImportKind.Default : ImportKind.CommonJS;
}
function getDefaultExportInfoWorker(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbolForMeaning: Symbol, readonly name: string } | undefined {
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
if (localSymbol) return { symbolForMeaning: localSymbol, name: localSymbol.name };
const name = getNameForExportDefault(defaultExport);
if (name !== undefined) return { symbolForMeaning: defaultExport, name };
if (defaultExport.flags & SymbolFlags.Alias) {
const aliased = checker.getImmediateAliasedSymbol(defaultExport);
if (aliased && aliased.parent) {
// - `aliased` will be undefined if the module is exporting an unresolvable name,
// but we can still offer completions for it.
// - `aliased.parent` will be undefined if the module is exporting `globalThis.something`,
// or another expression that resolves to a global.
return getDefaultExportInfoWorker(aliased, checker, compilerOptions);
}
}
if (defaultExport.escapedName !== InternalSymbolName.Default &&
defaultExport.escapedName !== InternalSymbolName.ExportEquals) {
return { symbolForMeaning: defaultExport, name: defaultExport.getName() };
}
return { symbolForMeaning: defaultExport, name: getNameForExportedSymbol(defaultExport, compilerOptions.target) };
}
function getNameForExportDefault(symbol: Symbol): string | undefined {
return symbol.declarations && firstDefined(symbol.declarations, declaration => {
if (isExportAssignment(declaration)) {
return tryCast(skipOuterExpressions(declaration.expression), isIdentifier)?.text;
}
else if (isExportSpecifier(declaration)) {
Debug.assert(declaration.name.text === InternalSymbolName.Default, "Expected the specifier to be a default export");
return declaration.propertyName && declaration.propertyName.text;
}
});
}
function codeActionForFix(context: textChanges.TextChangesContext, sourceFile: SourceFile, symbolName: string, fix: ImportFix, quotePreference: QuotePreference): CodeFixAction {
let diag!: DiagnosticAndArguments;
const changes = textChanges.ChangeTracker.with(context, tracker => {
@@ -1047,34 +889,6 @@ namespace ts.codefix {
return some(declarations, decl => !!(getMeaningFromDeclaration(decl) & meaning));
}
export function forEachExternalModuleToImportFrom(
program: Program,
host: LanguageServiceHost,
useAutoImportProvider: boolean,
cb: (module: Symbol, moduleFile: SourceFile | undefined, program: Program, isFromPackageJson: boolean) => void,
) {
forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false));
const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.();
if (autoImportProvider) {
const start = timestamp();
forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true));
host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`);
}
}
function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
for (const ambient of checker.getAmbientModules()) {
if (!stringContains(ambient.name, "*")) {
cb(ambient, /*sourceFile*/ undefined);
}
}
for (const sourceFile of allSourceFiles) {
if (isExternalOrCommonJsModule(sourceFile)) {
cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile);
}
}
}
export function moduleSymbolToValidIdentifier(moduleSymbol: Symbol, target: ScriptTarget | undefined): string {
return moduleSpecifierToValidIdentifier(removeFileExtension(stripQuotes(moduleSymbol.name)), target);
}
+10 -12
View File
@@ -297,7 +297,7 @@ namespace ts.Completions {
if (!previousResponse) return undefined;
const lowerCaseTokenText = location.text.toLowerCase();
const exportMap = codefix.getSymbolToExportInfoMap(file, host, program);
const exportMap = getExportInfoMap(file, host, program);
const checker = program.getTypeChecker();
const autoImportProvider = host.getPackageJsonAutoImportProvider?.();
const autoImportProviderChecker = autoImportProvider?.getTypeChecker();
@@ -321,9 +321,10 @@ namespace ts.Completions {
const { symbol, origin } = Debug.checkDefined(getAutoImportSymbolFromCompletionEntryData(entry.name, entry.data, program, host));
const info = exportMap.get(
file.path,
entry.name,
origin.isDefaultExport ? symbol.exportSymbol || symbol : symbol,
origin.moduleSymbol,
symbol,
origin.moduleSymbol.name,
origin.isFromPackageJson ? autoImportProviderChecker! : checker);
const result = info && context.tryResolve(info, !isExternalModuleNameRelative(stripQuotes(origin.moduleSymbol.name)));
@@ -1913,10 +1914,9 @@ namespace ts.Completions {
const moduleSpecifierCache = host.getModuleSpecifierCache?.();
const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : "";
const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program);
const exportInfo = getExportInfoMap(sourceFile, host, program);
const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.();
const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host);
const getChecker = (isFromPackageJson: boolean) => isFromPackageJson ? packageJsonAutoImportProvider!.getTypeChecker() : typeChecker;
resolvingModuleSpecifiers(
"collectAutoImports",
host,
@@ -1925,20 +1925,18 @@ namespace ts.Completions {
preferences,
!!importCompletionNode,
context => {
exportInfo.forEach(getChecker, (info, symbolName, isFromAmbientModule) => {
exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => {
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name));
if (isCompletionDetailsMatch || charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) {
if (isFromAmbientModule && !some(info, isImportableExportInfo)) {
const defaultExportInfo = find(info, isImportableExportInfo);
if (!defaultExportInfo) {
return;
}
// If we don't need to resolve module specifiers, we can use any re-export that is importable at all
// (We need to ensure that at least one is importable to show a completion.)
const { exportInfo = find(info, isImportableExportInfo), moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {};
if (!exportInfo) return;
const moduleFile = tryCast(exportInfo.moduleSymbol.valueDeclaration, isSourceFile);
const { exportInfo = defaultExportInfo, moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {};
const isDefaultExport = exportInfo.exportKind === ExportKind.Default;
const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol;
pushAutoImportSymbol(symbol, {
@@ -1946,7 +1944,7 @@ namespace ts.Completions {
moduleSpecifier,
symbolName,
exportName: exportInfo.exportKind === ExportKind.ExportEquals ? InternalSymbolName.ExportEquals : exportInfo.symbol.name,
fileName: moduleFile?.fileName,
fileName: exportInfo.moduleFileName,
isDefaultExport,
moduleSymbol: exportInfo.moduleSymbol,
isFromPackageJson: exportInfo.isFromPackageJson,
+411
View File
@@ -0,0 +1,411 @@
/*@internal*/
namespace ts {
export const enum ImportKind {
Named,
Default,
Namespace,
CommonJS,
}
export const enum ExportKind {
Named,
Default,
ExportEquals,
UMD,
}
export interface SymbolExportInfo {
readonly symbol: Symbol;
readonly moduleSymbol: Symbol;
/** Set if `moduleSymbol` is an external module, not an ambient module */
moduleFileName: string | undefined;
exportKind: ExportKind;
/** If true, can't use an es6 import from a js file. */
isTypeOnly: boolean;
/** True if export was only found via the package.json AutoImportProvider (for telemetry). */
isFromPackageJson: boolean;
}
interface CachedSymbolExportInfo {
// Used to rehydrate `symbol` and `moduleSymbol` when transient
id: number;
symbolName: string;
moduleName: string;
moduleFile: SourceFile | undefined;
// SymbolExportInfo, but optional symbols
readonly symbol: Symbol | undefined;
readonly moduleSymbol: Symbol | undefined;
moduleFileName: string | undefined;
exportKind: ExportKind;
isTypeOnly: boolean;
isFromPackageJson: boolean;
}
export interface ExportInfoMap {
isUsableByFile(importingFile: Path): boolean;
clear(): void;
add(importingFile: Path, symbol: Symbol, 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;
releaseSymbols(): void;
isEmpty(): boolean;
/** @returns Whether the change resulted in the cache being cleared */
onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean): boolean;
}
export interface CacheableExportInfoMapHost {
getCurrentProgram(): Program | undefined;
getPackageJsonAutoImportProvider(): Program | undefined;
}
export function createCacheableExportInfoMap(host: CacheableExportInfoMapHost): ExportInfoMap {
let exportInfoId = 1;
const exportInfo = createMultiMap<string, CachedSymbolExportInfo>();
const symbols = new Map<number, [symbol: Symbol, moduleSymbol: Symbol]>();
let usableByFileName: Path | undefined;
const cache: ExportInfoMap = {
isUsableByFile: importingFile => importingFile === usableByFileName,
isEmpty: () => !exportInfo.size,
clear: () => {
exportInfo.clear();
symbols.clear();
usableByFileName = undefined;
},
add: (importingFile, symbol, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => {
if (importingFile !== usableByFileName) {
cache.clear();
usableByFileName = importingFile;
}
const isDefault = exportKind === ExportKind.Default;
const importedName = getNameForExportedSymbol(isDefault && getLocalSymbolForExportDefault(symbol) || symbol, scriptTarget);
const moduleName = stripQuotes(moduleSymbol.name);
const id = exportInfoId++;
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), {
id,
symbolName: importedName,
moduleName,
moduleFile,
moduleFileName: moduleFile?.fileName,
exportKind,
isTypeOnly: !(skipAlias(symbol, checker).flags & SymbolFlags.Value),
isFromPackageJson,
symbol: storedSymbol,
moduleSymbol: storedModuleSymbol,
});
},
get: (importingFile, importedName, symbol, moduleName, checker) => {
if (importingFile !== usableByFileName) return;
const result = exportInfo.get(key(importedName, symbol, moduleName, checker));
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);
});
},
releaseSymbols: () => {
symbols.clear();
},
onFileChanged: (oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) => {
if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) {
// File is purely global; doesn't affect export map
return false;
}
if (
usableByFileName && usableByFileName !== newSourceFile.path ||
// If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node.
// Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list.
typeAcquisitionEnabled && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile) ||
// Module agumentation and ambient module changes can add or remove exports available to be auto-imported.
// Changes elsewhere in the file can change the *type* of an export in a module augmentation,
// but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache.
!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) ||
!ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)
) {
cache.clear();
return true;
}
usableByFileName = newSourceFile.path;
return false;
},
};
if (Debug.isDebugging) {
Object.defineProperty(cache, "__cache", { get: () => exportInfo });
}
return cache;
function rehydrateCachedInfo(info: CachedSymbolExportInfo): SymbolExportInfo {
if (info.symbol && info.moduleSymbol) return info as SymbolExportInfo;
const { id, exportKind, isTypeOnly, isFromPackageJson, moduleFileName } = info;
const [cachedSymbol, cachedModuleSymbol] = symbols.get(id) || emptyArray;
if (cachedSymbol && cachedModuleSymbol) {
return {
symbol: cachedSymbol,
moduleSymbol: cachedModuleSymbol,
moduleFileName,
exportKind,
isTypeOnly,
isFromPackageJson,
};
}
const checker = (isFromPackageJson
? host.getPackageJsonAutoImportProvider()!
: host.getCurrentProgram()!).getTypeChecker();
const moduleSymbol = info.moduleSymbol || cachedModuleSymbol || Debug.checkDefined(info.moduleFile
? checker.getMergedSymbol(info.moduleFile.symbol)
: checker.tryFindAmbientModule(info.moduleName));
const symbolName = exportKind === ExportKind.Default
? InternalSymbolName.Default
: info.symbolName;
const symbol = info.symbol || cachedSymbol || Debug.checkDefined(exportKind === ExportKind.ExportEquals
? checker.resolveExternalModuleSymbol(moduleSymbol)
: checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol));
symbols.set(id, [symbol, moduleSymbol]);
return {
symbol,
moduleSymbol,
moduleFileName,
exportKind,
isTypeOnly,
isFromPackageJson,
};
}
function key(importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker) {
const unquoted = stripQuotes(moduleName);
const moduleKey = isExternalModuleNameRelative(unquoted) ? "/" : unquoted;
const target = skipAlias(symbol, checker);
return `${importedName}|${createSymbolKey(target)}|${moduleKey}`;
}
function parseKey(key: string) {
const symbolName = key.substring(0, key.indexOf("|"));
const moduleKey = key.substring(key.lastIndexOf("|") + 1);
const ambientModuleName = moduleKey === "/" ? undefined : moduleKey;
return { symbolName, ambientModuleName };
}
function createSymbolKey(symbol: Symbol) {
let key = symbol.name;
while (symbol.parent) {
key += `,${symbol.parent.name}`;
symbol = symbol.parent;
}
return key;
}
function fileIsGlobalOnly(file: SourceFile) {
return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames;
}
function ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) {
if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) {
return false;
}
let oldFileStatementIndex = -1;
let newFileStatementIndex = -1;
for (const ambientModuleName of newSourceFile.ambientModuleNames) {
const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName;
oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1);
newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1);
if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) {
return false;
}
}
return true;
}
}
export function isImportableFile(
program: Program,
from: SourceFile,
to: SourceFile,
preferences: UserPreferences,
packageJsonFilter: PackageJsonImportFilter | undefined,
moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost,
moduleSpecifierCache: ModuleSpecifierCache | undefined,
): boolean {
if (from === to) return false;
const cachedResult = moduleSpecifierCache?.get(from.path, to.path, preferences);
if (cachedResult?.isAutoImportable !== undefined) {
return cachedResult.isAutoImportable;
}
const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost);
const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.();
const hasImportablePath = !!moduleSpecifiers.forEachFileNameOfModule(
from.fileName,
to.fileName,
moduleSpecifierResolutionHost,
/*preferSymlinks*/ false,
toPath => {
const toFile = program.getSourceFile(toPath);
// Determine to import using toPath only if toPath is what we were looking at
// or there doesnt exist the file in the program by the symlink
return (toFile === to || !toFile) &&
isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache);
}
);
if (packageJsonFilter) {
const isAutoImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost);
moduleSpecifierCache?.setIsAutoImportable(from.path, to.path, preferences, isAutoImportable);
return isAutoImportable;
}
return hasImportablePath;
}
/**
* Don't include something from a `node_modules` that isn't actually reachable by a global import.
* A relative import to node_modules is usually a bad idea.
*/
function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: GetCanonicalFileName, globalCachePath?: string): boolean {
// If it's in a `node_modules` but is not reachable from here via a global import, don't bother.
const toNodeModules = forEachAncestorDirectory(toPath, ancestor => getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined);
const toNodeModulesParent = toNodeModules && getDirectoryPath(getCanonicalFileName(toNodeModules));
return toNodeModulesParent === undefined
|| startsWith(getCanonicalFileName(fromPath), toNodeModulesParent)
|| (!!globalCachePath && startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent));
}
export function forEachExternalModuleToImportFrom(
program: Program,
host: LanguageServiceHost,
useAutoImportProvider: boolean,
cb: (module: Symbol, moduleFile: SourceFile | undefined, program: Program, isFromPackageJson: boolean) => void,
) {
forEachExternalModule(program.getTypeChecker(), program.getSourceFiles(), (module, file) => cb(module, file, program, /*isFromPackageJson*/ false));
const autoImportProvider = useAutoImportProvider && host.getPackageJsonAutoImportProvider?.();
if (autoImportProvider) {
const start = timestamp();
forEachExternalModule(autoImportProvider.getTypeChecker(), autoImportProvider.getSourceFiles(), (module, file) => cb(module, file, autoImportProvider, /*isFromPackageJson*/ true));
host.log?.(`forEachExternalModuleToImportFrom autoImportProvider: ${timestamp() - start}`);
}
}
function forEachExternalModule(checker: TypeChecker, allSourceFiles: readonly SourceFile[], cb: (module: Symbol, sourceFile: SourceFile | undefined) => void) {
for (const ambient of checker.getAmbientModules()) {
if (!stringContains(ambient.name, "*")) {
cb(ambient, /*sourceFile*/ undefined);
}
}
for (const sourceFile of allSourceFiles) {
if (isExternalOrCommonJsModule(sourceFile)) {
cb(checker.getMergedSymbol(sourceFile.symbol), sourceFile);
}
}
}
export function getExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program): ExportInfoMap {
const start = timestamp();
// Pulling the AutoImportProvider project will trigger its updateGraph if pending,
// which will invalidate the export map cache if things change, so pull it before
// checking the cache.
host.getPackageJsonAutoImportProvider?.();
const cache = host.getCachedExportInfoMap?.() || createCacheableExportInfoMap({
getCurrentProgram: () => program,
getPackageJsonAutoImportProvider: () => host.getPackageJsonAutoImportProvider?.(),
});
if (cache.isUsableByFile(importingFile.path)) {
host.log?.("getExportInfoMap: cache hit");
return cache;
}
host.log?.("getExportInfoMap: cache miss or empty; calculating new results");
const compilerOptions = program.getCompilerOptions();
const scriptTarget = getEmitScriptTarget(compilerOptions);
forEachExternalModuleToImportFrom(program, host, /*useAutoImportProvider*/ true, (moduleSymbol, moduleFile, program, isFromPackageJson) => {
const seenExports = new Map<Symbol, true>();
const checker = program.getTypeChecker();
const defaultInfo = getDefaultLikeExportInfo(moduleSymbol, checker, compilerOptions);
if (defaultInfo && !checker.isUndefinedSymbol(defaultInfo.symbol)) {
cache.add(
importingFile.path,
defaultInfo.symbol,
moduleSymbol,
moduleFile,
defaultInfo.exportKind,
isFromPackageJson,
scriptTarget,
checker);
}
for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) {
if (exported !== defaultInfo?.symbol && addToSeen(seenExports, exported)) {
cache.add(
importingFile.path,
exported,
moduleSymbol,
moduleFile,
ExportKind.Named,
isFromPackageJson,
scriptTarget,
checker);
}
}
});
host.log?.(`getExportInfoMap: done in ${timestamp() - start} ms`);
return cache;
}
export function getDefaultLikeExportInfo(moduleSymbol: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions) {
const exported = getDefaultLikeExportWorker(moduleSymbol, checker);
if (!exported) return undefined;
const { symbol, exportKind } = exported;
const info = getDefaultExportInfoWorker(symbol, checker, compilerOptions);
return info && { symbol, exportKind, ...info };
}
function getDefaultLikeExportWorker(moduleSymbol: Symbol, checker: TypeChecker): { readonly symbol: Symbol, readonly exportKind: ExportKind } | undefined {
const exportEquals = checker.resolveExternalModuleSymbol(moduleSymbol);
if (exportEquals !== moduleSymbol) return { symbol: exportEquals, exportKind: ExportKind.ExportEquals };
const defaultExport = checker.tryGetMemberInModuleExports(InternalSymbolName.Default, moduleSymbol);
if (defaultExport) return { symbol: defaultExport, exportKind: ExportKind.Default };
}
function getDefaultExportInfoWorker(defaultExport: Symbol, checker: TypeChecker, compilerOptions: CompilerOptions): { readonly symbolForMeaning: Symbol, readonly name: string } | undefined {
const localSymbol = getLocalSymbolForExportDefault(defaultExport);
if (localSymbol) return { symbolForMeaning: localSymbol, name: localSymbol.name };
const name = getNameForExportDefault(defaultExport);
if (name !== undefined) return { symbolForMeaning: defaultExport, name };
if (defaultExport.flags & SymbolFlags.Alias) {
const aliased = checker.getImmediateAliasedSymbol(defaultExport);
if (aliased && aliased.parent) {
// - `aliased` will be undefined if the module is exporting an unresolvable name,
// but we can still offer completions for it.
// - `aliased.parent` will be undefined if the module is exporting `globalThis.something`,
// or another expression that resolves to a global.
return getDefaultExportInfoWorker(aliased, checker, compilerOptions);
}
}
if (defaultExport.escapedName !== InternalSymbolName.Default &&
defaultExport.escapedName !== InternalSymbolName.ExportEquals) {
return { symbolForMeaning: defaultExport, name: defaultExport.getName() };
}
return { symbolForMeaning: defaultExport, name: getNameForExportedSymbol(defaultExport, compilerOptions.target) };
}
function getNameForExportDefault(symbol: Symbol): string | undefined {
return symbol.declarations && firstDefined(symbol.declarations, declaration => {
if (isExportAssignment(declaration)) {
return tryCast(skipOuterExpressions(declaration.expression), isIdentifier)?.text;
}
else if (isExportSpecifier(declaration)) {
Debug.assert(declaration.name.text === InternalSymbolName.Default, "Expected the specifier to be a default export");
return declaration.propertyName && declaration.propertyName.text;
}
});
}
}
+1
View File
@@ -11,6 +11,7 @@
"files": [
"types.ts",
"utilities.ts",
"exportInfoMap.ts",
"classifier.ts",
"classifier2020.ts",
"stringCompletions.ts",
+1 -1
View File
@@ -303,7 +303,7 @@ namespace ts {
/* @internal */ getPackageJsonsVisibleToFile?(fileName: string, rootDir?: string): readonly PackageJsonInfo[];
/* @internal */ getNearestAncestorDirectoryWithPackageJson?(fileName: string): string | undefined;
/* @internal */ getPackageJsonsForAutoImport?(rootDir?: string): readonly PackageJsonInfo[];
/* @internal */ getExportMapCache?(): ExportMapCache;
/* @internal */ getCachedExportInfoMap?(): ExportInfoMap;
/* @internal */ getModuleSpecifierCache?(): ModuleSpecifierCache;
/* @internal */ setCompilerHost?(host: CompilerHost): void;
/* @internal */ useSourceOfProjectReferenceRedirect?(): boolean;
-255
View File
@@ -3167,261 +3167,6 @@ namespace ts {
return isInJSFile(declaration) || !findAncestor(declaration, isGlobalScopeAugmentation);
}
export const enum ImportKind {
Named,
Default,
Namespace,
CommonJS,
}
export const enum ExportKind {
Named,
Default,
ExportEquals,
UMD,
}
/** Information about how a symbol is exported from a module. */
export interface SymbolExportInfo {
symbol: Symbol;
moduleSymbol: Symbol;
exportKind: ExportKind;
/** If true, can't use an es6 import from a js file. */
exportedSymbolIsTypeOnly: boolean;
/** True if export was only found via the package.json AutoImportProvider (for telemetry). */
isFromPackageJson: boolean;
}
export interface ExportMapCache {
clear(): void;
get(file: Path, checker: TypeChecker): MultiMap<string, SymbolExportInfo> | undefined;
getProjectVersion(): string | undefined;
set(suggestions: MultiMap<string, SymbolExportInfo>, projectVersion?: string): void;
isEmpty(): boolean;
/** @returns Whether the change resulted in the cache being cleared */
onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean): boolean;
}
export function createExportMapCache(): ExportMapCache {
let cache: MultiMap<string, SymbolExportInfo> | undefined;
let projectVersion: string | undefined;
let usableByFileName: Path | undefined;
const wrapped: ExportMapCache = {
isEmpty() {
return !cache;
},
clear() {
cache = undefined;
projectVersion = undefined;
},
set(suggestions, version) {
cache = suggestions;
if (version) {
projectVersion = version;
}
},
get: (file) => {
if (usableByFileName && file !== usableByFileName) {
return undefined;
}
return cache;
},
getProjectVersion: () => projectVersion,
onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) {
if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) {
// File is purely global; doesn't affect export map
return false;
}
if (
usableByFileName && usableByFileName !== newSourceFile.path ||
// If ATA is enabled, auto-imports uses existing imports to guess whether you want auto-imports from node.
// Adding or removing imports from node could change the outcome of that guess, so could change the suggestions list.
typeAcquisitionEnabled && consumesNodeCoreModules(oldSourceFile) !== consumesNodeCoreModules(newSourceFile) ||
// Module agumentation and ambient module changes can add or remove exports available to be auto-imported.
// Changes elsewhere in the file can change the *type* of an export in a module augmentation,
// but type info is gathered in getCompletionEntryDetails, which doesn’t use the cache.
!arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) ||
!ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile)
) {
this.clear();
return true;
}
usableByFileName = newSourceFile.path;
return false;
},
};
if (Debug.isDebugging) {
Object.defineProperty(wrapped, "__cache", { get: () => cache });
}
return wrapped;
function fileIsGlobalOnly(file: SourceFile) {
return !file.commonJsModuleIndicator && !file.externalModuleIndicator && !file.moduleAugmentations && !file.ambientModuleNames;
}
function ambientModuleDeclarationsAreEqual(oldSourceFile: SourceFile, newSourceFile: SourceFile) {
if (!arrayIsEqualTo(oldSourceFile.ambientModuleNames, newSourceFile.ambientModuleNames)) {
return false;
}
let oldFileStatementIndex = -1;
let newFileStatementIndex = -1;
for (const ambientModuleName of newSourceFile.ambientModuleNames) {
const isMatchingModuleDeclaration = (node: Statement) => isNonGlobalAmbientModule(node) && node.name.text === ambientModuleName;
oldFileStatementIndex = findIndex(oldSourceFile.statements, isMatchingModuleDeclaration, oldFileStatementIndex + 1);
newFileStatementIndex = findIndex(newSourceFile.statements, isMatchingModuleDeclaration, newFileStatementIndex + 1);
if (oldSourceFile.statements[oldFileStatementIndex] !== newSourceFile.statements[newFileStatementIndex]) {
return false;
}
}
return true;
}
}
export interface ModuleSpecifierResolutionCacheHost {
watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher;
}
export function createModuleSpecifierCache(host: ModuleSpecifierResolutionCacheHost): ModuleSpecifierCache {
let containedNodeModulesWatchers: ESMap<string, FileWatcher> | undefined;
let cache: ESMap<Path, ResolvedModuleSpecifierInfo> | undefined;
let currentKey: string | undefined;
const result: ModuleSpecifierCache = {
get(fromFileName, toFileName, preferences) {
if (!cache || currentKey !== key(fromFileName, preferences)) return undefined;
return cache.get(toFileName);
},
set(fromFileName, toFileName, preferences, modulePaths, moduleSpecifiers) {
ensureCache(fromFileName, preferences).set(toFileName, createInfo(modulePaths, moduleSpecifiers, /*isAutoImportable*/ true));
// If any module specifiers were generated based off paths in node_modules,
// a package.json file in that package was read and is an input to the cached.
// Instead of watching each individual package.json file, set up a wildcard
// directory watcher for any node_modules referenced and clear the cache when
// it sees any changes.
if (moduleSpecifiers) {
for (const p of modulePaths) {
if (p.isInNodeModules) {
// No trailing slash
const nodeModulesPath = p.path.substring(0, p.path.indexOf(nodeModulesPathPart) + nodeModulesPathPart.length - 1);
if (!containedNodeModulesWatchers?.has(nodeModulesPath)) {
(containedNodeModulesWatchers ||= new Map()).set(
nodeModulesPath,
host.watchNodeModulesForPackageJsonChanges(nodeModulesPath),
);
}
}
}
}
},
setModulePaths(fromFileName, toFileName, preferences, modulePaths) {
const cache = ensureCache(fromFileName, preferences);
const info = cache.get(toFileName);
if (info) {
info.modulePaths = modulePaths;
}
else {
cache.set(toFileName, createInfo(modulePaths, /*moduleSpecifiers*/ undefined, /*isAutoImportable*/ undefined));
}
},
setIsAutoImportable(fromFileName, toFileName, preferences, isAutoImportable) {
const cache = ensureCache(fromFileName, preferences);
const info = cache.get(toFileName);
if (info) {
info.isAutoImportable = isAutoImportable;
}
else {
cache.set(toFileName, createInfo(/*modulePaths*/ undefined, /*moduleSpecifiers*/ undefined, isAutoImportable));
}
},
clear() {
containedNodeModulesWatchers?.forEach(watcher => watcher.close());
cache?.clear();
containedNodeModulesWatchers?.clear();
currentKey = undefined;
},
count() {
return cache ? cache.size : 0;
}
};
if (Debug.isDebugging) {
Object.defineProperty(result, "__cache", { get: () => cache });
}
return result;
function ensureCache(fromFileName: Path, preferences: UserPreferences) {
const newKey = key(fromFileName, preferences);
if (cache && (currentKey !== newKey)) {
result.clear();
}
currentKey = newKey;
return cache ||= new Map();
}
function key(fromFileName: Path, preferences: UserPreferences) {
return `${fromFileName},${preferences.importModuleSpecifierEnding},${preferences.importModuleSpecifierPreference}`;
}
function createInfo(
modulePaths: readonly ModulePath[] | undefined,
moduleSpecifiers: readonly string[] | undefined,
isAutoImportable: boolean | undefined,
): ResolvedModuleSpecifierInfo {
return { modulePaths, moduleSpecifiers, isAutoImportable };
}
}
export function isImportableFile(
program: Program,
from: SourceFile,
to: SourceFile,
preferences: UserPreferences,
packageJsonFilter: PackageJsonImportFilter | undefined,
moduleSpecifierResolutionHost: ModuleSpecifierResolutionHost,
moduleSpecifierCache: ModuleSpecifierCache | undefined,
): boolean {
if (from === to) return false;
const cachedResult = moduleSpecifierCache?.get(from.path, to.path, preferences);
if (cachedResult?.isAutoImportable !== undefined) {
return cachedResult.isAutoImportable;
}
const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost);
const globalTypingsCache = moduleSpecifierResolutionHost.getGlobalTypingsCacheLocation?.();
const hasImportablePath = !!moduleSpecifiers.forEachFileNameOfModule(
from.fileName,
to.fileName,
moduleSpecifierResolutionHost,
/*preferSymlinks*/ false,
toPath => {
const toFile = program.getSourceFile(toPath);
// Determine to import using toPath only if toPath is what we were looking at
// or there doesnt exist the file in the program by the symlink
return (toFile === to || !toFile) &&
isImportablePath(from.fileName, toPath, getCanonicalFileName, globalTypingsCache);
}
);
if (packageJsonFilter) {
const isAutoImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost);
moduleSpecifierCache?.setIsAutoImportable(from.path, to.path, preferences, isAutoImportable);
return isAutoImportable;
}
return hasImportablePath;
}
/**
* Don't include something from a `node_modules` that isn't actually reachable by a global import.
* A relative import to node_modules is usually a bad idea.
*/
function isImportablePath(fromPath: string, toPath: string, getCanonicalFileName: GetCanonicalFileName, globalCachePath?: string): boolean {
// If it's in a `node_modules` but is not reachable from here via a global import, don't bother.
const toNodeModules = forEachAncestorDirectory(toPath, ancestor => getBaseFileName(ancestor) === "node_modules" ? ancestor : undefined);
const toNodeModulesParent = toNodeModules && getDirectoryPath(getCanonicalFileName(toNodeModules));
return toNodeModulesParent === undefined
|| startsWith(getCanonicalFileName(fromPath), toNodeModulesParent)
|| (!!globalCachePath && startsWith(getCanonicalFileName(globalCachePath), toNodeModulesParent));
}
export function isDeprecatedDeclaration(decl: Declaration) {
return !!(getCombinedNodeFlagsAlwaysIncludeJSDoc(decl) & ModifierFlags.Deprecated);
}
@@ -119,6 +119,29 @@ namespace ts.projectSystem {
completions.entries.find(entry => (entry.data as any)?.moduleSpecifier?.startsWith("dep-a"))!);
});
});
it("works for transient symbols between requests", () => {
const constantsDts: File = {
path: "/lib/foo/constants.d.ts",
content: `
type Signals = "SIGINT" | "SIGABRT";
declare const exp: {} & { [K in Signals]: K };
export = exp;`,
};
const exportingFiles = createExportingModuleFiles("/lib/a", Completions.moduleSpecifierResolutionLimit, 1, i => `S${i}`);
const { typeToTriggerCompletions, session } = setup([tsconfigFile, indexFile, ...exportingFiles, constantsDts]);
openFilesForSession([indexFile], session);
typeToTriggerCompletions(indexFile.path, "s", completions => {
const sigint = completions.entries.find(e => e.name === "SIGINT");
assert(sigint);
assert(!(sigint!.data as any).moduleSpecifier);
})
.continueTyping("i", completions => {
const sigint = completions.entries.find(e => e.name === "SIGINT");
assert((sigint!.data as any).moduleSpecifier);
});
});
});
function setup(files: File[]) {
@@ -140,11 +163,11 @@ namespace ts.projectSystem {
return { host, session, projectService, typeToTriggerCompletions, assertCompletionDetailsOk };
function typeToTriggerCompletions(fileName: string, typedCharacters: string, cb: (completions: protocol.CompletionInfo) => void) {
function typeToTriggerCompletions(fileName: string, typedCharacters: string, cb?: (completions: protocol.CompletionInfo) => void) {
const project = projectService.getDefaultProjectForFile(server.toNormalizedPath(fileName), /*ensureProject*/ true)!;
return type(typedCharacters, cb, /*isIncompleteContinuation*/ false);
function type(typedCharacters: string, cb: (completions: protocol.CompletionInfo) => void, isIncompleteContinuation: boolean) {
function type(typedCharacters: string, cb: ((completions: protocol.CompletionInfo) => void) | undefined, isIncompleteContinuation: boolean) {
const file = Debug.checkDefined(project.getLanguageService(/*ensureSynchronized*/ true).getProgram()?.getSourceFile(fileName));
const { line, character } = getLineAndCharacterOfPosition(file, file.text.length);
const oneBasedEditPosition = { line: line + 1, offset: character + 1 };
@@ -174,7 +197,7 @@ namespace ts.projectSystem {
}
}).response as protocol.CompletionInfo;
cb(Debug.checkDefined(response));
cb?.(Debug.checkDefined(response));
return {
backspace,
continueTyping: (typedCharacters: string, cb: (completions: protocol.CompletionInfo) => void) => {
@@ -23,54 +23,109 @@ namespace ts.projectSystem {
path: "/node_modules/mobx/index.d.ts",
content: "export declare function observable(): unknown;"
};
const exportEqualsMappedType: File = {
path: "/lib/foo/constants.d.ts",
content: `
type Signals = "SIGINT" | "SIGABRT";
declare const exp: {} & { [K in Signals]: K };
export = exp;`,
};
describe("unittests:: tsserver:: exportMapCache", () => {
it("caches auto-imports in the same file", () => {
const { exportMapCache, checker } = setup();
assert.ok(exportMapCache.get(bTs.path as Path, checker));
const { exportMapCache } = setup();
assert.ok(exportMapCache.isUsableByFile(bTs.path as Path));
assert.ok(!exportMapCache.isEmpty());
});
it("invalidates the cache when new files are added", () => {
const { host, exportMapCache, checker } = setup();
const { host, exportMapCache } = setup();
host.writeFile("/src/a2.ts", aTs.content);
host.runQueuedTimeoutCallbacks();
assert.isUndefined(exportMapCache.get(bTs.path as Path, checker));
assert.ok(!exportMapCache.isUsableByFile(bTs.path as Path));
assert.ok(exportMapCache.isEmpty());
});
it("invalidates the cache when files are deleted", () => {
const { host, projectService, exportMapCache, checker } = setup();
const { host, projectService, exportMapCache } = setup();
projectService.closeClientFile(aTs.path);
host.deleteFile(aTs.path);
host.runQueuedTimeoutCallbacks();
assert.isUndefined(exportMapCache.get(bTs.path as Path, checker));
assert.ok(!exportMapCache.isUsableByFile(bTs.path as Path));
assert.ok(exportMapCache.isEmpty());
});
it("does not invalidate the cache when package.json is changed inconsequentially", () => {
const { host, exportMapCache, checker, project } = setup();
const { host, exportMapCache, project } = setup();
host.writeFile("/package.json", `{ "name": "blah", "dependencies": { "mobx": "*" } }`);
host.runQueuedTimeoutCallbacks();
project.getPackageJsonAutoImportProvider();
assert.ok(exportMapCache.get(bTs.path as Path, checker));
assert.ok(exportMapCache.isUsableByFile(bTs.path as Path));
assert.ok(!exportMapCache.isEmpty());
});
it("invalidates the cache when package.json change results in AutoImportProvider change", () => {
const { host, exportMapCache, checker, project } = setup();
const { host, exportMapCache, project } = setup();
host.writeFile("/package.json", `{}`);
host.runQueuedTimeoutCallbacks();
project.getPackageJsonAutoImportProvider();
assert.isUndefined(exportMapCache.get(bTs.path as Path, checker));
assert.ok(!exportMapCache.isUsableByFile(bTs.path as Path));
assert.ok(exportMapCache.isEmpty());
});
it("does not store transient symbols through program updates", () => {
const { exportMapCache, project, session } = setup();
// SIGINT, exported from /lib/foo/constants.d.ts, is a mapped type property, which will be a transient symbol.
// Transient symbols contain types, which retain the checkers they came from, so are not safe to cache.
// We clear symbols from the cache during updateGraph, leaving only the information about how to re-get them
// (see getters on `CachedSymbolExportInfo`). We can roughly test that this is working by ensuring that
// accessing a transient symbol with two different checkers results in different symbol identities, since
// transient symbols are recreated with every new checker.
const programBefore = project.getCurrentProgram()!;
let sigintPropBefore: readonly SymbolExportInfo[] | undefined;
exportMapCache.forEach(bTs.path as Path, (info, name) => {
if (name === "SIGINT") sigintPropBefore = info;
});
assert.ok(sigintPropBefore);
assert.ok(sigintPropBefore![0].symbol.flags & SymbolFlags.Transient);
const symbolIdBefore = getSymbolId(sigintPropBefore![0].symbol);
// Update program without clearing cache
session.executeCommandSeq<protocol.UpdateOpenRequest>({
command: protocol.CommandTypes.UpdateOpen,
arguments: {
changedFiles: [{
fileName: bTs.path,
textChanges: [{
newText: " ",
start: { line: 1, offset: 1 },
end: { line: 1, offset: 1 },
}]
}]
}
});
project.getLanguageService(/*ensureSynchronized*/ true);
assert.notEqual(programBefore, project.getCurrentProgram()!);
// Get same info from cache again
let sigintPropAfter: readonly SymbolExportInfo[] | undefined;
exportMapCache.forEach(bTs.path as Path, (info, name) => {
if (name === "SIGINT") sigintPropAfter = info;
});
assert.ok(sigintPropAfter);
assert.notEqual(symbolIdBefore, getSymbolId(sigintPropAfter![0].symbol));
});
});
function setup() {
const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts]);
const host = createServerHost([aTs, bTs, ambientDeclaration, tsconfig, packageJson, mobxDts, exportEqualsMappedType]);
const session = createSession(host);
openFilesForSession([aTs, bTs], session);
const projectService = session.getProjectService();
const project = configuredProjectAt(projectService, 0);
triggerCompletions();
const checker = project.getLanguageService().getProgram()!.getTypeChecker();
return { host, project, projectService, exportMapCache: project.getExportMapCache(), checker, triggerCompletions };
return { host, project, projectService, session, exportMapCache: project.getCachedExportInfoMap(), checker, triggerCompletions };
function triggerCompletions() {
const requestLocation: protocol.FileLocationRequestArgs = {