mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
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:
1 parent
fc5c765f81
commit
1da18c60d6
11 files changed
+641
-491
No files matched your search
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"typingsCache.ts",
|
||||
"project.ts",
|
||||
"editorServices.ts",
|
||||
"moduleSpecifierCache.ts",
|
||||
"packageJsonCache.ts",
|
||||
"session.ts",
|
||||
"scriptVersionCache.ts"
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
"files": [
|
||||
"types.ts",
|
||||
"utilities.ts",
|
||||
"exportInfoMap.ts",
|
||||
"classifier.ts",
|
||||
"classifier2020.ts",
|
||||
"stringCompletions.ts",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in new issue
Block a user