diff --git a/src/compiler/builder.ts b/src/compiler/builder.ts index 489111f26db..c0af4f5b13d 100644 --- a/src/compiler/builder.ts +++ b/src/compiler/builder.ts @@ -22,7 +22,7 @@ namespace ts { /** * This is the callback when file infos in the builder are updated */ - onProgramUpdateGraph(program: Program): void; + onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void; getFilesAffectedBy(program: Program, path: Path): string[]; emitFile(program: Program, path: Path): EmitOutput; emitChangedFiles(program: Program): EmitOutputDetailed[]; @@ -84,7 +84,7 @@ namespace ts { clear }; - function createProgramGraph(program: Program) { + function createProgramGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) { const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None; if (isModuleEmit !== currentIsModuleEmit) { isModuleEmit = currentIsModuleEmit; @@ -100,7 +100,7 @@ namespace ts { // Remove existing file info removeExistingFileInfo, // We will update in place instead of deleting existing value and adding new one - (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile) + (existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution) ); } @@ -115,8 +115,8 @@ namespace ts { emitHandler.removeScriptInfo(path); } - function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) { - if (existingInfo.version !== sourceFile.version) { + function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) { + if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) { changedFilesSinceLastEmit.set(sourceFile.path, true); existingInfo.version = sourceFile.version; emitHandler.updateScriptInfo(program, sourceFile); @@ -125,13 +125,13 @@ namespace ts { function ensureProgramGraph(program: Program) { if (!emitHandler) { - createProgramGraph(program); + createProgramGraph(program, noop); } } - function onProgramUpdateGraph(program: Program) { + function onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) { if (emitHandler) { - createProgramGraph(program); + createProgramGraph(program, hasInvalidatedResolution); } } @@ -298,8 +298,6 @@ namespace ts { return result; } - function noop() { } - function getNonModuleEmitHandler(): EmitHandler { return { addScriptInfo: noop, diff --git a/src/compiler/core.ts b/src/compiler/core.ts index 2a41427f2fd..813d805adea 100644 --- a/src/compiler/core.ts +++ b/src/compiler/core.ts @@ -1220,7 +1220,7 @@ namespace ts { } /** Does nothing. */ - export function noop(): void {} + export function noop(): any {} /** Throws an error because a function is not implemented. */ export function notImplemented(): never { diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 027d7ca2025..3258bba6656 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -768,6 +768,8 @@ namespace ts { return !host.directoryExists || host.directoryExists(directoryName); } + export type HasInvalidatedResolution = (sourceFile: Path) => boolean; + /** * @param {boolean} onlyRecordFailures - if true then function won't try to actually load files but instead record all attempts as failures. This flag is necessary * in cases when we know upfront that all load attempts will fail (because containing folder does not exists) however we still need to record all failed lookup locations. diff --git a/src/compiler/program.ts b/src/compiler/program.ts index 8ce51ac5e8c..58931379d7c 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -394,7 +394,7 @@ namespace ts { } export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, - getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean): boolean { + getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution): boolean { // If we haven't create a program yet, then it is not up-to-date if (!program) { return false; @@ -432,10 +432,9 @@ namespace ts { return true; function sourceFileUpToDate(sourceFile: SourceFile): boolean { - if (!sourceFile) { - return false; - } - return sourceFile.version === getSourceVersion(sourceFile.path); + return sourceFile && + sourceFile.version === getSourceVersion(sourceFile.path) && + !hasInvalidatedResolution(sourceFile.path); } } @@ -565,6 +564,7 @@ namespace ts { let moduleResolutionCache: ModuleResolutionCache; let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[]; + const hasInvalidatedResolution = host.hasInvalidatedResolution || noop; if (host.resolveModuleNames) { resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile).map(resolved => { // An older host may have omitted extension, in which case we should infer it from the file extension of resolvedFileName. @@ -803,7 +803,7 @@ namespace ts { trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile); } } - else { + else if (!hasInvalidatedResolution(oldProgramState.file.path)) { resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState); } @@ -962,6 +962,13 @@ namespace ts { // tentatively approve the file modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); } + else if (hasInvalidatedResolution(oldSourceFile.path)) { + // 'module/types' references could have changed + oldProgram.structureIsReused = StructureIsReused.SafeModules; + + // add file to the modified list so that we will resolve it later + modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile }); + } // if file has passed all checks it should be safe to reuse it newSourceFiles.push(newSourceFile); diff --git a/src/compiler/resolutionCache.ts b/src/compiler/resolutionCache.ts index 6ca318f7183..b6f3e7547f1 100644 --- a/src/compiler/resolutionCache.ts +++ b/src/compiler/resolutionCache.ts @@ -4,12 +4,18 @@ 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; invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string): void; + + createHasInvalidatedResolution(): HasInvalidatedResolution; + clear(): void; } @@ -40,6 +46,7 @@ namespace ts { let host: ModuleResolutionHost; let filesWithChangedSetOfUnresolvedImports: Path[]; + let filesWithInvalidatedResolutions: Map; const resolvedModuleNames = createMap>(); const resolvedTypeReferenceDirectives = createMap>(); @@ -55,6 +62,7 @@ namespace ts { resolveTypeReferenceDirectives, invalidateResolutionOfDeletedFile, invalidateResolutionOfChangedFailedLookupLocation, + createHasInvalidatedResolution, clear }; @@ -82,6 +90,12 @@ namespace ts { return collected; } + function createHasInvalidatedResolution(): HasInvalidatedResolution { + const collected = filesWithInvalidatedResolutions; + filesWithInvalidatedResolutions = undefined; + return path => collected && collected.has(path); + } + 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 @@ -250,7 +264,7 @@ namespace ts { cache: Map>, getResult: (s: T) => R, getResultFileName: (result: R) => string | undefined) { - cache.forEach((value, path) => { + cache.forEach((value, path: Path) => { if (path === deletedFilePath) { cache.delete(path); value.forEach((resolution, name) => { @@ -264,6 +278,7 @@ namespace ts { if (result) { if (getResultFileName(result) === deletedFilePath) { resolution.isInvalidated = true; + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(path, true); } } } @@ -275,14 +290,13 @@ namespace ts { function invalidateResolutionCacheOfChangedFailedLookupLocation( failedLookupLocation: string, cache: Map>) { - cache.forEach((value, _containingFilePath) => { + cache.forEach((value, containingFile: Path) => { if (value) { value.forEach((resolution, __name) => { if (resolution && !resolution.isInvalidated && contains(resolution.failedLookupLocations, failedLookupLocation)) { - // TODO: mark the file as needing re-evaluation of module resolution instead of using it blindly. - // Note: Right now this invalidation path is not used at all as it doesnt matter as we are anyways clearing the program, - // which means all the resolutions will be discarded. + // Mark the file as needing re-evaluation of module resolution instead of using it blindly. resolution.isInvalidated = true; + (filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap())).set(containingFile, true); } }); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 45f5a16a80c..c1ab964af30 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3979,6 +3979,7 @@ namespace ts { resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; getEnvironmentVariable?(name: string): string; onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void; + hasInvalidatedResolution?: HasInvalidatedResolution; } /* @internal */ diff --git a/src/compiler/watchedProgram.ts b/src/compiler/watchedProgram.ts index c668d4a1510..8628b6d049b 100644 --- a/src/compiler/watchedProgram.ts +++ b/src/compiler/watchedProgram.ts @@ -256,6 +256,7 @@ namespace ts { const sourceFilesCache = createMap(); // Cache that stores the source file and version info let missingFilePathsRequestedForRelease: Path[]; // These paths are held temparirly so that we can remove the entry from source file cache if the file is not tracked by missing files + let hasInvalidatedResolution: HasInvalidatedResolution; // Passed along to see if source file has invalidated resolutions watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty); const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost; @@ -292,7 +293,8 @@ namespace ts { function synchronizeProgram() { writeLog(`Synchronizing program`); - if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists)) { + hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution(); + if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution)) { return; } @@ -306,7 +308,7 @@ namespace ts { // Compile the program program = createProgram(rootFileNames, compilerOptions, compilerHost, program); - builder.onProgramUpdateGraph(program); + builder.onProgramUpdateGraph(program, hasInvalidatedResolution); // Update watches missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher); @@ -351,7 +353,8 @@ namespace ts { realpath, resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile), resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false), - onReleaseOldSourceFile + onReleaseOldSourceFile, + hasInvalidatedResolution }; } @@ -569,13 +572,7 @@ namespace ts { writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`); const path = toPath(failedLookupLocation); updateCachedSystem(failedLookupLocation, path); - - // TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file - // refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions. - // For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again. - - // resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); - program = undefined; + resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); scheduleProgramUpdate(); } diff --git a/src/server/lsHost.ts b/src/server/lsHost.ts index 274c3c3aef6..f4cbd402604 100644 --- a/src/server/lsHost.ts +++ b/src/server/lsHost.ts @@ -110,6 +110,10 @@ namespace ts.server { readonly trace: (s: string) => void; readonly realpath?: (path: string) => string; + + /*@internal*/ + hasInvalidatedResolution: HasInvalidatedResolution; + /** * This is the host that is associated with the project. This is normally same as projectService's host * except in Configured projects where it is CachedServerHost so that we can cache the results of the diff --git a/src/server/project.ts b/src/server/project.ts index e600c3796e6..5ea54aca54e 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -241,12 +241,8 @@ namespace ts.server { if (this.projectKind === ProjectKind.Configured) { (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(failedLookupLocation)); } - this.updateTypes(); - // TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file - // refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions. - // For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again. - // this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); - // this.markAsDirty(); + this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation); + this.markAsDirty(); this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); }); } @@ -605,6 +601,7 @@ namespace ts.server { */ updateGraph(): boolean { this.resolutionCache.startRecordingFilesWithChangedResolutions(); + this.lsHost.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution(); let hasChanges = this.updateGraphWorker(); @@ -640,7 +637,7 @@ namespace ts.server { // otherwise tell it to drop its internal state if (this.builder) { if (this.languageServiceEnabled && this.compileOnSaveEnabled) { - this.builder.onProgramUpdateGraph(this.program); + this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution); } else { this.builder.clear(); diff --git a/src/services/services.ts b/src/services/services.ts index 40a446e0967..14b5ddd85c4 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1115,8 +1115,11 @@ namespace ts { // Get a fresh cache of the host information let hostCache = new HostCache(host, getCanonicalFileName); const rootFileNames = hostCache.getRootFileNames(); + + const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || noop; + // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists)) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution)) { return; } @@ -1155,7 +1158,8 @@ namespace ts { getDirectories: path => { return host.getDirectories ? host.getDirectories(path) : []; }, - onReleaseOldSourceFile + onReleaseOldSourceFile, + hasInvalidatedResolution }; if (host.trace) { compilerHost.trace = message => host.trace(message); diff --git a/src/services/types.ts b/src/services/types.ts index e17df08378a..018e8941765 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -185,6 +185,7 @@ namespace ts { */ resolveModuleNames?(moduleNames: string[], containingFile: string): ResolvedModule[]; resolveTypeReferenceDirectives?(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[]; + hasInvalidatedResolution?: HasInvalidatedResolution; directoryExists?(directoryName: string): boolean; /*