Include actual generated module specifiers in module specifier cache (#44176)

* Add cache invalidation for node_modules, config, and preferences changes

* Share watches with closed script info

* Update API baseline

* Small adjustments for fewer object allocations

* Document overloaded return value

* Update baselines

* Store isAutoImportable separately from modulePaths

* Add back missing return

* Return wrapped watcher instead of underlying one

* Make user preferences part of the cache key instead of implicitly clearing in editor services

* Fix missed merge conflict
This commit is contained in:
Andrew Branch
2021-06-10 12:26:32 -05:00
committed by GitHub
parent 130e16d73b
commit 7c293c8d46
18 changed files with 348 additions and 130 deletions
+16 -15
View File
@@ -62,7 +62,7 @@ namespace ts.codefix {
const symbolName = getNameForExportedSymbol(exportedSymbol, getEmitScriptTarget(compilerOptions));
const checker = program.getTypeChecker();
const symbol = checker.getMergedSymbol(skipAlias(exportedSymbol, checker));
const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, host, program, useAutoImportProvider);
const exportInfos = getAllReExportingModules(sourceFile, symbol, moduleSymbol, symbolName, host, program, preferences, useAutoImportProvider);
const preferTypeOnlyImport = !!usageIsTypeOnly && compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error;
const useRequire = shouldUseRequire(sourceFile, program);
const fix = getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, /*position*/ undefined, preferTypeOnlyImport, useRequire, host, preferences);
@@ -202,7 +202,7 @@ namespace ts.codefix {
const compilerOptions = program.getCompilerOptions();
const exportInfos = pathIsBareSpecifier(stripQuotes(moduleSymbol.name))
? [getSymbolExportInfoForSymbol(exportedSymbol, moduleSymbol, program, host)]
: getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, /*useAutoImportProvider*/ true);
: getAllReExportingModules(sourceFile, exportedSymbol, moduleSymbol, symbolName, host, program, preferences, /*useAutoImportProvider*/ true);
const useRequire = shouldUseRequire(sourceFile, program);
const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && !isSourceFileJS(sourceFile) && isValidTypeOnlyAliasUseSite(getTokenAtPosition(sourceFile, position));
const fix = Debug.checkDefined(getImportFixForSymbol(sourceFile, exportInfos, moduleSymbol, symbolName, program, position, preferTypeOnlyImport, useRequire, host, preferences));
@@ -211,7 +211,7 @@ namespace ts.codefix {
function getImportFixForSymbol(sourceFile: SourceFile, exportInfos: readonly SymbolExportInfo[], moduleSymbol: Symbol, symbolName: string, program: Program, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, host: LanguageServiceHost, preferences: UserPreferences) {
Debug.assert(exportInfos.some(info => info.moduleSymbol === moduleSymbol), "Some exportInfo should match the specified moduleSymbol");
return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, host);
return getBestFix(getImportFixes(exportInfos, symbolName, position, preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences), sourceFile, host, preferences);
}
function codeFixActionToCodeAction({ description, changes, commands }: CodeFixAction): CodeAction {
@@ -239,7 +239,7 @@ namespace ts.codefix {
}
}
function getAllReExportingModules(importingFile: SourceFile, exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
function getAllReExportingModules(importingFile: SourceFile, exportedSymbol: Symbol, exportingModuleSymbol: Symbol, symbolName: string, host: LanguageServiceHost, program: Program, preferences: UserPreferences, useAutoImportProvider: boolean): readonly SymbolExportInfo[] {
const result: SymbolExportInfo[] = [];
const compilerOptions = program.getCompilerOptions();
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
@@ -267,7 +267,7 @@ namespace ts.codefix {
return result;
function isImportable(program: Program, moduleFile: SourceFile | undefined, isFromPackageJson: boolean) {
return !moduleFile || isImportableFile(program, importingFile, moduleFile, /*packageJsonFilter*/ undefined, getModuleSpecifierResolutionHost(isFromPackageJson), host.getModuleSpecifierCache?.());
return !moduleFile || isImportableFile(program, importingFile, moduleFile, preferences, /*packageJsonFilter*/ undefined, getModuleSpecifierResolutionHost(isFromPackageJson), host.getModuleSpecifierCache?.());
}
}
@@ -277,7 +277,7 @@ namespace ts.codefix {
host: LanguageServiceHost,
preferences: UserPreferences
): { exportInfo?: SymbolExportInfo, moduleSpecifier: string } | undefined {
return getBestFix(getNewImportFixes(program, importingFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, exportInfo, host, preferences), importingFile, host);
return getBestFix(getNewImportFixes(program, importingFile, /*position*/ undefined, /*preferTypeOnlyImport*/ false, /*useRequire*/ false, exportInfo, host, preferences), importingFile, host, preferences);
}
export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program) {
@@ -523,21 +523,21 @@ namespace ts.codefix {
const info = errorCode === Diagnostics._0_refers_to_a_UMD_global_but_the_current_file_is_a_module_Consider_adding_an_import_instead.code
? getFixesInfoForUMDImport(context, symbolToken)
: isIdentifier(symbolToken) ? getFixesInfoForNonUMDImport(context, symbolToken, useAutoImportProvider) : undefined;
return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.host) };
return info && { ...info, fixes: sortFixes(info.fixes, context.sourceFile, context.host, context.preferences) };
}
function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, host: LanguageServiceHost): readonly ImportFix[] {
const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, host);
function sortFixes(fixes: readonly ImportFix[], sourceFile: SourceFile, host: LanguageServiceHost, preferences: UserPreferences): readonly ImportFix[] {
const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, preferences, host);
return sort(fixes, (a, b) => compareValues(a.kind, b.kind) || compareModuleSpecifiers(a, b, allowsImportingSpecifier));
}
function getBestFix<T extends ImportFix>(fixes: readonly T[], sourceFile: SourceFile, host: LanguageServiceHost): T | undefined {
function getBestFix<T extends ImportFix>(fixes: readonly T[], sourceFile: SourceFile, host: LanguageServiceHost, preferences: UserPreferences): T | undefined {
if (!some(fixes)) return;
// These will always be placed first if available, and are better than other kinds
if (fixes[0].kind === ImportFixKind.UseNamespace || fixes[0].kind === ImportFixKind.AddToExisting) {
return fixes[0];
}
const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, host);
const { allowsImportingSpecifier } = createPackageJsonImportFilter(sourceFile, preferences, host);
return fixes.reduce((best, fix) =>
compareModuleSpecifiers(fix, best, allowsImportingSpecifier) === Comparison.LessThan ? fix : best
);
@@ -621,7 +621,7 @@ namespace ts.codefix {
const preferTypeOnlyImport = compilerOptions.importsNotUsedAsValues === ImportsNotUsedAsValues.Error && isValidTypeOnlyAliasUseSite(symbolToken);
const useRequire = shouldUseRequire(sourceFile, program);
const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host);
const exportInfos = getExportInfos(symbolName, getMeaningFromLocation(symbolToken), cancellationToken, sourceFile, program, useAutoImportProvider, host, preferences);
const fixes = arrayFrom(flatMapIterator(exportInfos.entries(), ([_, exportInfos]) =>
getImportFixes(exportInfos, symbolName, symbolToken.getStart(sourceFile), preferTypeOnlyImport, useRequire, program, sourceFile, host, preferences)));
return { fixes, symbolName };
@@ -646,19 +646,20 @@ namespace ts.codefix {
fromFile: SourceFile,
program: Program,
useAutoImportProvider: boolean,
host: LanguageServiceHost
host: LanguageServiceHost,
preferences: UserPreferences,
): ReadonlyESMap<string, readonly SymbolExportInfo[]> {
// For each original symbol, keep all re-exports of that symbol together so we can call `getCodeActionsForImport` on the whole group at once.
// Maps symbol id to info for modules providing that symbol (original export + re-exports).
const originalSymbolToExportInfos = createMultiMap<SymbolExportInfo>();
const packageJsonFilter = createPackageJsonImportFilter(fromFile, host);
const packageJsonFilter = createPackageJsonImportFilter(fromFile, preferences, host);
const moduleSpecifierCache = host.getModuleSpecifierCache?.();
const getModuleSpecifierResolutionHost = memoizeOne((isFromPackageJson: boolean) => {
return createModuleSpecifierResolutionHost(isFromPackageJson ? host.getPackageJsonAutoImportProvider!()! : program, host);
});
function addSymbol(moduleSymbol: Symbol, toFile: SourceFile | undefined, exportedSymbol: Symbol, exportKind: ExportKind, program: Program, isFromPackageJson: boolean): void {
const moduleSpecifierResolutionHost = getModuleSpecifierResolutionHost(isFromPackageJson);
if (toFile && isImportableFile(program, fromFile, toFile, packageJsonFilter, moduleSpecifierResolutionHost, moduleSpecifierCache) ||
if (toFile && isImportableFile(program, fromFile, toFile, preferences, packageJsonFilter, moduleSpecifierResolutionHost, moduleSpecifierCache) ||
!toFile && packageJsonFilter.allowsImportingAmbientModule(moduleSymbol, moduleSpecifierResolutionHost)
) {
const checker = program.getTypeChecker();
+2 -1
View File
@@ -1755,7 +1755,7 @@ namespace ts.Completions {
const lowerCaseTokenText = previousToken && isIdentifier(previousToken) ? previousToken.text.toLowerCase() : "";
const exportInfo = codefix.getSymbolToExportInfoMap(sourceFile, host, program);
const packageJsonAutoImportProvider = host.getPackageJsonAutoImportProvider?.();
const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, host);
const packageJsonFilter = detailsEntryId ? undefined : createPackageJsonImportFilter(sourceFile, preferences, host);
exportInfo.forEach((info, key) => {
const symbolName = key.substring(0, key.indexOf("|"));
if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return;
@@ -1804,6 +1804,7 @@ namespace ts.Completions {
info.isFromPackageJson ? packageJsonAutoImportProvider! : program,
sourceFile,
moduleFile,
preferences,
packageJsonFilter,
getModuleSpecifierResolutionHost(info.isFromPackageJson),
moduleSpecifierCache);
+88 -22
View File
@@ -2894,7 +2894,7 @@ namespace ts {
allowsImportingSpecifier: (moduleSpecifier: string) => boolean;
}
export function createPackageJsonImportFilter(fromFile: SourceFile, host: LanguageServiceHost): PackageJsonImportFilter {
export function createPackageJsonImportFilter(fromFile: SourceFile, preferences: UserPreferences, host: LanguageServiceHost): PackageJsonImportFilter {
const packageJsons = (
(host.getPackageJsonsVisibleToFile && host.getPackageJsonsVisibleToFile(fromFile.fileName)) || getPackageJsonsVisibleToFile(fromFile.fileName, host)
).filter(p => p.parseable);
@@ -2980,6 +2980,7 @@ namespace ts {
fromFile.path,
importedFileName,
moduleSpecifierResolutionHost,
preferences,
);
if (!specifier) {
@@ -3275,47 +3276,112 @@ namespace ts {
}
}
export function createModuleSpecifierCache(): ModuleSpecifierCache {
let cache: ESMap<Path, boolean | readonly ModulePath[]> | undefined;
let importingFileName: Path | undefined;
const wrapped: ModuleSpecifierCache = {
get(fromFileName, toFileName) {
if (!cache || fromFileName !== importingFileName) return undefined;
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, moduleSpecifiers) {
if (cache && fromFileName !== importingFileName) {
cache.clear();
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));
}
importingFileName = fromFileName;
(cache ||= new Map()).set(toFileName, moduleSpecifiers);
},
clear() {
cache = undefined;
importingFileName = undefined;
containedNodeModulesWatchers?.forEach(watcher => watcher.close());
cache?.clear();
containedNodeModulesWatchers?.clear();
currentKey = undefined;
},
count() {
return cache ? cache.size : 0;
}
};
if (Debug.isDebugging) {
Object.defineProperty(wrapped, "__cache", { get: () => cache });
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 };
}
return wrapped;
}
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);
if (cachedResult !== undefined) {
return !!cachedResult;
const cachedResult = moduleSpecifierCache?.get(from.path, to.path, preferences);
if (cachedResult?.isAutoImportable !== undefined) {
return cachedResult.isAutoImportable;
}
const getCanonicalFileName = hostGetCanonicalFileName(moduleSpecifierResolutionHost);
@@ -3335,9 +3401,9 @@ namespace ts {
);
if (packageJsonFilter) {
const isImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost);
moduleSpecifierCache?.set(from.path, to.path, isImportable);
return isImportable;
const isAutoImportable = hasImportablePath && packageJsonFilter.allowsImportingSourceFile(to, moduleSpecifierResolutionHost);
moduleSpecifierCache?.setIsAutoImportable(from.path, to.path, preferences, isAutoImportable);
return isAutoImportable;
}
return hasImportablePath;