diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 2caa36f14251e..66c0494e11ffc 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -282,7 +282,7 @@ namespace ts { } // directoryExists - if (originalDirectoryExists && originalCreateDirectory) { + if (originalDirectoryExists) { host.directoryExists = directory => { const key = toPath(directory); const value = directoryExistsCache.get(key); @@ -291,11 +291,14 @@ namespace ts { directoryExistsCache.set(key, !!newValue); return newValue; }; - host.createDirectory = directory => { - const key = toPath(directory); - directoryExistsCache.delete(key); - originalCreateDirectory.call(host, directory); - }; + + if (originalCreateDirectory) { + host.createDirectory = directory => { + const key = toPath(directory); + directoryExistsCache.delete(key); + originalCreateDirectory.call(host, directory); + }; + } } return { diff --git a/src/services/services.ts b/src/services/services.ts index ac731115bebda..00bf4cef73c4d 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -926,14 +926,6 @@ namespace ts { /// Language Service - // Information about a specific host file. - interface HostFileInformation { - hostFileName: string; - version: string; - scriptSnapshot: IScriptSnapshot; - scriptKind: ScriptKind; - } - /* @internal */ export interface DisplayPartsSymbolWriter extends EmitTextWriter { displayParts(): SymbolDisplayPart[]; @@ -987,82 +979,6 @@ namespace ts { return codefix.getSupportedErrorCodes(); } - // Either it will be file name if host doesnt have file or it will be the host's file information - type CachedHostFileInformation = HostFileInformation | string; - - // Cache host information about script Should be refreshed - // at each language service public entry point, since we don't know when - // the set of scripts handled by the host changes. - class HostCache { - private fileNameToEntry: ESMap; - private currentDirectory: string; - - constructor(private host: LanguageServiceHost, getCanonicalFileName: GetCanonicalFileName) { - // script id => script index - this.currentDirectory = host.getCurrentDirectory(); - this.fileNameToEntry = new Map(); - - // Initialize the list with the root file names - const rootFileNames = host.getScriptFileNames(); - tracing?.push(tracing.Phase.Session, "initializeHostCache", { count: rootFileNames.length }); - for (const fileName of rootFileNames) { - this.createEntry(fileName, toPath(fileName, this.currentDirectory, getCanonicalFileName)); - } - tracing?.pop(); - } - - private createEntry(fileName: string, path: Path) { - let entry: CachedHostFileInformation; - const scriptSnapshot = this.host.getScriptSnapshot(fileName); - if (scriptSnapshot) { - entry = { - hostFileName: fileName, - version: this.host.getScriptVersion(fileName), - scriptSnapshot, - scriptKind: getScriptKind(fileName, this.host) - }; - } - else { - entry = fileName; - } - - this.fileNameToEntry.set(path, entry); - return entry; - } - - public getEntryByPath(path: Path): CachedHostFileInformation | undefined { - return this.fileNameToEntry.get(path); - } - - public getHostFileInformation(path: Path): HostFileInformation | undefined { - const entry = this.fileNameToEntry.get(path); - return !isString(entry) ? entry : undefined; - } - - public getOrCreateEntryByPath(fileName: string, path: Path): HostFileInformation { - const info = this.getEntryByPath(path) || this.createEntry(fileName, path); - return isString(info) ? undefined! : info; // TODO: GH#18217 - } - - public getRootFileNames(): string[] { - const names: string[] = []; - this.fileNameToEntry.forEach(entry => { - if (isString(entry)) { - names.push(entry); - } - else { - names.push(entry.hostFileName); - } - }); - return names; - } - - public getScriptSnapshot(path: Path): IScriptSnapshot { - const file = this.getHostFileInformation(path); - return (file && file.scriptSnapshot)!; // TODO: GH#18217 - } - } - class SyntaxTreeCache { // For our syntactic only features, we also keep a cache of the syntax tree for the // currently edited file. @@ -1366,37 +1282,17 @@ namespace ts { lastTypesRootVersion = typeRootsVersion; } + const rootFileNames = host.getScriptFileNames(); + // Get a fresh cache of the host information - let hostCache: HostCache | undefined = new HostCache(host, getCanonicalFileName); - const rootFileNames = hostCache.getRootFileNames(); const newSettings = host.getCompilationSettings() || getDefaultCompilerOptions(); const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse; const hasChangedAutomaticTypeDirectiveNames = maybeBind(host, host.hasChangedAutomaticTypeDirectiveNames); const projectReferences = host.getProjectReferences?.(); let parsedCommandLines: ESMap | undefined; - const parseConfigHost: ParseConfigFileHost = { - useCaseSensitiveFileNames, - fileExists, - readFile, - readDirectory, - trace: maybeBind(host, host.trace), - getCurrentDirectory: () => currentDirectory, - onUnRecoverableConfigFileDiagnostic: noop, - }; - - // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) { - return; - } - - // IMPORTANT - It is critical from this moment onward that we do not check - // cancellation tokens. We are about to mutate source files from a previous program - // instance. If we cancel midway through, we may end up in an inconsistent state where - // the program points to old source files that have been invalidated because of - // incremental parsing. // Now create a new compiler - const compilerHost: CompilerHost = { + let compilerHost: CompilerHost | undefined = { getSourceFile: getOrCreateSourceFile, getSourceFileByPath: getOrCreateSourceFileByPath, getCancellationToken: () => cancellationToken, @@ -1406,8 +1302,8 @@ namespace ts { getDefaultLibFileName: options => host.getDefaultLibFileName(options), writeFile: noop, getCurrentDirectory: () => currentDirectory, - fileExists, - readFile, + fileExists: fileName => host.fileExists(fileName), + readFile: fileName => host.readFile && host.readFile(fileName), getSymlinkCache: maybeBind(host, host.getSymlinkCache), realpath: maybeBind(host, host.realpath), directoryExists: directoryName => { @@ -1416,20 +1312,54 @@ namespace ts { getDirectories: path => { return host.getDirectories ? host.getDirectories(path) : []; }, - readDirectory, + readDirectory: (path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) => { + Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'"); + return host.readDirectory!(path, extensions, exclude, include, depth); + }, onReleaseOldSourceFile, onReleaseParsedCommandLine, hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, - trace: parseConfigHost.trace, + trace: maybeBind(host, host.trace), resolveModuleNames: maybeBind(host, host.resolveModuleNames), getModuleResolutionCache: maybeBind(host, host.getModuleResolutionCache), resolveTypeReferenceDirectives: maybeBind(host, host.resolveTypeReferenceDirectives), useSourceOfProjectReferenceRedirect: maybeBind(host, host.useSourceOfProjectReferenceRedirect), getParsedCommandLine, }; + + const originalGetSourceFile = compilerHost.getSourceFile; + + const { getSourceFileWithCache } = changeCompilerHostLikeToUseCache( + compilerHost, + fileName => toPath(fileName, currentDirectory, getCanonicalFileName), + (...args) => originalGetSourceFile.call(compilerHost, ...args) + ); + compilerHost.getSourceFile = getSourceFileWithCache!; + host.setCompilerHost?.(compilerHost); + const parseConfigHost: ParseConfigFileHost = { + useCaseSensitiveFileNames, + fileExists: fileName => compilerHost!.fileExists(fileName), + readFile: fileName => compilerHost!.readFile(fileName), + readDirectory: (...args) => compilerHost!.readDirectory!(...args), + trace: compilerHost.trace, + getCurrentDirectory: compilerHost.getCurrentDirectory, + onUnRecoverableConfigFileDiagnostic: noop, + }; + + // If the program is already up-to-date, we can reuse it + if (isProgramUptoDate(program, rootFileNames, newSettings, (_path, fileName) => host.getScriptVersion(fileName), fileName => compilerHost!.fileExists(fileName), hasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames, getParsedCommandLine, projectReferences)) { + return; + } + + // IMPORTANT - It is critical from this moment onward that we do not check + // cancellation tokens. We are about to mutate source files from a previous program + // instance. If we cancel midway through, we may end up in an inconsistent state where + // the program points to old source files that have been invalidated because of + // incremental parsing. + const documentRegistryBucketKey = documentRegistry.getKeyForCompilationSettings(newSettings); const options: CreateProgramOptions = { rootNames: rootFileNames, @@ -1440,9 +1370,9 @@ namespace ts { }; program = createProgram(options); - // hostCache is captured in the closure for 'getOrCreateSourceFile' but it should not be used past this point. - // It needs to be cleared to allow all collected snapshots to be released - hostCache = undefined; + // 'getOrCreateSourceFile' depends on caching but should be used past this point. + // After this point, the cache needs to be cleared to allow all collected snapshots to be released + compilerHost = undefined; parsedCommandLines = undefined; // We reset this cache on structure invalidation so we don't hold on to outdated files for long; however we can't use the `compilerHost` above, @@ -1491,29 +1421,6 @@ namespace ts { } } - function fileExists(fileName: string): boolean { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache && hostCache.getEntryByPath(path); - return entry ? - !isString(entry) : - (!!host.fileExists && host.fileExists(fileName)); - } - - function readFile(fileName: string) { - // stub missing host functionality - const path = toPath(fileName, currentDirectory, getCanonicalFileName); - const entry = hostCache && hostCache.getEntryByPath(path); - if (entry) { - return isString(entry) ? undefined : getSnapshotText(entry.scriptSnapshot); - } - return host.readFile && host.readFile(fileName); - } - - function readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number) { - Debug.checkDefined(host.readDirectory, "'LanguageServiceHost.readDirectory' must be implemented to correctly process 'projectReferences'"); - return host.readDirectory!(path, extensions, exclude, include, depth); - } - // Release any files we have acquired in the old program but are // not part of the new program. function onReleaseOldSourceFile(oldSourceFile: SourceFile, oldOptions: CompilerOptions) { @@ -1526,15 +1433,18 @@ namespace ts { } function getOrCreateSourceFileByPath(fileName: string, path: Path, _languageVersion: ScriptTarget, _onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined { - Debug.assert(hostCache !== undefined, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host."); + Debug.assert(compilerHost, "getOrCreateSourceFileByPath called after typical CompilerHost lifetime, check the callstack something with a reference to an old host."); // The program is asking for this file, check first if the host can locate it. // If the host can not locate the file, then it does not exist. return undefined // to the program to allow reporting of errors for missing files. - const hostFileInformation = hostCache && hostCache.getOrCreateEntryByPath(fileName, path); - if (!hostFileInformation) { + const scriptSnapshot = host.getScriptSnapshot(fileName); + if (!scriptSnapshot) { return undefined; } + const scriptKind = getScriptKind(fileName, host); + const scriptVersion = host.getScriptVersion(fileName); + // Check if the language version has changed since we last created a program; if they are the same, // it is safe to reuse the sourceFiles; if not, then the shape of the AST can change, and the oldSourceFile // can not be reused. we have to dump all syntax trees and create new ones. @@ -1567,8 +1477,8 @@ namespace ts { // We do not support the scenario where a host can modify a registered // file's script kind, i.e. in one project some file is treated as ".ts" // and in another as ".js" - if (hostFileInformation.scriptKind === oldSourceFile.scriptKind) { - return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); + if (scriptKind === oldSourceFile.scriptKind) { + return documentRegistry.updateDocumentWithKey(fileName, path, host, documentRegistryBucketKey, scriptSnapshot, scriptVersion, scriptKind); } else { // Release old source file and fall through to aquire new file with new script kind @@ -1580,7 +1490,7 @@ namespace ts { } // Could not find this file in the old program, create a new SourceFile for it. - return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, hostFileInformation.scriptSnapshot, hostFileInformation.version, hostFileInformation.scriptKind); + return documentRegistry.acquireDocumentWithKey(fileName, path, host, documentRegistryBucketKey, scriptSnapshot, scriptVersion, scriptKind); } }