mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user