From 863307811cb5ad4763451a5e6889d68f1d7caaff Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Wed, 26 Jun 2024 12:04:44 -0700 Subject: [PATCH] Typings cache need not be a map but directly on the project --- src/server/_namespaces/ts.server.ts | 1 - src/server/editorServices.ts | 27 ++- src/server/project.ts | 115 ++++++++++-- src/server/types.ts | 25 +++ src/server/typingsCache.ts | 164 ------------------ tests/baselines/reference/api/typescript.d.ts | 28 +-- 6 files changed, 162 insertions(+), 198 deletions(-) delete mode 100644 src/server/typingsCache.ts diff --git a/src/server/_namespaces/ts.server.ts b/src/server/_namespaces/ts.server.ts index cc95db272e9d9..ee4aed7c8bcc8 100644 --- a/src/server/_namespaces/ts.server.ts +++ b/src/server/_namespaces/ts.server.ts @@ -8,7 +8,6 @@ export * from "../utilities.js"; import * as protocol from "./ts.server.protocol.js"; export { protocol }; export * from "../scriptInfo.js"; -export * from "../typingsCache.js"; export * from "../project.js"; export * from "../editorServices.js"; export * from "../moduleSpecifierCache.js"; diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ffe12328f6290..637f038311339 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -83,6 +83,7 @@ import { noop, normalizePath, normalizeSlashes, + notImplemented, optionDeclarations, optionsForWatch, orderedRemoveItem, @@ -170,7 +171,6 @@ import { Msg, NormalizedPath, normalizedPathToPath, - nullTypingsInstaller, PackageInstalledResponse, PackageJsonCache, Project, @@ -183,7 +183,6 @@ import { SetTypings, ThrottledOperations, toNormalizedPath, - TypingsCache, WatchTypingLocations, } from "./_namespaces/ts.server.js"; import * as protocol from "./protocol.js"; @@ -569,6 +568,16 @@ function findProjectByName(projectName: string, projects: T[] } } +export const nullTypingsInstaller: ITypingsInstaller = { + isKnownTypesPackageName: returnFalse, + // Should never be called because we never provide a types registry. + installPackage: notImplemented, + enqueueInstallTypingsRequest: noop, + attach: noop, + onProjectClosed: noop, + globalTypingsCacheLocation: undefined!, // TODO: GH#18217 +}; + const noopConfigFileWatcher: FileWatcher = { close: noop }; /** @internal */ @@ -1173,9 +1182,6 @@ function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseW } export class ProjectService { - /** @internal */ - readonly typingsCache: TypingsCache; - /** @internal */ readonly documentRegistry: DocumentRegistry; @@ -1367,8 +1373,6 @@ export class ProjectService { this.typingsInstaller.attach(this); - this.typingsCache = new TypingsCache(this.typingsInstaller); - this.hostConfiguration = { formatCodeOptions: getDefaultFormatCodeSettings(this.host.newLine), preferences: emptyOptions, @@ -1481,11 +1485,16 @@ export class ProjectService { switch (response.kind) { case ActionSet: // Update the typing files and update the project - project.updateTypingFiles(this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typeAcquisition, response.unresolvedImports, response.typings)); + project.updateTypingFiles( + response.compilerOptions, + response.typeAcquisition, + response.unresolvedImports, + response.typings, + ); return; case ActionInvalidate: // Do not clear resolution cache, there was changes detected in typings, so enque typing request and let it get us correct results - this.typingsCache.enqueueInstallTypingsForProject(project, project.lastCachedUnresolvedImportsList, /*forceRefresh*/ true); + project.enqueueInstallTypingsForProject(/*forceRefresh*/ true); return; } } diff --git a/src/server/project.ts b/src/server/project.ts index e03954a05924a..844e2bc12f0ee 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -4,6 +4,7 @@ import { append, ApplyCodeActionCommandResult, arrayFrom, + arrayIsEqualTo, arrayToMap, BuilderState, CachedDirectoryStructureHost, @@ -143,6 +144,7 @@ import { ModuleImportResult, Msg, NormalizedPath, + nullTypingsInstaller, PackageJsonWatcher, ProjectOptions, ProjectService, @@ -150,7 +152,6 @@ import { ServerHost, Session, toNormalizedPath, - TypingsCache, updateProjectIfDirty, } from "./_namespaces/ts.server.js"; import * as protocol from "./protocol.js"; @@ -306,6 +307,62 @@ const enum TypingWatcherType { type TypingWatchers = Map & { isInvoked?: boolean; }; +interface TypingsCacheEntry { + readonly typeAcquisition: TypeAcquisition; + readonly compilerOptions: CompilerOptions; + readonly typings: SortedReadonlyArray; + readonly unresolvedImports: SortedReadonlyArray | undefined; + /* mainly useful for debugging */ + poisoned: boolean; +} + +function setIsEqualTo(arr1: string[] | undefined, arr2: string[] | undefined): boolean { + if (arr1 === arr2) { + return true; + } + if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) { + return true; + } + const set = new Map(); + let unique = 0; + + for (const v of arr1!) { + if (set.get(v) !== true) { + set.set(v, true); + unique++; + } + } + for (const v of arr2!) { + const isSet = set.get(v); + if (isSet === undefined) { + return false; + } + if (isSet === true) { + set.set(v, false); + unique--; + } + } + return unique === 0; +} + +function typeAcquisitionChanged(opt1: TypeAcquisition, opt2: TypeAcquisition): boolean { + return opt1.enable !== opt2.enable || + !setIsEqualTo(opt1.include, opt2.include) || + !setIsEqualTo(opt1.exclude, opt2.exclude); +} + +function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean { + // TODO: add more relevant properties + return getAllowJSCompilerOption(opt1) !== getAllowJSCompilerOption(opt2); +} + +function unresolvedImportsChanged(imports1: SortedReadonlyArray | undefined, imports2: SortedReadonlyArray | undefined): boolean { + if (imports1 === imports2) { + return false; + } + return !arrayIsEqualTo(imports1, imports2); +} + export abstract class Project implements LanguageServiceHost, ModuleResolutionHost { private rootFilesMap = new Map(); private program: Program | undefined; @@ -388,6 +445,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo /** @internal */ typingFiles: SortedReadonlyArray = emptyArray; + private typingsCache: TypingsCacheEntry | undefined; + private typingWatchers: TypingWatchers | undefined; /** @internal */ @@ -578,10 +637,10 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo } isKnownTypesPackageName(name: string): boolean { - return this.typingsCache.isKnownTypesPackageName(name); + return this.projectService.typingsInstaller.isKnownTypesPackageName(name); } installPackage(options: InstallPackageOptions): Promise { - return this.typingsCache.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) }); + return this.projectService.typingsInstaller.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) }); } /** @internal */ @@ -589,10 +648,6 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo return this.getGlobalCache(); } - private get typingsCache(): TypingsCache { - return this.projectService.typingsCache; - } - /** @internal */ getSymlinkCache(): SymlinkCache { if (!this.symlinks) { @@ -1059,7 +1114,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo } close() { - this.projectService.typingsCache.onProjectClosed(this); + if (this.typingsCache) this.projectService.typingsInstaller.onProjectClosed(this); + this.typingsCache = undefined; this.closeWatchingTypingLocations(); // if we have a program - release all files that are enlisted in program but arent root // The releasing of the roots happens later @@ -1379,7 +1435,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo this.lastCachedUnresolvedImportsList = getUnresolvedImports(this.program!, this.cachedUnresolvedImportsPerFile); } - this.projectService.typingsCache.enqueueInstallTypingsForProject(this, this.lastCachedUnresolvedImportsList, hasAddedorRemovedFiles); + this.enqueueInstallTypingsForProject(hasAddedorRemovedFiles); } else { this.lastCachedUnresolvedImportsList = undefined; @@ -1401,7 +1457,46 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo } /** @internal */ - updateTypingFiles(typingFiles: SortedReadonlyArray) { + enqueueInstallTypingsForProject(forceRefresh: boolean) { + const typeAcquisition = this.getTypeAcquisition(); + + if (!typeAcquisition || !typeAcquisition.enable || this.projectService.typingsInstaller === nullTypingsInstaller) { + return; + } + + const entry = this.typingsCache; + if ( + forceRefresh || + !entry || + typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) || + compilerOptionsChanged(this.getCompilationSettings(), entry.compilerOptions) || + unresolvedImportsChanged(this.lastCachedUnresolvedImportsList, entry.unresolvedImports) + ) { + // Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options. + // instead it acts as a placeholder to prevent issuing multiple requests + this.typingsCache = { + compilerOptions: this.getCompilationSettings(), + typeAcquisition, + typings: entry ? entry.typings : emptyArray, + unresolvedImports: this.lastCachedUnresolvedImportsList, + poisoned: true, + }; + // something has been changed, issue a request to update typings + this.projectService.typingsInstaller.enqueueInstallTypingsRequest(this, typeAcquisition, this.lastCachedUnresolvedImportsList); + } + } + + /** @internal */ + updateTypingFiles(compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, newTypings: string[]) { + const typings = sort(newTypings); + this.typingsCache = { + compilerOptions, + typeAcquisition, + typings, + unresolvedImports, + poisoned: false, + }; + const typingFiles = !typeAcquisition || !typeAcquisition.enable ? emptyArray : typings; if (enumerateInsertsAndDeletes(typingFiles, this.typingFiles, getStringComparer(!this.useCaseSensitiveFileNames()), /*inserted*/ noop, removed => this.detachScriptInfoFromProject(removed))) { // If typing files changed, then only schedule project update this.typingFiles = typingFiles; diff --git a/src/server/types.ts b/src/server/types.ts index 1ab26e0f20cf9..e14fb9e527135 100644 --- a/src/server/types.ts +++ b/src/server/types.ts @@ -1,10 +1,19 @@ import { + ApplyCodeActionCommandResult, DirectoryWatcherCallback, FileWatcher, FileWatcherCallback, + InstallPackageOptions, + Path, + SortedReadonlyArray, System, + TypeAcquisition, WatchOptions, } from "./_namespaces/ts.js"; +import { + Project, + ProjectService, +} from "./_namespaces/ts.server.js"; export interface CompressedData { length: number; @@ -30,3 +39,19 @@ export interface ServerHost extends System { /** @internal */ importPlugin?(root: string, moduleName: string): Promise; } + +export interface InstallPackageOptionsWithProject extends InstallPackageOptions { + projectName: string; + projectRootPath: Path; +} + +// for backwards-compatibility +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface ITypingsInstaller { + isKnownTypesPackageName(name: string): boolean; + installPackage(options: InstallPackageOptionsWithProject): Promise; + enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray | undefined): void; + attach(projectService: ProjectService): void; + onProjectClosed(p: Project): void; + readonly globalTypingsCacheLocation: string | undefined; +} diff --git a/src/server/typingsCache.ts b/src/server/typingsCache.ts deleted file mode 100644 index daa3737344011..0000000000000 --- a/src/server/typingsCache.ts +++ /dev/null @@ -1,164 +0,0 @@ -import { - ApplyCodeActionCommandResult, - arrayIsEqualTo, - CompilerOptions, - getAllowJSCompilerOption, - InstallPackageOptions, - noop, - notImplemented, - Path, - returnFalse, - sort, - SortedReadonlyArray, - TypeAcquisition, -} from "./_namespaces/ts.js"; -import { - emptyArray, - Project, - ProjectService, -} from "./_namespaces/ts.server.js"; - -export interface InstallPackageOptionsWithProject extends InstallPackageOptions { - projectName: string; - projectRootPath: Path; -} - -// for backwards-compatibility -// eslint-disable-next-line @typescript-eslint/naming-convention -export interface ITypingsInstaller { - isKnownTypesPackageName(name: string): boolean; - installPackage(options: InstallPackageOptionsWithProject): Promise; - enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray | undefined): void; - attach(projectService: ProjectService): void; - onProjectClosed(p: Project): void; - readonly globalTypingsCacheLocation: string | undefined; -} - -export const nullTypingsInstaller: ITypingsInstaller = { - isKnownTypesPackageName: returnFalse, - // Should never be called because we never provide a types registry. - installPackage: notImplemented, - enqueueInstallTypingsRequest: noop, - attach: noop, - onProjectClosed: noop, - globalTypingsCacheLocation: undefined!, // TODO: GH#18217 -}; - -interface TypingsCacheEntry { - readonly typeAcquisition: TypeAcquisition; - readonly compilerOptions: CompilerOptions; - readonly typings: SortedReadonlyArray; - readonly unresolvedImports: SortedReadonlyArray | undefined; - /* mainly useful for debugging */ - poisoned: boolean; -} - -function setIsEqualTo(arr1: string[] | undefined, arr2: string[] | undefined): boolean { - if (arr1 === arr2) { - return true; - } - if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) { - return true; - } - const set = new Map(); - let unique = 0; - - for (const v of arr1!) { - if (set.get(v) !== true) { - set.set(v, true); - unique++; - } - } - for (const v of arr2!) { - const isSet = set.get(v); - if (isSet === undefined) { - return false; - } - if (isSet === true) { - set.set(v, false); - unique--; - } - } - return unique === 0; -} - -function typeAcquisitionChanged(opt1: TypeAcquisition, opt2: TypeAcquisition): boolean { - return opt1.enable !== opt2.enable || - !setIsEqualTo(opt1.include, opt2.include) || - !setIsEqualTo(opt1.exclude, opt2.exclude); -} - -function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean { - // TODO: add more relevant properties - return getAllowJSCompilerOption(opt1) !== getAllowJSCompilerOption(opt2); -} - -function unresolvedImportsChanged(imports1: SortedReadonlyArray | undefined, imports2: SortedReadonlyArray | undefined): boolean { - if (imports1 === imports2) { - return false; - } - return !arrayIsEqualTo(imports1, imports2); -} - -/** @internal */ -export class TypingsCache { - private readonly perProjectCache = new Map(); - - constructor(private readonly installer: ITypingsInstaller) { - } - - isKnownTypesPackageName(name: string): boolean { - return this.installer.isKnownTypesPackageName(name); - } - - installPackage(options: InstallPackageOptionsWithProject): Promise { - return this.installer.installPackage(options); - } - - enqueueInstallTypingsForProject(project: Project, unresolvedImports: SortedReadonlyArray | undefined, forceRefresh: boolean) { - const typeAcquisition = project.getTypeAcquisition(); - - if (!typeAcquisition || !typeAcquisition.enable) { - return; - } - - const entry = this.perProjectCache.get(project.getProjectName()); - if ( - forceRefresh || - !entry || - typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) || - compilerOptionsChanged(project.getCompilationSettings(), entry.compilerOptions) || - unresolvedImportsChanged(unresolvedImports, entry.unresolvedImports) - ) { - // Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options. - // instead it acts as a placeholder to prevent issuing multiple requests - this.perProjectCache.set(project.getProjectName(), { - compilerOptions: project.getCompilationSettings(), - typeAcquisition, - typings: entry ? entry.typings : emptyArray, - unresolvedImports, - poisoned: true, - }); - // something has been changed, issue a request to update typings - this.installer.enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports); - } - } - - updateTypingsForProject(projectName: string, compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, newTypings: string[]) { - const typings = sort(newTypings); - this.perProjectCache.set(projectName, { - compilerOptions, - typeAcquisition, - typings, - unresolvedImports, - poisoned: false, - }); - return !typeAcquisition || !typeAcquisition.enable ? emptyArray : typings; - } - - onProjectClosed(project: Project) { - if (this.perProjectCache.delete(project.getProjectName())) { - this.installer.onProjectClosed(project); - } - } -} diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 715fdfd947822..55bb1a42a9cf8 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -2659,6 +2659,18 @@ declare namespace ts { trace?(s: string): void; require?(initialPath: string, moduleName: string): ModuleImportResult; } + interface InstallPackageOptionsWithProject extends InstallPackageOptions { + projectName: string; + projectRootPath: Path; + } + interface ITypingsInstaller { + isKnownTypesPackageName(name: string): boolean; + installPackage(options: InstallPackageOptionsWithProject): Promise; + enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray | undefined): void; + attach(projectService: ProjectService): void; + onProjectClosed(p: Project): void; + readonly globalTypingsCacheLocation: string | undefined; + } function createInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray, cachePath?: string): DiscoverTypings; function toNormalizedPath(fileName: string): NormalizedPath; function normalizedPathToPath(normalizedPath: NormalizedPath, currentDirectory: string, getCanonicalFileName: (f: string) => string): Path; @@ -2751,19 +2763,6 @@ declare namespace ts { positionToLineOffset(position: number): protocol.Location; isJavaScript(): boolean; } - interface InstallPackageOptionsWithProject extends InstallPackageOptions { - projectName: string; - projectRootPath: Path; - } - interface ITypingsInstaller { - isKnownTypesPackageName(name: string): boolean; - installPackage(options: InstallPackageOptionsWithProject): Promise; - enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray | undefined): void; - attach(projectService: ProjectService): void; - onProjectClosed(p: Project): void; - readonly globalTypingsCacheLocation: string | undefined; - } - const nullTypingsInstaller: ITypingsInstaller; function allRootFilesAreJsOrDts(project: Project): boolean; function allFilesAreJsOrDts(project: Project): boolean; enum ProjectKind { @@ -2817,6 +2816,7 @@ declare namespace ts { private lastReportedVersion; protected projectErrors: Diagnostic[] | undefined; protected isInitialLoadPending: () => boolean; + private typingsCache; private typingWatchers; private readonly cancellationToken; isNonTsProject(): boolean; @@ -2829,7 +2829,6 @@ declare namespace ts { readonly jsDocParsingMode: JSDocParsingMode | undefined; isKnownTypesPackageName(name: string): boolean; installPackage(options: InstallPackageOptions): Promise; - private get typingsCache(); getCompilationSettings(): ts.CompilerOptions; getCompilerOptions(): ts.CompilerOptions; getNewLine(): string; @@ -3148,6 +3147,7 @@ declare namespace ts { configFileName?: NormalizedPath; configFileErrors?: readonly Diagnostic[]; } + const nullTypingsInstaller: ITypingsInstaller; interface ProjectServiceOptions { host: ServerHost; logger: Logger;