From 2795bad2571acd8f116c9d7f5a623a2fa680f0b9 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Thu, 24 Jun 2021 17:59:13 -0500 Subject: [PATCH 01/10] Add some failing tests around transient symbols --- .../tsserver/completionsIncomplete.ts | 29 +++++++++++++++++-- .../unittests/tsserver/exportMapCache.ts | 19 +++++++++++- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/src/testRunner/unittests/tsserver/completionsIncomplete.ts b/src/testRunner/unittests/tsserver/completionsIncomplete.ts index 5ac53ccda78f7..ae9c53cbc9289 100644 --- a/src/testRunner/unittests/tsserver/completionsIncomplete.ts +++ b/src/testRunner/unittests/tsserver/completionsIncomplete.ts @@ -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) => { diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 803d8b9d62234..5889016f0a3bb 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -23,6 +23,13 @@ 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", () => { @@ -60,10 +67,20 @@ namespace ts.projectSystem { project.getPackageJsonAutoImportProvider(); assert.isUndefined(exportMapCache.get(bTs.path as Path, checker)); }); + + it("does not contain transient symbols", () => { + const { exportMapCache, checker } = setup(); + exportMapCache.get(bTs.path as Path, checker)?.forEach((info, key) => { + if (key.startsWith("SIGINT")) { + assert(!hasProperty(info[0].symbol, "type")); + assert(!hasProperty(info[0].symbol, "nameType")); + } + }); + }); }); 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(); From 9fd152724c977b6d8ec7d608b7b5456d11d9ea0c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 25 Jun 2021 14:25:46 -0500 Subject: [PATCH 02/10] Working, but slower --- src/server/project.ts | 16 +- src/services/codefixes/importFixes.ts | 145 ++++++----------- src/services/completions.ts | 21 ++- src/services/utilities.ts | 151 +++++++++++++++--- .../unittests/tsserver/exportMapCache.ts | 72 +++++++-- 5 files changed, 244 insertions(+), 161 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index 3e138dc12a0b2..030e126939f4d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -251,7 +251,7 @@ namespace ts.server { public readonly getCanonicalFileName: GetCanonicalFileName; /*@internal*/ - private exportMapCache = createExportMapCache(); + exportMapCache: ExportMapCache | undefined; /*@internal*/ private changedFilesForExportMapCache: Set | 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); }); } } @@ -1667,7 +1669,7 @@ namespace ts.server { /*@internal*/ getExportMapCache() { - return this.exportMapCache; + return this.exportMapCache ||= createExportMapCache(this); } /*@internal*/ @@ -2017,7 +2019,7 @@ namespace ts.server { const oldProgram = this.getCurrentProgram(); const hasSameSetOfFiles = super.updateGraph(); if (oldProgram && oldProgram !== this.getCurrentProgram()) { - this.hostProject.getExportMapCache().clear(); + this.hostProject.exportMapCache?.clear(); } return hasSameSetOfFiles; } diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 9bdb6ea430c5a..7a8db48444b82 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -153,18 +153,18 @@ namespace ts.codefix { // Sorted with the preferred fix coming first. const enum ImportFixKind { UseNamespace, ImportType, AddToExisting, AddNew } - type ImportFix = FixUseNamespaceImport | FixUseImportType | FixAddToExistingImport | FixAddNewImport; + type ImportFix = FixUseNamespaceImport | FixUseImportType | FixAddToExistingImport | FixAddNewImport; interface FixUseNamespaceImport { readonly kind: ImportFixKind.UseNamespace; readonly namespacePrefix: string; readonly position: number; readonly moduleSpecifier: string; } - interface FixUseImportType { + interface FixUseImportType { readonly kind: ImportFixKind.ImportType; readonly moduleSpecifier: string; readonly position: number; - readonly exportInfo: SymbolExportInfo; + readonly exportInfo: T; } interface FixAddToExistingImport { readonly kind: ImportFixKind.AddToExisting; @@ -173,13 +173,13 @@ namespace ts.codefix { readonly importKind: ImportKind.Default | ImportKind.Named; readonly canUseTypeOnlyImport: boolean; } - interface FixAddNewImport { + interface FixAddNewImport { readonly kind: ImportFixKind.AddNew; readonly moduleSpecifier: string; readonly importKind: ImportKind; readonly typeOnly: boolean; readonly useRequire: boolean; - readonly exportInfo?: SymbolExportInfo; + readonly exportInfo?: T; } /** Information needed to augment an existing import declaration. */ @@ -230,11 +230,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, 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, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } } } @@ -255,12 +255,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, 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, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); @@ -271,13 +271,13 @@ namespace ts.codefix { } } - export function getModuleSpecifierForBestExportInfo(exportInfo: readonly SymbolExportInfo[], + export function getModuleSpecifierForBestExportInfo(exportInfo: readonly T[], importingFile: SourceFile, program: Program, host: LanguageServiceHost, preferences: UserPreferences, fromCacheOnly?: boolean, - ): { exportInfo?: SymbolExportInfo, moduleSpecifier: string, computedWithoutCacheCount: number } | undefined { + ): { exportInfo?: T, moduleSpecifier: string, computedWithoutCacheCount: number } | undefined { const { fixes, computedWithoutCacheCount } = getNewImportFixes( program, importingFile, @@ -292,112 +292,57 @@ 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 { + export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program): ExportMapCache { 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 cache = host.getExportMapCache?.() || createExportMapCache({ + getCurrentProgram: () => program, + getPackageJsonAutoImportProvider: () => host.getPackageJsonAutoImportProvider?.(), + }); + + if (cache.isUsableByFile(importingFile.path)) { + host.log?.("getSymbolToExportInfoMap: cache hit"); + return cache; } - const result: MultiMap = createMultiMap(); + host.log?.("getSymbolToExportInfoMap: cache miss or empty; calculating new results"); const compilerOptions = program.getCompilerOptions(); - const target = getEmitScriptTarget(compilerOptions); + const scriptTarget = 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, + cache.add( + importingFile.path, + defaultInfo.symbol, moduleSymbol, - exportKind: defaultInfo.exportKind, - exportedSymbolIsTypeOnly: isTypeOnlySymbol(defaultInfo.symbol, checker), + defaultInfo.exportKind, + isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson, - }); + scriptTarget, + checker); } const seenExports = new Map(); 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, + cache.add( + importingFile.path, + exported, moduleSymbol, - exportKind: ExportKind.Named, - exportedSymbolIsTypeOnly: isTypeOnlySymbol(exported, checker), + ExportKind.Named, + isTypeOnlySymbol(exported, checker), isFromPackageJson, - }); + scriptTarget, + checker); } } }); - 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, 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; - } - } + return cache; } function isTypeOnlySymbol(s: Symbol, checker: TypeChecker): boolean { @@ -501,7 +446,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); @@ -543,17 +488,17 @@ namespace ts.codefix { return true; } - function getNewImportFixes( + function getNewImportFixes( program: Program, sourceFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, - moduleSymbols: readonly SymbolExportInfo[], + moduleSymbols: readonly T[], host: LanguageServiceHost, preferences: UserPreferences, fromCacheOnly?: boolean, - ): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixUseImportType)[] } { + ): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixUseImportType)[] } { const isJs = isSourceFileJS(sourceFile); const compilerOptions = program.getCompilerOptions(); const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); @@ -566,9 +511,9 @@ namespace ts.codefix { const fixes = flatMap(moduleSymbols, exportInfo => { const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo.moduleSymbol); computedWithoutCacheCount += Number(computedWithoutCache); - return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixUseImportType => + 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, @@ -645,7 +590,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, 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 }; @@ -751,7 +696,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, exportKind, isTypeOnly: isTypeOnlySymbol(exportedSymbol, checker), isFromPackageJson }); } } forEachExternalModuleToImportFrom(program, host, useAutoImportProvider, (moduleSymbol, sourceFile, program, isFromPackageJson) => { diff --git a/src/services/completions.ts b/src/services/completions.ts index 13d8be8e09e4e..7ac62a7910a5f 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -148,12 +148,12 @@ namespace ts.Completions { const enum GlobalsSearch { Continue, Success, Fail } interface ModuleSpecifierResolutioContext { - tryResolve: (exportInfo: readonly SymbolExportInfo[], isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult | undefined; + tryResolve: (exportInfo: readonly CachedSymbolExportInfo[], isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult | undefined; resolutionLimitExceeded: () => boolean; } interface ModuleSpecifierResolutionResult { - exportInfo?: SymbolExportInfo; + exportInfo?: CachedSymbolExportInfo; moduleSpecifier: string; } @@ -181,7 +181,7 @@ namespace ts.Completions { host.log?.(`${logPrefix}: ${timestamp() - start}`); return result; - function tryResolve(exportInfo: readonly SymbolExportInfo[], isFromAmbientModule: boolean): ModuleSpecifierResolutionResult | undefined { + function tryResolve(exportInfo: readonly CachedSymbolExportInfo[], isFromAmbientModule: boolean): ModuleSpecifierResolutionResult | undefined { if (isFromAmbientModule) { const result = codefix.getModuleSpecifierForBestExportInfo(exportInfo, sourceFile, program, host, preferences); if (result) { @@ -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))); @@ -1916,7 +1917,6 @@ namespace ts.Completions { const exportInfo = codefix.getSymbolToExportInfoMap(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,9 +1925,9 @@ 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)); + const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === i.moduleName); if (isCompletionDetailsMatch || charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) { if (isFromAmbientModule && !some(info, isImportableExportInfo)) { return; @@ -1938,7 +1938,6 @@ namespace ts.Completions { const { exportInfo = find(info, isImportableExportInfo), moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {}; if (!exportInfo) return; - const moduleFile = tryCast(exportInfo.moduleSymbol.valueDeclaration, isSourceFile); const isDefaultExport = exportInfo.exportKind === ExportKind.Default; const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; pushAutoImportSymbol(symbol, { @@ -1946,7 +1945,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, @@ -1958,7 +1957,7 @@ namespace ts.Completions { } ); - function isImportableExportInfo(info: SymbolExportInfo) { + function isImportableExportInfo(info: CachedSymbolExportInfo) { const moduleFile = tryCast(info.moduleSymbol.valueDeclaration, isSourceFile); if (!moduleFile) { return packageJsonFilter diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 9ce4260c115f9..bc85470c59e5c 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3181,51 +3181,127 @@ namespace ts { UMD, } - /** Information about how a symbol is exported from a module. */ export interface SymbolExportInfo { - symbol: Symbol; - moduleSymbol: Symbol; + readonly symbol: Symbol; + readonly moduleSymbol: Symbol; exportKind: ExportKind; /** If true, can't use an es6 import from a js file. */ - exportedSymbolIsTypeOnly: boolean; + isTypeOnly: boolean; /** True if export was only found via the package.json AutoImportProvider (for telemetry). */ isFromPackageJson: boolean; } + export interface CachedSymbolExportInfo extends SymbolExportInfo { + id: number; + symbolName: string; + moduleName: string; + moduleFileName: string | undefined; + } + export interface ExportMapCache { + isUsableByFile(importingFile: Path): boolean; clear(): void; - get(file: Path, checker: TypeChecker): MultiMap | undefined; - getProjectVersion(): string | undefined; - set(suggestions: MultiMap, projectVersion?: string): void; + add(importingFile: Path, symbol: Symbol, moduleSymbol: Symbol, exportKind: ExportKind, isTypeOnly: boolean, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void; + get(importingFile: Path, importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker): readonly CachedSymbolExportInfo[] | undefined; + forEach(importingFile: Path, action: (info: readonly CachedSymbolExportInfo[], 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 function createExportMapCache(): ExportMapCache { - let cache: MultiMap | undefined; - let projectVersion: string | undefined; + + export interface ExportMapCacheHost { + getCurrentProgram(): Program | undefined; + getPackageJsonAutoImportProvider(): Program | undefined; + } + + export function createExportMapCache(host: ExportMapCacheHost): ExportMapCache { + let exportInfoId = 0; + const exportInfo = createMultiMap(); + const symbols = new Map(); let usableByFileName: Path | undefined; const wrapped: ExportMapCache = { - isEmpty() { - return !cache; - }, + isUsableByFile: importingFile => importingFile === usableByFileName, + isEmpty: () => !exportInfo.size, clear() { - cache = undefined; - projectVersion = undefined; + exportInfo.clear(); + symbols.clear(); + usableByFileName = undefined; }, - set(suggestions, version) { - cache = suggestions; - if (version) { - projectVersion = version; + add(importingFile, symbol, moduleSymbol, exportKind, isTypeOnly, isFromPackageJson, scriptTarget, checker) { + if (importingFile !== usableByFileName) { + this.clear(); + usableByFileName = importingFile; } + const isDefault = exportKind === ExportKind.Default; + const importedName = getNameForExportedSymbol(isDefault && getLocalSymbolForExportDefault(symbol) || symbol, scriptTarget); + const moduleName = stripQuotes(moduleSymbol.name); + const moduleFileName = isExternalModuleNameRelative(moduleName) ? Debug.checkDefined(getSourceFileOfModule(moduleSymbol)).fileName : undefined; + const id = exportInfoId++; + const k = key(importedName, symbol, moduleName, checker); + symbols.set(id, { symbol, moduleSymbol }); + // Reminder not to access these in getters; + // they should be stored only in `symbols` so they can be + // released on program updates. + symbol = undefined!; + moduleSymbol = undefined!; + + const info: CachedSymbolExportInfo = { + id, + symbolName: importedName, + moduleName, + moduleFileName, + exportKind, + isTypeOnly, + isFromPackageJson, + get moduleSymbol() { + const fromCache = symbols.get(this.id)?.moduleSymbol; + if (fromCache) return fromCache; + const containingProgram = this.isFromPackageJson + ? host.getPackageJsonAutoImportProvider()! + : host.getCurrentProgram()!; + const checker = containingProgram.getTypeChecker(); + const moduleSymbol = Debug.checkDefined(this.moduleFileName + ? checker.getMergedSymbol(containingProgram.getSourceFile(this.moduleFileName)!.symbol) + : checker.tryFindAmbientModule(this.moduleName)); + symbols.set(this.id, { moduleSymbol, symbol: undefined }); + return moduleSymbol; + }, + get symbol() { + const fromCache = symbols.get(this.id)?.symbol; + if (fromCache) return fromCache; + const moduleSymbol = this.moduleSymbol; + const containingProgram = this.isFromPackageJson + ? host.getPackageJsonAutoImportProvider()! + : host.getCurrentProgram()!; + const checker = containingProgram.getTypeChecker(); + const symbolName = this.exportKind === ExportKind.Default + ? InternalSymbolName.Default + : this.symbolName; + const symbol = Debug.checkDefined(this.exportKind === ExportKind.ExportEquals + ? checker.resolveExternalModuleSymbol(moduleSymbol) + : checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol)); + symbols.set(this.id, { moduleSymbol, symbol }); + return symbol; + } + }; + + exportInfo.add(k, info); }, - get: (file) => { - if (usableByFileName && file !== usableByFileName) { - return undefined; - } - return cache; + get: (importingFile, importedName, symbol, moduleName, checker) => { + if (importingFile !== usableByFileName) return; + return exportInfo.get(key(importedName, symbol, moduleName, checker)); + }, + forEach: (importingFile, action) => { + if (importingFile !== usableByFileName) return; + exportInfo.forEach((info, key) => { + const { symbolName, ambientModuleName } = parseKey(key); + action(info, symbolName, !!ambientModuleName); + }); + }, + releaseSymbols: () => { + symbols.clear(); }, - getProjectVersion: () => projectVersion, onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) { if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) { // File is purely global; doesn't affect export map @@ -3250,10 +3326,33 @@ namespace ts { }, }; if (Debug.isDebugging) { - Object.defineProperty(wrapped, "__cache", { get: () => cache }); + Object.defineProperty(wrapped, "__cache", { get: () => exportInfo }); } return wrapped; + 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; } diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 5889016f0a3bb..3b57850c5773b 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -33,49 +33,87 @@ namespace ts.projectSystem { 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 contain transient symbols", () => { - const { exportMapCache, checker } = setup(); - exportMapCache.get(bTs.path as Path, checker)?.forEach((info, key) => { - if (key.startsWith("SIGINT")) { - assert(!hasProperty(info[0].symbol, "type")); - assert(!hasProperty(info[0].symbol, "nameType")); + 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 trasient 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 CachedSymbolExportInfo[] | 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({ + 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(programBefore !== project.getCurrentProgram()!); + + // Get same info from cache again + let sigintPropAfter: readonly CachedSymbolExportInfo[] | undefined; + exportMapCache.forEach(bTs.path as Path, (info, name) => { + if (name === "SIGINT") sigintPropAfter = info; + }); + assert.ok(sigintPropAfter); + assert.notEqual(symbolIdBefore, getSymbolId(sigintPropAfter![0].symbol)); }); }); @@ -87,7 +125,7 @@ namespace ts.projectSystem { 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.getExportMapCache(), checker, triggerCompletions }; function triggerCompletions() { const requestLocation: protocol.FileLocationRequestArgs = { From 73b241e173fa229c112389b18c80ba54b8745d7c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 25 Jun 2021 16:10:04 -0500 Subject: [PATCH 03/10] A class is much faster, apparently --- src/services/utilities.ts | 81 +++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 34 deletions(-) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index bc85470c59e5c..016dd46516adb 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3216,6 +3216,50 @@ namespace ts { } export function createExportMapCache(host: ExportMapCacheHost): ExportMapCache { + class CachedSymbolExportInfo { + constructor( + public id: number, + public symbolName: string, + public moduleName: string, + public moduleFileName: string | undefined, + public exportKind: ExportKind, + public isTypeOnly: boolean, + public isFromPackageJson: boolean, + ) {} + + get symbol() { + const fromCache = symbols.get(this.id)?.symbol; + if (fromCache) return fromCache; + const moduleSymbol = this.moduleSymbol; + const containingProgram = this.isFromPackageJson + ? host.getPackageJsonAutoImportProvider()! + : host.getCurrentProgram()!; + const checker = containingProgram.getTypeChecker(); + const symbolName = this.exportKind === ExportKind.Default + ? InternalSymbolName.Default + : this.symbolName; + const symbol = Debug.checkDefined(this.exportKind === ExportKind.ExportEquals + ? checker.resolveExternalModuleSymbol(moduleSymbol) + : checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol)); + symbols.set(this.id, { moduleSymbol, symbol }); + return symbol; + } + + get moduleSymbol() { + const fromCache = symbols.get(this.id)?.moduleSymbol; + if (fromCache) return fromCache; + const containingProgram = this.isFromPackageJson + ? host.getPackageJsonAutoImportProvider()! + : host.getCurrentProgram()!; + const checker = containingProgram.getTypeChecker(); + const moduleSymbol = Debug.checkDefined(this.moduleFileName + ? checker.getMergedSymbol(containingProgram.getSourceFile(this.moduleFileName)!.symbol) + : checker.tryFindAmbientModule(this.moduleName)); + symbols.set(this.id, { moduleSymbol, symbol: undefined }); + return moduleSymbol; + } + } + let exportInfoId = 0; const exportInfo = createMultiMap(); const symbols = new Map(); @@ -3246,45 +3290,14 @@ namespace ts { symbol = undefined!; moduleSymbol = undefined!; - const info: CachedSymbolExportInfo = { + const info = new CachedSymbolExportInfo( id, - symbolName: importedName, + importedName, moduleName, moduleFileName, exportKind, isTypeOnly, - isFromPackageJson, - get moduleSymbol() { - const fromCache = symbols.get(this.id)?.moduleSymbol; - if (fromCache) return fromCache; - const containingProgram = this.isFromPackageJson - ? host.getPackageJsonAutoImportProvider()! - : host.getCurrentProgram()!; - const checker = containingProgram.getTypeChecker(); - const moduleSymbol = Debug.checkDefined(this.moduleFileName - ? checker.getMergedSymbol(containingProgram.getSourceFile(this.moduleFileName)!.symbol) - : checker.tryFindAmbientModule(this.moduleName)); - symbols.set(this.id, { moduleSymbol, symbol: undefined }); - return moduleSymbol; - }, - get symbol() { - const fromCache = symbols.get(this.id)?.symbol; - if (fromCache) return fromCache; - const moduleSymbol = this.moduleSymbol; - const containingProgram = this.isFromPackageJson - ? host.getPackageJsonAutoImportProvider()! - : host.getCurrentProgram()!; - const checker = containingProgram.getTypeChecker(); - const symbolName = this.exportKind === ExportKind.Default - ? InternalSymbolName.Default - : this.symbolName; - const symbol = Debug.checkDefined(this.exportKind === ExportKind.ExportEquals - ? checker.resolveExternalModuleSymbol(moduleSymbol) - : checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol)); - symbols.set(this.id, { moduleSymbol, symbol }); - return symbol; - } - }; + isFromPackageJson); exportInfo.add(k, info); }, From ba59f6d20c2cd28b7e52794611f68800cc487b73 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 29 Jun 2021 12:43:14 -0500 Subject: [PATCH 04/10] This is probably best? --- src/services/codefixes/importFixes.ts | 35 ++-- src/services/completions.ts | 16 +- src/services/utilities.ts | 160 ++++++++++-------- .../unittests/tsserver/exportMapCache.ts | 4 +- 4 files changed, 117 insertions(+), 98 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 7a8db48444b82..f27ad088e7606 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -230,11 +230,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, isTypeOnly: 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, isTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; + return { symbol: named, moduleSymbol, moduleFileName: undefined, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(symbol, checker), isFromPackageJson }; } } } @@ -255,12 +255,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, isTypeOnly: 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, isTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); + result.push({ symbol: exported, moduleSymbol, moduleFileName: moduleFile?.fileName, exportKind: ExportKind.Named, isTypeOnly: isTypeOnlySymbol(exported, checker), isFromPackageJson }); } } }); @@ -271,7 +271,8 @@ namespace ts.codefix { } } - export function getModuleSpecifierForBestExportInfo(exportInfo: readonly T[], + export function getModuleSpecifierForBestExportInfo( + exportInfo: readonly T[], importingFile: SourceFile, program: Program, host: LanguageServiceHost, @@ -311,7 +312,8 @@ namespace ts.codefix { host.log?.("getSymbolToExportInfoMap: 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(); + 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)) { @@ -319,21 +321,20 @@ namespace ts.codefix { importingFile.path, defaultInfo.symbol, moduleSymbol, + moduleFile, defaultInfo.exportKind, - isTypeOnlySymbol(defaultInfo.symbol, checker), isFromPackageJson, scriptTarget, checker); } - const seenExports = new Map(); for (const exported of checker.getExportsAndPropertiesOfModule(moduleSymbol)) { if (exported !== defaultInfo?.symbol && addToSeen(seenExports, exported)) { cache.add( importingFile.path, exported, moduleSymbol, + moduleFile, ExportKind.Named, - isTypeOnlySymbol(exported, checker), isFromPackageJson, scriptTarget, checker); @@ -494,7 +495,7 @@ namespace ts.codefix { position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, - moduleSymbols: readonly T[], + moduleSymbols: T | readonly T[], host: LanguageServiceHost, preferences: UserPreferences, fromCacheOnly?: boolean, @@ -508,7 +509,11 @@ namespace ts.codefix { : (moduleSymbol: Symbol) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences); let computedWithoutCacheCount = 0; - const fixes = flatMap(moduleSymbols, exportInfo => { + const fixes = isArray(moduleSymbols) ? flatMap(moduleSymbols, getFixesForExportInfo) : getFixesForExportInfo(moduleSymbols) || emptyArray; + + return { computedWithoutCacheCount, fixes }; + + function getFixesForExportInfo(exportInfo: T) { const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo.moduleSymbol); computedWithoutCacheCount += Number(computedWithoutCache); return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixUseImportType => @@ -523,9 +528,7 @@ namespace ts.codefix { typeOnly: preferTypeOnlyImport, exportInfo, }); - }); - - return { computedWithoutCacheCount, fixes }; + } } function getFixesForAddImport( @@ -590,7 +593,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, isTypeOnly: 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 }; @@ -696,7 +699,7 @@ namespace ts.codefix { !toFile && packageJsonFilter.allowsImportingAmbientModule(moduleSymbol, moduleSpecifierResolutionHost) ) { const checker = program.getTypeChecker(); - originalSymbolToExportInfos.add(getUniqueSymbolId(exportedSymbol, checker).toString(), { symbol: exportedSymbol, moduleSymbol, exportKind, isTypeOnly: 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) => { diff --git a/src/services/completions.ts b/src/services/completions.ts index 7ac62a7910a5f..3c78fbe9a72bc 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -148,12 +148,12 @@ namespace ts.Completions { const enum GlobalsSearch { Continue, Success, Fail } interface ModuleSpecifierResolutioContext { - tryResolve: (exportInfo: readonly CachedSymbolExportInfo[], isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult | undefined; + tryResolve: (exportInfo: readonly SymbolExportInfo[], isFromAmbientModule: boolean) => ModuleSpecifierResolutionResult | undefined; resolutionLimitExceeded: () => boolean; } interface ModuleSpecifierResolutionResult { - exportInfo?: CachedSymbolExportInfo; + exportInfo?: SymbolExportInfo; moduleSpecifier: string; } @@ -181,7 +181,7 @@ namespace ts.Completions { host.log?.(`${logPrefix}: ${timestamp() - start}`); return result; - function tryResolve(exportInfo: readonly CachedSymbolExportInfo[], isFromAmbientModule: boolean): ModuleSpecifierResolutionResult | undefined { + function tryResolve(exportInfo: readonly SymbolExportInfo[], isFromAmbientModule: boolean): ModuleSpecifierResolutionResult | undefined { if (isFromAmbientModule) { const result = codefix.getModuleSpecifierForBestExportInfo(exportInfo, sourceFile, program, host, preferences); if (result) { @@ -1927,15 +1927,16 @@ namespace ts.Completions { context => { exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => { if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; - const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === i.moduleName); + const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === i.moduleSymbol.name); if (isCompletionDetailsMatch || charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) { - if (isFromAmbientModule && !some(info, isImportableExportInfo)) { + const defaultExportInfo = find(info, isImportableExportInfo); + if (isFromAmbientModule && !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) || {}; + const { exportInfo = defaultExportInfo, moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {}; if (!exportInfo) return; const isDefaultExport = exportInfo.exportKind === ExportKind.Default; @@ -1957,7 +1958,7 @@ namespace ts.Completions { } ); - function isImportableExportInfo(info: CachedSymbolExportInfo) { + function isImportableExportInfo(info: SymbolExportInfo) { const moduleFile = tryCast(info.moduleSymbol.valueDeclaration, isSourceFile); if (!moduleFile) { return packageJsonFilter @@ -1975,6 +1976,7 @@ namespace ts.Completions { } } + function pushAutoImportSymbol(symbol: Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { const symbolId = getSymbolId(symbol); if (symbolToSortTextIdMap[symbolId] === SortTextId.GlobalsOrKeywords) { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 016dd46516adb..3b976050cfaf6 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3184,6 +3184,8 @@ namespace ts { 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; @@ -3191,19 +3193,28 @@ namespace ts { isFromPackageJson: boolean; } - export interface CachedSymbolExportInfo extends SymbolExportInfo { + 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 ExportMapCache { isUsableByFile(importingFile: Path): boolean; clear(): void; - add(importingFile: Path, symbol: Symbol, moduleSymbol: Symbol, exportKind: ExportKind, isTypeOnly: boolean, isFromPackageJson: boolean, scriptTarget: ScriptTarget, checker: TypeChecker): void; - get(importingFile: Path, importedName: string, symbol: Symbol, moduleName: string, checker: TypeChecker): readonly CachedSymbolExportInfo[] | undefined; - forEach(importingFile: Path, action: (info: readonly CachedSymbolExportInfo[], name: string, isFromAmbientModule: boolean) => void): 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 */ @@ -3216,106 +3227,78 @@ namespace ts { } export function createExportMapCache(host: ExportMapCacheHost): ExportMapCache { - class CachedSymbolExportInfo { - constructor( - public id: number, - public symbolName: string, - public moduleName: string, - public moduleFileName: string | undefined, - public exportKind: ExportKind, - public isTypeOnly: boolean, - public isFromPackageJson: boolean, - ) {} - - get symbol() { - const fromCache = symbols.get(this.id)?.symbol; - if (fromCache) return fromCache; - const moduleSymbol = this.moduleSymbol; - const containingProgram = this.isFromPackageJson - ? host.getPackageJsonAutoImportProvider()! - : host.getCurrentProgram()!; - const checker = containingProgram.getTypeChecker(); - const symbolName = this.exportKind === ExportKind.Default - ? InternalSymbolName.Default - : this.symbolName; - const symbol = Debug.checkDefined(this.exportKind === ExportKind.ExportEquals - ? checker.resolveExternalModuleSymbol(moduleSymbol) - : checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol)); - symbols.set(this.id, { moduleSymbol, symbol }); - return symbol; - } - - get moduleSymbol() { - const fromCache = symbols.get(this.id)?.moduleSymbol; - if (fromCache) return fromCache; - const containingProgram = this.isFromPackageJson - ? host.getPackageJsonAutoImportProvider()! - : host.getCurrentProgram()!; - const checker = containingProgram.getTypeChecker(); - const moduleSymbol = Debug.checkDefined(this.moduleFileName - ? checker.getMergedSymbol(containingProgram.getSourceFile(this.moduleFileName)!.symbol) - : checker.tryFindAmbientModule(this.moduleName)); - symbols.set(this.id, { moduleSymbol, symbol: undefined }); - return moduleSymbol; - } - } - - let exportInfoId = 0; - const exportInfo = createMultiMap(); - const symbols = new Map(); + let exportInfoId = 1; + const exportInfo = new Map(); + const symbols = new Map(); + const moduleSymbols = new Map(); let usableByFileName: Path | undefined; const wrapped: ExportMapCache = { isUsableByFile: importingFile => importingFile === usableByFileName, isEmpty: () => !exportInfo.size, - clear() { + clear: () => { exportInfo.clear(); symbols.clear(); usableByFileName = undefined; }, - add(importingFile, symbol, moduleSymbol, exportKind, isTypeOnly, isFromPackageJson, scriptTarget, checker) { + add: (importingFile, symbol, moduleSymbol, moduleFile, exportKind, isFromPackageJson, scriptTarget, checker) => { if (importingFile !== usableByFileName) { - this.clear(); + wrapped.clear(); usableByFileName = importingFile; } const isDefault = exportKind === ExportKind.Default; const importedName = getNameForExportedSymbol(isDefault && getLocalSymbolForExportDefault(symbol) || symbol, scriptTarget); const moduleName = stripQuotes(moduleSymbol.name); - const moduleFileName = isExternalModuleNameRelative(moduleName) ? Debug.checkDefined(getSourceFileOfModule(moduleSymbol)).fileName : undefined; const id = exportInfoId++; const k = key(importedName, symbol, moduleName, checker); - symbols.set(id, { symbol, moduleSymbol }); - // Reminder not to access these in getters; - // they should be stored only in `symbols` so they can be - // released on program updates. - symbol = undefined!; - moduleSymbol = undefined!; - - const info = new CachedSymbolExportInfo( + const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol; + const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol; + + if (!storedSymbol) symbols.set(id, symbol); + if (!storedModuleSymbol) moduleSymbols.set(id, moduleSymbol); + + const info: CachedSymbolExportInfo = { id, - importedName, + symbolName: importedName, moduleName, - moduleFileName, + moduleFile, + moduleFileName: moduleFile?.fileName, exportKind, - isTypeOnly, - isFromPackageJson); - - exportInfo.add(k, info); + isTypeOnly: !(skipAlias(symbol, checker).flags & SymbolFlags.Value), + isFromPackageJson, + symbol: storedSymbol, + moduleSymbol: storedModuleSymbol, + }; + + const existing = exportInfo.get(k); + if (existing) { + if (isArray(existing)) { + existing.push(info); + } + else { + exportInfo.set(k, [existing, info]); + } + } + else { + exportInfo.set(k, info); + } }, get: (importingFile, importedName, symbol, moduleName, checker) => { if (importingFile !== usableByFileName) return; - return exportInfo.get(key(importedName, symbol, moduleName, checker)); + const result = exportInfo.get(key(importedName, symbol, moduleName, checker)); + return !result ? result : toArray(result).map(rehydrateCachedInfo); }, forEach: (importingFile, action) => { if (importingFile !== usableByFileName) return; exportInfo.forEach((info, key) => { const { symbolName, ambientModuleName } = parseKey(key); - action(info, symbolName, !!ambientModuleName); + action(toArray(info).map(rehydrateCachedInfo), symbolName, !!ambientModuleName); }); }, releaseSymbols: () => { symbols.clear(); + moduleSymbols.clear(); }, - onFileChanged(oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) { + onFileChanged: (oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) => { if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) { // File is purely global; doesn't affect export map return false; @@ -3331,7 +3314,7 @@ namespace ts { !arrayIsEqualTo(oldSourceFile.moduleAugmentations, newSourceFile.moduleAugmentations) || !ambientModuleDeclarationsAreEqual(oldSourceFile, newSourceFile) ) { - this.clear(); + wrapped.clear(); return true; } usableByFileName = newSourceFile.path; @@ -3343,6 +3326,33 @@ namespace ts { } return wrapped; + function rehydrateCachedInfo(info: CachedSymbolExportInfo): SymbolExportInfo { + if (info.symbol && info.moduleSymbol) return info as SymbolExportInfo; + const { id, exportKind, isTypeOnly, isFromPackageJson, moduleFileName } = info; + const checker = (isFromPackageJson + ? host.getPackageJsonAutoImportProvider()! + : host.getCurrentProgram()!).getTypeChecker(); + const moduleSymbol = info.moduleSymbol || moduleSymbols.get(id) || Debug.checkDefined(info.moduleFile + ? checker.getMergedSymbol(info.moduleFile.symbol) + : checker.tryFindAmbientModule(info.moduleName)); + if (!info.moduleSymbol) moduleSymbols.set(id, moduleSymbol); + const symbolName = exportKind === ExportKind.Default + ? InternalSymbolName.Default + : info.symbolName; + const symbol = info.symbol || symbols.get(id) || Debug.checkDefined(exportKind === ExportKind.ExportEquals + ? checker.resolveExternalModuleSymbol(moduleSymbol) + : checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol)); + if (!info.symbol) symbols.set(id, symbol); + 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; @@ -3386,6 +3396,10 @@ namespace ts { } return true; } + + function isArray(info: CachedSymbolExportInfo | CachedSymbolExportInfo[]): info is CachedSymbolExportInfo[] { + return !(info as CachedSymbolExportInfo).id; + } } export interface ModuleSpecifierResolutionCacheHost { diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 3b57850c5773b..4516b4f101233 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -82,7 +82,7 @@ namespace ts.projectSystem { // 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 CachedSymbolExportInfo[] | undefined; + let sigintPropBefore: readonly SymbolExportInfo[] | undefined; exportMapCache.forEach(bTs.path as Path, (info, name) => { if (name === "SIGINT") sigintPropBefore = info; }); @@ -108,7 +108,7 @@ namespace ts.projectSystem { assert(programBefore !== project.getCurrentProgram()!); // Get same info from cache again - let sigintPropAfter: readonly CachedSymbolExportInfo[] | undefined; + let sigintPropAfter: readonly SymbolExportInfo[] | undefined; exportMapCache.forEach(bTs.path as Path, (info, name) => { if (name === "SIGINT") sigintPropAfter = info; }); From 1d315959bc77d08543a5c6d3f74c9bdad55ce281 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 29 Jun 2021 13:04:01 -0500 Subject: [PATCH 05/10] Back to multimap --- src/services/utilities.ts | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 3b976050cfaf6..547a763f2be53 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3228,7 +3228,7 @@ namespace ts { export function createExportMapCache(host: ExportMapCacheHost): ExportMapCache { let exportInfoId = 1; - const exportInfo = new Map(); + const exportInfo = createMultiMap(); const symbols = new Map(); const moduleSymbols = new Map(); let usableByFileName: Path | undefined; @@ -3249,14 +3249,13 @@ namespace ts { const importedName = getNameForExportedSymbol(isDefault && getLocalSymbolForExportDefault(symbol) || symbol, scriptTarget); const moduleName = stripQuotes(moduleSymbol.name); const id = exportInfoId++; - const k = key(importedName, symbol, moduleName, checker); const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol; const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol; if (!storedSymbol) symbols.set(id, symbol); if (!storedModuleSymbol) moduleSymbols.set(id, moduleSymbol); - const info: CachedSymbolExportInfo = { + exportInfo.add(key(importedName, symbol, moduleName, checker), { id, symbolName: importedName, moduleName, @@ -3267,31 +3266,18 @@ namespace ts { isFromPackageJson, symbol: storedSymbol, moduleSymbol: storedModuleSymbol, - }; - - const existing = exportInfo.get(k); - if (existing) { - if (isArray(existing)) { - existing.push(info); - } - else { - exportInfo.set(k, [existing, info]); - } - } - else { - exportInfo.set(k, info); - } + }); }, get: (importingFile, importedName, symbol, moduleName, checker) => { if (importingFile !== usableByFileName) return; const result = exportInfo.get(key(importedName, symbol, moduleName, checker)); - return !result ? result : toArray(result).map(rehydrateCachedInfo); + return result?.map(rehydrateCachedInfo); }, forEach: (importingFile, action) => { if (importingFile !== usableByFileName) return; exportInfo.forEach((info, key) => { const { symbolName, ambientModuleName } = parseKey(key); - action(toArray(info).map(rehydrateCachedInfo), symbolName, !!ambientModuleName); + action(info.map(rehydrateCachedInfo), symbolName, !!ambientModuleName); }); }, releaseSymbols: () => { @@ -3396,10 +3382,6 @@ namespace ts { } return true; } - - function isArray(info: CachedSymbolExportInfo | CachedSymbolExportInfo[]): info is CachedSymbolExportInfo[] { - return !(info as CachedSymbolExportInfo).id; - } } export interface ModuleSpecifierResolutionCacheHost { From 3b9946718d4292b258c16417ce193b083485c97b Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 29 Jun 2021 14:25:47 -0500 Subject: [PATCH 06/10] Go back to single symbol cache --- src/services/utilities.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 547a763f2be53..9abf9fa8bddec 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3229,8 +3229,7 @@ namespace ts { export function createExportMapCache(host: ExportMapCacheHost): ExportMapCache { let exportInfoId = 1; const exportInfo = createMultiMap(); - const symbols = new Map(); - const moduleSymbols = new Map(); + const symbols = new Map(); let usableByFileName: Path | undefined; const wrapped: ExportMapCache = { isUsableByFile: importingFile => importingFile === usableByFileName, @@ -3251,9 +3250,7 @@ namespace ts { const id = exportInfoId++; const storedSymbol = symbol.flags & SymbolFlags.Transient ? undefined : symbol; const storedModuleSymbol = moduleSymbol.flags & SymbolFlags.Transient ? undefined : moduleSymbol; - - if (!storedSymbol) symbols.set(id, symbol); - if (!storedModuleSymbol) moduleSymbols.set(id, moduleSymbol); + if (!storedSymbol || !storedModuleSymbol) symbols.set(id, [symbol, moduleSymbol]); exportInfo.add(key(importedName, symbol, moduleName, checker), { id, @@ -3282,7 +3279,6 @@ namespace ts { }, releaseSymbols: () => { symbols.clear(); - moduleSymbols.clear(); }, onFileChanged: (oldSourceFile: SourceFile, newSourceFile: SourceFile, typeAcquisitionEnabled: boolean) => { if (fileIsGlobalOnly(oldSourceFile) && fileIsGlobalOnly(newSourceFile)) { @@ -3315,20 +3311,30 @@ namespace ts { 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 || moduleSymbols.get(id) || Debug.checkDefined(info.moduleFile + const moduleSymbol = info.moduleSymbol || cachedModuleSymbol || Debug.checkDefined(info.moduleFile ? checker.getMergedSymbol(info.moduleFile.symbol) : checker.tryFindAmbientModule(info.moduleName)); - if (!info.moduleSymbol) moduleSymbols.set(id, moduleSymbol); const symbolName = exportKind === ExportKind.Default ? InternalSymbolName.Default : info.symbolName; - const symbol = info.symbol || symbols.get(id) || Debug.checkDefined(exportKind === ExportKind.ExportEquals + const symbol = info.symbol || cachedSymbol || Debug.checkDefined(exportKind === ExportKind.ExportEquals ? checker.resolveExternalModuleSymbol(moduleSymbol) : checker.tryGetMemberInModuleExportsAndProperties(symbolName, moduleSymbol)); - if (!info.symbol) symbols.set(id, symbol); + symbols.set(id, [symbol, moduleSymbol]); return { symbol, moduleSymbol, From f429d0c66dbcc29862a2b31005e169b3a8b1570a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 29 Jun 2021 14:34:33 -0500 Subject: [PATCH 07/10] Revert now-unnecessary generics --- src/services/codefixes/importFixes.ts | 39 +++++++++++++-------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index f27ad088e7606..057017e206f57 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -153,18 +153,18 @@ namespace ts.codefix { // Sorted with the preferred fix coming first. const enum ImportFixKind { UseNamespace, ImportType, AddToExisting, AddNew } - type ImportFix = FixUseNamespaceImport | FixUseImportType | FixAddToExistingImport | FixAddNewImport; + type ImportFix = FixUseNamespaceImport | FixUseImportType | FixAddToExistingImport | FixAddNewImport; interface FixUseNamespaceImport { readonly kind: ImportFixKind.UseNamespace; readonly namespacePrefix: string; readonly position: number; readonly moduleSpecifier: string; } - interface FixUseImportType { + interface FixUseImportType { readonly kind: ImportFixKind.ImportType; readonly moduleSpecifier: string; readonly position: number; - readonly exportInfo: T; + readonly exportInfo: SymbolExportInfo; } interface FixAddToExistingImport { readonly kind: ImportFixKind.AddToExisting; @@ -173,13 +173,13 @@ namespace ts.codefix { readonly importKind: ImportKind.Default | ImportKind.Named; readonly canUseTypeOnlyImport: boolean; } - interface FixAddNewImport { + interface FixAddNewImport { readonly kind: ImportFixKind.AddNew; readonly moduleSpecifier: string; readonly importKind: ImportKind; readonly typeOnly: boolean; readonly useRequire: boolean; - readonly exportInfo?: T; + readonly exportInfo?: SymbolExportInfo; } /** Information needed to augment an existing import declaration. */ @@ -271,14 +271,14 @@ namespace ts.codefix { } } - export function getModuleSpecifierForBestExportInfo( - exportInfo: readonly T[], + export function getModuleSpecifierForBestExportInfo( + exportInfo: readonly SymbolExportInfo[], importingFile: SourceFile, program: Program, host: LanguageServiceHost, preferences: UserPreferences, fromCacheOnly?: boolean, - ): { exportInfo?: T, moduleSpecifier: string, computedWithoutCacheCount: number } | undefined { + ): { exportInfo?: SymbolExportInfo, moduleSpecifier: string, computedWithoutCacheCount: number } | undefined { const { fixes, computedWithoutCacheCount } = getNewImportFixes( program, importingFile, @@ -489,17 +489,17 @@ namespace ts.codefix { return true; } - function getNewImportFixes( + function getNewImportFixes( program: Program, sourceFile: SourceFile, position: number | undefined, preferTypeOnlyImport: boolean, useRequire: boolean, - moduleSymbols: T | readonly T[], + moduleSymbols: readonly SymbolExportInfo[], host: LanguageServiceHost, preferences: UserPreferences, fromCacheOnly?: boolean, - ): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixUseImportType)[] } { + ): { computedWithoutCacheCount: number, fixes: readonly (FixAddNewImport | FixUseImportType)[] } { const isJs = isSourceFileJS(sourceFile); const compilerOptions = program.getCompilerOptions(); const moduleSpecifierResolutionHost = createModuleSpecifierResolutionHost(program, host); @@ -509,14 +509,10 @@ namespace ts.codefix { : (moduleSymbol: Symbol) => moduleSpecifiers.getModuleSpecifiersWithCacheInfo(moduleSymbol, checker, compilerOptions, sourceFile, moduleSpecifierResolutionHost, preferences); let computedWithoutCacheCount = 0; - const fixes = isArray(moduleSymbols) ? flatMap(moduleSymbols, getFixesForExportInfo) : getFixesForExportInfo(moduleSymbols) || emptyArray; - - return { computedWithoutCacheCount, fixes }; - - function getFixesForExportInfo(exportInfo: T) { + const fixes = flatMap(moduleSymbols, exportInfo => { const { computedWithoutCache, moduleSpecifiers } = getModuleSpecifiers(exportInfo.moduleSymbol); - computedWithoutCacheCount += Number(computedWithoutCache); - return moduleSpecifiers?.map((moduleSpecifier): FixAddNewImport | FixUseImportType => + 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.isTypeOnly && isJs && position !== undefined ? { kind: ImportFixKind.ImportType, moduleSpecifier, position, exportInfo } @@ -527,8 +523,11 @@ namespace ts.codefix { useRequire, typeOnly: preferTypeOnlyImport, exportInfo, - }); - } + } + ); + }); + + return { computedWithoutCacheCount, fixes }; } function getFixesForAddImport( From 2dbec1951a24b92cd194e05659fef5daf95e581e Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 29 Jun 2021 15:40:20 -0500 Subject: [PATCH 08/10] Rename and reorganize --- src/server/moduleSpecifierCache.ts | 95 ++++ src/server/project.ts | 6 +- src/server/tsconfig.json | 1 + src/services/codefixes/importFixes.ts | 133 ------ src/services/completions.ts | 6 +- src/services/exportInfoMap.ts | 411 ++++++++++++++++++ src/services/tsconfig.json | 1 + src/services/types.ts | 2 +- src/services/utilities.ts | 369 ---------------- .../unittests/tsserver/exportMapCache.ts | 2 +- 10 files changed, 516 insertions(+), 510 deletions(-) create mode 100644 src/server/moduleSpecifierCache.ts create mode 100644 src/services/exportInfoMap.ts diff --git a/src/server/moduleSpecifierCache.ts b/src/server/moduleSpecifierCache.ts new file mode 100644 index 0000000000000..eed821a99c627 --- /dev/null +++ b/src/server/moduleSpecifierCache.ts @@ -0,0 +1,95 @@ +/*@internal*/ +namespace ts.server { + export interface ModuleSpecifierResolutionCacheHost { + watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher; + } + + export function createModuleSpecifierCache(host: ModuleSpecifierResolutionCacheHost): ModuleSpecifierCache { + let containedNodeModulesWatchers: ESMap | undefined; + let cache: ESMap | 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 }; + } + } +} diff --git a/src/server/project.ts b/src/server/project.ts index 030e126939f4d..aec6b50466542 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -251,7 +251,7 @@ namespace ts.server { public readonly getCanonicalFileName: GetCanonicalFileName; /*@internal*/ - exportMapCache: ExportMapCache | undefined; + exportMapCache: ExportInfoMap | undefined; /*@internal*/ private changedFilesForExportMapCache: Set | undefined; /*@internal*/ @@ -1668,8 +1668,8 @@ namespace ts.server { } /*@internal*/ - getExportMapCache() { - return this.exportMapCache ||= createExportMapCache(this); + getCachedExportInfoMap() { + return this.exportMapCache ||= createCacheableExportInfoMap(this); } /*@internal*/ diff --git a/src/server/tsconfig.json b/src/server/tsconfig.json index e9d853de9ee5a..b0a2f59ba77d6 100644 --- a/src/server/tsconfig.json +++ b/src/server/tsconfig.json @@ -23,6 +23,7 @@ "typingsCache.ts", "project.ts", "editorServices.ts", + "moduleSpecifierCache.ts", "packageJsonCache.ts", "session.ts", "scriptVersionCache.ts" diff --git a/src/services/codefixes/importFixes.ts b/src/services/codefixes/importFixes.ts index 057017e206f57..378a541425417 100644 --- a/src/services/codefixes/importFixes.ts +++ b/src/services/codefixes/importFixes.ts @@ -293,59 +293,6 @@ namespace ts.codefix { return result && { ...result, computedWithoutCacheCount }; } - export function getSymbolToExportInfoMap(importingFile: SourceFile, host: LanguageServiceHost, program: Program): ExportMapCache { - 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?.() || createExportMapCache({ - getCurrentProgram: () => program, - getPackageJsonAutoImportProvider: () => host.getPackageJsonAutoImportProvider?.(), - }); - - if (cache.isUsableByFile(importingFile.path)) { - host.log?.("getSymbolToExportInfoMap: cache hit"); - return cache; - } - - host.log?.("getSymbolToExportInfoMap: cache miss or empty; calculating new results"); - const compilerOptions = program.getCompilerOptions(); - const scriptTarget = getEmitScriptTarget(compilerOptions); - const seenExports = new Map(); - 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)) { - 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?.(`getSymbolToExportInfoMap: done in ${timestamp() - start} ms`); - return cache; - } - function isTypeOnlySymbol(s: Symbol, checker: TypeChecker): boolean { return !(skipAlias(s, checker).flags & SymbolFlags.Value); } @@ -720,21 +667,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 @@ -761,43 +693,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 => { @@ -993,34 +888,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); } diff --git a/src/services/completions.ts b/src/services/completions.ts index 3c78fbe9a72bc..7c9583a0b6347 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -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(); @@ -1914,7 +1914,7 @@ 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); resolvingModuleSpecifiers( @@ -1927,7 +1927,7 @@ namespace ts.Completions { context => { exportInfo.forEach(sourceFile.path, (info, symbolName, isFromAmbientModule) => { if (!detailsEntryId && isStringANonContextualKeyword(symbolName)) return; - const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === i.moduleSymbol.name); + const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); if (isCompletionDetailsMatch || charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) { const defaultExportInfo = find(info, isImportableExportInfo); if (isFromAmbientModule && !defaultExportInfo) { diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts new file mode 100644 index 0000000000000..3246db963c97e --- /dev/null +++ b/src/services/exportInfoMap.ts @@ -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(); + const symbols = new Map(); + 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?.("getSymbolToExportInfoMap: cache hit"); + return cache; + } + + host.log?.("getSymbolToExportInfoMap: 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(); + 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?.(`getSymbolToExportInfoMap: 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; + } + }); + } +} diff --git a/src/services/tsconfig.json b/src/services/tsconfig.json index ba72007a40e2d..f1aa438fefb57 100644 --- a/src/services/tsconfig.json +++ b/src/services/tsconfig.json @@ -11,6 +11,7 @@ "files": [ "types.ts", "utilities.ts", + "exportInfoMap.ts", "classifier.ts", "classifier2020.ts", "stringCompletions.ts", diff --git a/src/services/types.ts b/src/services/types.ts index a7c6238d60cce..4ad9ddfa88946 100644 --- a/src/services/types.ts +++ b/src/services/types.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; diff --git a/src/services/utilities.ts b/src/services/utilities.ts index 9abf9fa8bddec..cba0669f43d9f 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -3167,375 +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, - } - - 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 ExportMapCache { - 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 ExportMapCacheHost { - getCurrentProgram(): Program | undefined; - getPackageJsonAutoImportProvider(): Program | undefined; - } - - export function createExportMapCache(host: ExportMapCacheHost): ExportMapCache { - let exportInfoId = 1; - const exportInfo = createMultiMap(); - const symbols = new Map(); - let usableByFileName: Path | undefined; - const wrapped: ExportMapCache = { - 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) { - wrapped.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) - ) { - wrapped.clear(); - return true; - } - usableByFileName = newSourceFile.path; - return false; - }, - }; - if (Debug.isDebugging) { - Object.defineProperty(wrapped, "__cache", { get: () => exportInfo }); - } - return wrapped; - - 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 interface ModuleSpecifierResolutionCacheHost { - watchNodeModulesForPackageJsonChanges(directoryPath: string): FileWatcher; - } - - export function createModuleSpecifierCache(host: ModuleSpecifierResolutionCacheHost): ModuleSpecifierCache { - let containedNodeModulesWatchers: ESMap | undefined; - let cache: ESMap | 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); } diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 4516b4f101233..046bf844ae9f6 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -125,7 +125,7 @@ namespace ts.projectSystem { const project = configuredProjectAt(projectService, 0); triggerCompletions(); const checker = project.getLanguageService().getProgram()!.getTypeChecker(); - return { host, project, projectService, session, exportMapCache: project.getExportMapCache(), checker, triggerCompletions }; + return { host, project, projectService, session, exportMapCache: project.getCachedExportInfoMap(), checker, triggerCompletions }; function triggerCompletions() { const requestLocation: protocol.FileLocationRequestArgs = { From bf88641513d1366e5685f94b21b866433c6debe5 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 6 Jul 2021 10:44:12 -0600 Subject: [PATCH 09/10] Fix weird compound condition --- src/services/completions.ts | 5 +---- src/services/exportInfoMap.ts | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index 7c9583a0b6347..f5f9824b0ad4e 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1930,15 +1930,13 @@ namespace ts.Completions { const isCompletionDetailsMatch = detailsEntryId && some(info, i => detailsEntryId.source === stripQuotes(i.moduleSymbol.name)); if (isCompletionDetailsMatch || charactersFuzzyMatchInString(symbolName, lowerCaseTokenText)) { const defaultExportInfo = find(info, isImportableExportInfo); - if (isFromAmbientModule && !defaultExportInfo) { + 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 = defaultExportInfo, moduleSpecifier } = context.tryResolve(info, isFromAmbientModule) || {}; - - if (!exportInfo) return; const isDefaultExport = exportInfo.exportKind === ExportKind.Default; const symbol = isDefaultExport && getLocalSymbolForExportDefault(exportInfo.symbol) || exportInfo.symbol; pushAutoImportSymbol(symbol, { @@ -1976,7 +1974,6 @@ namespace ts.Completions { } } - function pushAutoImportSymbol(symbol: Symbol, origin: SymbolOriginInfoResolvedExport | SymbolOriginInfoExport) { const symbolId = getSymbolId(symbol); if (symbolToSortTextIdMap[symbolId] === SortTextId.GlobalsOrKeywords) { diff --git a/src/services/exportInfoMap.ts b/src/services/exportInfoMap.ts index 3246db963c97e..78c20881cae3d 100644 --- a/src/services/exportInfoMap.ts +++ b/src/services/exportInfoMap.ts @@ -316,11 +316,11 @@ namespace ts { }); if (cache.isUsableByFile(importingFile.path)) { - host.log?.("getSymbolToExportInfoMap: cache hit"); + host.log?.("getExportInfoMap: cache hit"); return cache; } - host.log?.("getSymbolToExportInfoMap: cache miss or empty; calculating new results"); + 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) => { @@ -353,7 +353,7 @@ namespace ts { } }); - host.log?.(`getSymbolToExportInfoMap: done in ${timestamp() - start} ms`); + host.log?.(`getExportInfoMap: done in ${timestamp() - start} ms`); return cache; } From df938164d4096059d646945605ed00baa6e7d2a4 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 6 Jul 2021 10:47:17 -0600 Subject: [PATCH 10/10] Clean up --- src/server/project.ts | 9 +++++++-- src/testRunner/unittests/tsserver/exportMapCache.ts | 4 ++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index aec6b50466542..f0a310d2c8662 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -251,7 +251,7 @@ namespace ts.server { public readonly getCanonicalFileName: GetCanonicalFileName; /*@internal*/ - exportMapCache: ExportInfoMap | undefined; + private exportMapCache: ExportInfoMap | undefined; /*@internal*/ private changedFilesForExportMapCache: Set | undefined; /*@internal*/ @@ -1672,6 +1672,11 @@ namespace ts.server { return this.exportMapCache ||= createCacheableExportInfoMap(this); } + /*@internal*/ + clearCachedExportInfoMap() { + this.exportMapCache?.clear(); + } + /*@internal*/ getModuleSpecifierCache() { return this.moduleSpecifierCache; @@ -2019,7 +2024,7 @@ namespace ts.server { const oldProgram = this.getCurrentProgram(); const hasSameSetOfFiles = super.updateGraph(); if (oldProgram && oldProgram !== this.getCurrentProgram()) { - this.hostProject.exportMapCache?.clear(); + this.hostProject.clearCachedExportInfoMap(); } return hasSameSetOfFiles; } diff --git a/src/testRunner/unittests/tsserver/exportMapCache.ts b/src/testRunner/unittests/tsserver/exportMapCache.ts index 046bf844ae9f6..5eb69c514cd1b 100644 --- a/src/testRunner/unittests/tsserver/exportMapCache.ts +++ b/src/testRunner/unittests/tsserver/exportMapCache.ts @@ -75,7 +75,7 @@ namespace ts.projectSystem { 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 trasient symbol. + // 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 @@ -105,7 +105,7 @@ namespace ts.projectSystem { } }); project.getLanguageService(/*ensureSynchronized*/ true); - assert(programBefore !== project.getCurrentProgram()!); + assert.notEqual(programBefore, project.getCurrentProgram()!); // Get same info from cache again let sigintPropAfter: readonly SymbolExportInfo[] | undefined;