diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts new file mode 100644 index 0000000000..f92464e5a3 --- /dev/null +++ b/src/compiler/resolutionCache.ts @@ -0,0 +1,198 @@ +/// +/// + +namespace ts { + export interface ResolutionCache { + setModuleResolutionHost(host: ModuleResolutionHost): void; + startRecordingFilesWithChangedResolutions(): void; + finishRecordingFilesWithChangedResolutions(): Path[]; + resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[]; + resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; + invalidateResolutionOfDeletedFile(filePath: Path): void; + clear(): void; + } + + type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; + type ResolverWithGlobalCache = (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost) => ResolvedModuleWithFailedLookupLocations | undefined; + + /*@internal*/ + export function resolveWithGlobalCache(primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string | undefined, projectName: string): ResolvedModuleWithFailedLookupLocations | undefined { + if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) { + // otherwise try to load typings from @types + + // create different collection of failed lookup locations for second pass + // if it will fail and we've already found something during the first pass - we don't want to pollute its results + const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache); + if (resolvedModule) { + return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) }; + } + } + } + + /*@internal*/ + export function createResolutionCache( + toPath: (fileName: string) => Path, + getCompilerOptions: () => CompilerOptions, + resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache { + + let host: ModuleResolutionHost; + let filesWithChangedSetOfUnresolvedImports: Path[]; + const resolvedModuleNames = createMap>(); + const resolvedTypeReferenceDirectives = createMap>(); + + return { + setModuleResolutionHost, + startRecordingFilesWithChangedResolutions, + finishRecordingFilesWithChangedResolutions, + resolveModuleNames, + resolveTypeReferenceDirectives, + invalidateResolutionOfDeletedFile, + clear + }; + + function setModuleResolutionHost(updatedHost: ModuleResolutionHost) { + host = updatedHost; + } + + function clear() { + resolvedModuleNames.clear(); + resolvedTypeReferenceDirectives.clear(); + } + + function startRecordingFilesWithChangedResolutions() { + filesWithChangedSetOfUnresolvedImports = []; + } + + function finishRecordingFilesWithChangedResolutions() { + const collected = filesWithChangedSetOfUnresolvedImports; + filesWithChangedSetOfUnresolvedImports = undefined; + return collected; + } + + function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations { + const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host); + // return result immediately only if it is .ts, .tsx or .d.ts + // otherwise try to load typings from @types + return (resolveWithGlobalCache && resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host)) || primaryResult; + } + + function resolveNamesWithLocalCache( + names: string[], + containingFile: string, + cache: Map>, + loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, + getResult: (s: T) => R, + getResultFileName: (result: R) => string | undefined, + logChanges: boolean): R[] { + + const path = toPath(containingFile); + const currentResolutionsInFile = cache.get(path); + + const newResolutions: Map = createMap(); + const resolvedModules: R[] = []; + const compilerOptions = getCompilerOptions(); + + for (const name of names) { + // check if this is a duplicate entry in the list + let resolution = newResolutions.get(name); + if (!resolution) { + const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name); + if (moduleResolutionIsValid(existingResolution)) { + // ok, it is safe to use existing name resolution results + resolution = existingResolution; + } + else { + resolution = loader(name, containingFile, compilerOptions, host); + newResolutions.set(name, resolution); + } + if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { + filesWithChangedSetOfUnresolvedImports.push(path); + // reset log changes to avoid recording the same file multiple times + logChanges = false; + } + } + + Debug.assert(resolution !== undefined); + + resolvedModules.push(getResult(resolution)); + } + + // replace old results with a new one + cache.set(path, newResolutions); + return resolvedModules; + + function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean { + if (oldResolution === newResolution) { + return true; + } + if (!oldResolution || !newResolution || oldResolution.isInvalidated) { + return false; + } + const oldResult = getResult(oldResolution); + const newResult = getResult(newResolution); + if (oldResult === newResult) { + return true; + } + if (!oldResult || !newResult) { + return false; + } + return getResultFileName(oldResult) === getResultFileName(newResult); + } + + function moduleResolutionIsValid(resolution: T): boolean { + if (!resolution || resolution.isInvalidated) { + return false; + } + + const result = getResult(resolution); + if (result) { + return true; + } + + // consider situation if we have no candidate locations as valid resolution. + // after all there is no point to invalidate it if we have no idea where to look for the module. + return resolution.failedLookupLocations.length === 0; + } + } + + + function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { + return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, + m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false); + } + + function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] { + return resolveNamesWithLocalCache(moduleNames, containingFile, resolvedModuleNames, resolveModuleName, + m => m.resolvedModule, r => r.resolvedFileName, logChanges); + } + + function invalidateResolutionCacheOfDeletedFile( + deletedFilePath: Path, + cache: Map>, + getResult: (s: T) => R, + getResultFileName: (result: R) => string | undefined) { + cache.forEach((value, path) => { + if (path === deletedFilePath) { + cache.delete(path); + } + else if (value) { + value.forEach((resolution) => { + if (resolution && !resolution.isInvalidated) { + const result = getResult(resolution); + if (result) { + if (getResultFileName(result) === deletedFilePath) { + resolution.isInvalidated = true; + } + } + } + }); + } + }); + } + + function invalidateResolutionOfDeletedFile(filePath: Path) { + invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName); + invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); + } + } +} diff --git a/src/compiler/tsconfig.json b/src/compiler/tsconfig.json index 08d50e5eb8..1001f51f33 100644 --- a/src/compiler/tsconfig.json +++ b/src/compiler/tsconfig.json @@ -37,6 +37,7 @@ "emitter.ts", "program.ts", "builder.ts", + "resolutionCache.ts", "watchedProgram.ts", "commandLineParser.ts", "tsc.ts", diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index 22625282a8..8bf5c54245 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -1,5 +1,6 @@ /// /// +/// namespace ts { export type DiagnosticReporter = (diagnostic: Diagnostic) => void; @@ -254,6 +255,7 @@ namespace ts { let timerToUpdateProgram: any; // timer callback to recompile the program const sourceFilesCache = createMap(); // Cache that stores the source file and version info + watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -268,6 +270,9 @@ namespace ts { const currentDirectory = host.getCurrentDirectory(); const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames); + // Cache for the module resolution + const resolutionCache = createResolutionCache(fileName => toPath(fileName), () => compilerOptions); + // There is no extra check needed since we can just rely on the program to decide emit const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true); @@ -287,6 +292,10 @@ namespace ts { // Create the compiler host const compilerHost = createWatchedCompilerHost(compilerOptions); + resolutionCache.setModuleResolutionHost(compilerHost); + if (changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) { + resolutionCache.clear(); + } beforeCompile(compilerOptions); // Compile the program @@ -321,22 +330,18 @@ namespace ts { getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "", getDirectories: (path: string) => host.getDirectories(path), realpath, - onReleaseOldSourceFile, + resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile), + resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false), + onReleaseOldSourceFile }; + } - // TODO: cache module resolution - // if (host.resolveModuleNames) { - // compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile); - // } - // if (host.resolveTypeReferenceDirectives) { - // compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => { - // return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile); - // }; - // } + function toPath(fileName: string) { + return ts.toPath(fileName, currentDirectory, getCanonicalFileName); } function fileExists(fileName: string) { - const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const path = toPath(fileName); const hostSourceFileInfo = sourceFilesCache.get(path); if (hostSourceFileInfo !== undefined) { return !isString(hostSourceFileInfo); @@ -350,7 +355,7 @@ namespace ts { } function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { - return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile); + return getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile); } function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile { @@ -418,6 +423,7 @@ namespace ts { if (hostSourceFile !== undefined) { if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); + resolutionCache.invalidateResolutionOfDeletedFile(path); } sourceFilesCache.delete(path); } @@ -501,6 +507,7 @@ namespace ts { if (hostSourceFile) { // Update the cache if (eventKind === FileWatcherEventKind.Deleted) { + resolutionCache.invalidateResolutionOfDeletedFile(path); if (!isString(hostSourceFile)) { hostSourceFile.fileWatcher.close(); sourceFilesCache.set(path, (hostSourceFile.version++).toString()); @@ -574,7 +581,7 @@ namespace ts { function onFileAddOrRemoveInWatchedDirectory(fileName: string) { Debug.assert(!!configFileName); - const path = toPath(fileName, currentDirectory, getCanonicalFileName); + const path = toPath(fileName); // Since the file existance changed, update the sourceFiles cache updateCachedSystem(fileName, path); diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 692547e491..274c3c3aef 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -1,6 +1,7 @@ /// /// /// +/// namespace ts.server { export class CachedServerHost implements ServerHost { @@ -103,15 +104,10 @@ namespace ts.server { } - type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean }; export class LSHost implements LanguageServiceHost, ModuleResolutionHost { - private compilationSettings: CompilerOptions; - private readonly resolvedModuleNames = createMap>(); - private readonly resolvedTypeReferenceDirectives = createMap>(); + /*@internal*/ + compilationSettings: CompilerOptions; - private filesWithChangedSetOfUnresolvedImports: Path[]; - - private resolveModuleName: typeof resolveModuleName; readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; /** @@ -130,25 +126,6 @@ namespace ts.server { this.trace = s => host.trace(s); } - this.resolveModuleName = (moduleName, containingFile, compilerOptions, host) => { - const globalCache = this.project.getTypeAcquisition().enable - ? this.project.projectService.typingsInstaller.globalTypingsCacheLocation - : undefined; - const primaryResult = resolveModuleName(moduleName, containingFile, compilerOptions, host); - // return result immediately only if it is .ts, .tsx or .d.ts - if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) { - // otherwise try to load typings from @types - - // create different collection of failed lookup locations for second pass - // if it will fail and we've already found something during the first pass - we don't want to pollute its results - const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, this.project.getProjectName(), compilerOptions, host, globalCache); - if (resolvedModule) { - return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) }; - } - } - return primaryResult; - }; - if (this.host.realpath) { this.realpath = path => this.host.realpath(path); } @@ -156,99 +133,9 @@ namespace ts.server { dispose() { this.project = undefined; - this.resolveModuleName = undefined; this.host = undefined; } - public startRecordingFilesWithChangedResolutions() { - this.filesWithChangedSetOfUnresolvedImports = []; - } - - public finishRecordingFilesWithChangedResolutions() { - const collected = this.filesWithChangedSetOfUnresolvedImports; - this.filesWithChangedSetOfUnresolvedImports = undefined; - return collected; - } - - private resolveNamesWithLocalCache( - names: string[], - containingFile: string, - cache: Map>, - loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined, - logChanges: boolean): R[] { - - const path = this.project.projectService.toPath(containingFile); - const currentResolutionsInFile = cache.get(path); - - const newResolutions: Map = createMap(); - const resolvedModules: R[] = []; - const compilerOptions = this.getCompilationSettings(); - - for (const name of names) { - // check if this is a duplicate entry in the list - let resolution = newResolutions.get(name); - if (!resolution) { - const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name); - if (moduleResolutionIsValid(existingResolution)) { - // ok, it is safe to use existing name resolution results - resolution = existingResolution; - } - else { - resolution = loader(name, containingFile, compilerOptions, this); - newResolutions.set(name, resolution); - } - if (logChanges && this.filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) { - this.filesWithChangedSetOfUnresolvedImports.push(path); - // reset log changes to avoid recording the same file multiple times - logChanges = false; - } - } - - Debug.assert(resolution !== undefined); - - resolvedModules.push(getResult(resolution)); - } - - // replace old results with a new one - cache.set(path, newResolutions); - return resolvedModules; - - function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean { - if (oldResolution === newResolution) { - return true; - } - if (!oldResolution || !newResolution || oldResolution.isInvalidated) { - return false; - } - const oldResult = getResult(oldResolution); - const newResult = getResult(newResolution); - if (oldResult === newResult) { - return true; - } - if (!oldResult || !newResult) { - return false; - } - return getResultFileName(oldResult) === getResultFileName(newResult); - } - - function moduleResolutionIsValid(resolution: T): boolean { - if (!resolution || resolution.isInvalidated) { - return false; - } - - const result = getResult(resolution); - if (result) { - return true; - } - - // consider situation if we have no candidate locations as valid resolution. - // after all there is no point to invalidate it if we have no idea where to look for the module. - return resolution.failedLookupLocations.length === 0; - } - } - getNewLine() { return this.host.newLine; } @@ -270,13 +157,11 @@ namespace ts.server { } resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] { - return this.resolveNamesWithLocalCache(typeDirectiveNames, containingFile, this.resolvedTypeReferenceDirectives, resolveTypeReferenceDirective, - m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false); + return this.project.resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile); } resolveModuleNames(moduleNames: string[], containingFile: string): ResolvedModuleFull[] { - return this.resolveNamesWithLocalCache(moduleNames, containingFile, this.resolvedModuleNames, this.resolveModuleName, - m => m.resolvedModule, r => r.resolvedFileName, /*logChanges*/ true); + return this.project.resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ true); } getDefaultLibFileName() { @@ -339,44 +224,5 @@ namespace ts.server { getDirectories(path: string): string[] { return this.host.getDirectories(path); } - - notifyFileRemoved(info: ScriptInfo) { - this.invalidateResolutionOfDeletedFile(info, this.resolvedModuleNames, - m => m.resolvedModule, r => r.resolvedFileName); - this.invalidateResolutionOfDeletedFile(info, this.resolvedTypeReferenceDirectives, - m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName); - } - - private invalidateResolutionOfDeletedFile( - deletedInfo: ScriptInfo, - cache: Map>, - getResult: (s: T) => R, - getResultFileName: (result: R) => string | undefined) { - cache.forEach((value, path) => { - if (path === deletedInfo.path) { - cache.delete(path); - } - else if (value) { - value.forEach((resolution) => { - if (resolution && !resolution.isInvalidated) { - const result = getResult(resolution); - if (result) { - if (getResultFileName(result) === deletedInfo.path) { - resolution.isInvalidated = true; - } - } - } - }); - } - }); - } - - setCompilationSettings(opt: CompilerOptions) { - if (changesAffectModuleResolution(this.compilationSettings, opt)) { - this.resolvedModuleNames.clear(); - this.resolvedTypeReferenceDirectives.clear(); - } - this.compilationSettings = opt; - } } } diff --git a/src/server/project.ts b/src/server/project.ts index 02e2fb2075..ebad00e03b 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -127,6 +127,9 @@ namespace ts.server { public languageServiceEnabled = true; + /*@internal*/ + resolutionCache: ResolutionCache; + /*@internal*/ lsHost: LSHost; @@ -211,7 +214,14 @@ namespace ts.server { this.setInternalCompilerOptionsForEmittingJsFiles(); this.lsHost = new LSHost(host, this, this.projectService.cancellationToken); - this.lsHost.setCompilationSettings(this.compilerOptions); + this.resolutionCache = createResolutionCache( + fileName => this.projectService.toPath(fileName), + () => this.compilerOptions, + (primaryResult, moduleName, compilerOptions, host) => resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host, + this.getTypeAcquisition().enable ? this.projectService.typingsInstaller.globalTypingsCacheLocation : undefined, this.getProjectName()) + ); + this.lsHost.compilationSettings = this.compilerOptions; + this.resolutionCache.setModuleResolutionHost(this.lsHost); this.languageService = createLanguageService(this.lsHost, this.documentRegistry); @@ -349,6 +359,7 @@ namespace ts.server { this.rootFilesMap = undefined; this.program = undefined; this.builder = undefined; + this.resolutionCache = undefined; this.cachedUnresolvedImportsPerFile = undefined; this.projectErrors = undefined; this.lsHost.dispose(); @@ -518,7 +529,7 @@ namespace ts.server { if (this.isRoot(info)) { this.removeRoot(info); } - this.lsHost.notifyFileRemoved(info); + this.resolutionCache.invalidateResolutionOfDeletedFile(info.path); this.cachedUnresolvedImportsPerFile.remove(info.path); if (detachFromProject) { @@ -573,11 +584,11 @@ namespace ts.server { * @returns: true if set of files in the project stays the same and false - otherwise. */ updateGraph(): boolean { - this.lsHost.startRecordingFilesWithChangedResolutions(); + this.resolutionCache.startRecordingFilesWithChangedResolutions(); let hasChanges = this.updateGraphWorker(); - const changedFiles: ReadonlyArray = this.lsHost.finishRecordingFilesWithChangedResolutions() || emptyArray; + const changedFiles: ReadonlyArray = this.resolutionCache.finishRecordingFilesWithChangedResolutions() || emptyArray; for (const file of changedFiles) { // delete cached information for changed files @@ -759,9 +770,13 @@ namespace ts.server { this.cachedUnresolvedImportsPerFile.clear(); this.lastCachedUnresolvedImportsList = undefined; } + const oldOptions = this.compilerOptions; this.compilerOptions = compilerOptions; this.setInternalCompilerOptionsForEmittingJsFiles(); - this.lsHost.setCompilationSettings(compilerOptions); + if (changesAffectModuleResolution(oldOptions, compilerOptions)) { + this.resolutionCache.clear(); + } + this.lsHost.compilationSettings = this.compilerOptions; this.markAsDirty(); }