Skip to content

Commit 7474ba7

Browse files
committed
Implementation for invalidating source file containing possibly changed module resolution
1 parent 8dc6248 commit 7474ba7

11 files changed

Lines changed: 66 additions & 41 deletions

File tree

src/compiler/builder.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ namespace ts {
2222
/**
2323
* This is the callback when file infos in the builder are updated
2424
*/
25-
onProgramUpdateGraph(program: Program): void;
25+
onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void;
2626
getFilesAffectedBy(program: Program, path: Path): string[];
2727
emitFile(program: Program, path: Path): EmitOutput;
2828
emitChangedFiles(program: Program): EmitOutputDetailed[];
@@ -84,7 +84,7 @@ namespace ts {
8484
clear
8585
};
8686

87-
function createProgramGraph(program: Program) {
87+
function createProgramGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
8888
const currentIsModuleEmit = program.getCompilerOptions().module !== ModuleKind.None;
8989
if (isModuleEmit !== currentIsModuleEmit) {
9090
isModuleEmit = currentIsModuleEmit;
@@ -100,7 +100,7 @@ namespace ts {
100100
// Remove existing file info
101101
removeExistingFileInfo,
102102
// We will update in place instead of deleting existing value and adding new one
103-
(existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile)
103+
(existingInfo, sourceFile) => updateExistingFileInfo(program, existingInfo, sourceFile, hasInvalidatedResolution)
104104
);
105105
}
106106

@@ -115,8 +115,8 @@ namespace ts {
115115
emitHandler.removeScriptInfo(path);
116116
}
117117

118-
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile) {
119-
if (existingInfo.version !== sourceFile.version) {
118+
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) {
119+
if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) {
120120
changedFilesSinceLastEmit.set(sourceFile.path, true);
121121
existingInfo.version = sourceFile.version;
122122
emitHandler.updateScriptInfo(program, sourceFile);
@@ -125,13 +125,13 @@ namespace ts {
125125

126126
function ensureProgramGraph(program: Program) {
127127
if (!emitHandler) {
128-
createProgramGraph(program);
128+
createProgramGraph(program, noop);
129129
}
130130
}
131131

132-
function onProgramUpdateGraph(program: Program) {
132+
function onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution) {
133133
if (emitHandler) {
134-
createProgramGraph(program);
134+
createProgramGraph(program, hasInvalidatedResolution);
135135
}
136136
}
137137

@@ -298,8 +298,6 @@ namespace ts {
298298
return result;
299299
}
300300

301-
function noop() { }
302-
303301
function getNonModuleEmitHandler(): EmitHandler {
304302
return {
305303
addScriptInfo: noop,

src/compiler/core.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ namespace ts {
12201220
}
12211221

12221222
/** Does nothing. */
1223-
export function noop(): void {}
1223+
export function noop(): any {}
12241224

12251225
/** Throws an error because a function is not implemented. */
12261226
export function notImplemented(): never {

src/compiler/moduleNameResolver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,8 @@ namespace ts {
768768
return !host.directoryExists || host.directoryExists(directoryName);
769769
}
770770

771+
export type HasInvalidatedResolution = (sourceFile: Path) => boolean;
772+
771773
/**
772774
* @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
773775
* 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.

src/compiler/program.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,7 @@ namespace ts {
394394
}
395395

396396
export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions,
397-
getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean): boolean {
397+
getSourceVersion: (path: Path) => string, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution): boolean {
398398
// If we haven't create a program yet, then it is not up-to-date
399399
if (!program) {
400400
return false;
@@ -432,10 +432,9 @@ namespace ts {
432432
return true;
433433

434434
function sourceFileUpToDate(sourceFile: SourceFile): boolean {
435-
if (!sourceFile) {
436-
return false;
437-
}
438-
return sourceFile.version === getSourceVersion(sourceFile.path);
435+
return sourceFile &&
436+
sourceFile.version === getSourceVersion(sourceFile.path) &&
437+
!hasInvalidatedResolution(sourceFile.path);
439438
}
440439
}
441440

@@ -565,6 +564,7 @@ namespace ts {
565564

566565
let moduleResolutionCache: ModuleResolutionCache;
567566
let resolveModuleNamesWorker: (moduleNames: string[], containingFile: string) => ResolvedModuleFull[];
567+
const hasInvalidatedResolution = host.hasInvalidatedResolution || noop;
568568
if (host.resolveModuleNames) {
569569
resolveModuleNamesWorker = (moduleNames, containingFile) => host.resolveModuleNames(checkAllDefined(moduleNames), containingFile).map(resolved => {
570570
// 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 {
803803
trace(host, Diagnostics.Module_0_was_resolved_as_locally_declared_ambient_module_in_file_1, moduleName, containingFile);
804804
}
805805
}
806-
else {
806+
else if (!hasInvalidatedResolution(oldProgramState.file.path)) {
807807
resolvesToAmbientModuleInNonModifiedFile = moduleNameResolvesToAmbientModuleInNonModifiedFile(moduleName, oldProgramState);
808808
}
809809

@@ -962,6 +962,13 @@ namespace ts {
962962
// tentatively approve the file
963963
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
964964
}
965+
else if (hasInvalidatedResolution(oldSourceFile.path)) {
966+
// 'module/types' references could have changed
967+
oldProgram.structureIsReused = StructureIsReused.SafeModules;
968+
969+
// add file to the modified list so that we will resolve it later
970+
modifiedSourceFiles.push({ oldFile: oldSourceFile, newFile: newSourceFile });
971+
}
965972

966973
// if file has passed all checks it should be safe to reuse it
967974
newSourceFiles.push(newSourceFile);

src/compiler/resolutionCache.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@
44
namespace ts {
55
export interface ResolutionCache {
66
setModuleResolutionHost(host: ModuleResolutionHost): void;
7+
78
startRecordingFilesWithChangedResolutions(): void;
89
finishRecordingFilesWithChangedResolutions(): Path[];
10+
911
resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[];
1012
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
13+
1114
invalidateResolutionOfDeletedFile(filePath: Path): void;
1215
invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation: string): void;
16+
17+
createHasInvalidatedResolution(): HasInvalidatedResolution;
18+
1319
clear(): void;
1420
}
1521

@@ -40,6 +46,7 @@ namespace ts {
4046

4147
let host: ModuleResolutionHost;
4248
let filesWithChangedSetOfUnresolvedImports: Path[];
49+
let filesWithInvalidatedResolutions: Map<true>;
4350

4451
const resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
4552
const resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
@@ -55,6 +62,7 @@ namespace ts {
5562
resolveTypeReferenceDirectives,
5663
invalidateResolutionOfDeletedFile,
5764
invalidateResolutionOfChangedFailedLookupLocation,
65+
createHasInvalidatedResolution,
5866
clear
5967
};
6068

@@ -82,6 +90,12 @@ namespace ts {
8290
return collected;
8391
}
8492

93+
function createHasInvalidatedResolution(): HasInvalidatedResolution {
94+
const collected = filesWithInvalidatedResolutions;
95+
filesWithInvalidatedResolutions = undefined;
96+
return path => collected && collected.has(path);
97+
}
98+
8599
function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
86100
const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host);
87101
// return result immediately only if it is .ts, .tsx or .d.ts
@@ -250,7 +264,7 @@ namespace ts {
250264
cache: Map<Map<T>>,
251265
getResult: (s: T) => R,
252266
getResultFileName: (result: R) => string | undefined) {
253-
cache.forEach((value, path) => {
267+
cache.forEach((value, path: Path) => {
254268
if (path === deletedFilePath) {
255269
cache.delete(path);
256270
value.forEach((resolution, name) => {
@@ -264,6 +278,7 @@ namespace ts {
264278
if (result) {
265279
if (getResultFileName(result) === deletedFilePath) {
266280
resolution.isInvalidated = true;
281+
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(path, true);
267282
}
268283
}
269284
}
@@ -275,14 +290,13 @@ namespace ts {
275290
function invalidateResolutionCacheOfChangedFailedLookupLocation<T extends NameResolutionWithFailedLookupLocations>(
276291
failedLookupLocation: string,
277292
cache: Map<Map<T>>) {
278-
cache.forEach((value, _containingFilePath) => {
293+
cache.forEach((value, containingFile: Path) => {
279294
if (value) {
280295
value.forEach((resolution, __name) => {
281296
if (resolution && !resolution.isInvalidated && contains(resolution.failedLookupLocations, failedLookupLocation)) {
282-
// TODO: mark the file as needing re-evaluation of module resolution instead of using it blindly.
283-
// Note: Right now this invalidation path is not used at all as it doesnt matter as we are anyways clearing the program,
284-
// which means all the resolutions will be discarded.
297+
// Mark the file as needing re-evaluation of module resolution instead of using it blindly.
285298
resolution.isInvalidated = true;
299+
(filesWithInvalidatedResolutions || (filesWithInvalidatedResolutions = createMap<true>())).set(containingFile, true);
286300
}
287301
});
288302
}

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3979,6 +3979,7 @@ namespace ts {
39793979
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
39803980
getEnvironmentVariable?(name: string): string;
39813981
onReleaseOldSourceFile?(oldSourceFile: SourceFile, oldOptions: CompilerOptions): void;
3982+
hasInvalidatedResolution?: HasInvalidatedResolution;
39823983
}
39833984

39843985
/* @internal */

src/compiler/watchedProgram.ts

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ namespace ts {
256256

257257
const sourceFilesCache = createMap<HostFileInfo | string>(); // Cache that stores the source file and version info
258258
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
259+
let hasInvalidatedResolution: HasInvalidatedResolution; // Passed along to see if source file has invalidated resolutions
259260

260261
watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty);
261262
const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost;
@@ -292,7 +293,8 @@ namespace ts {
292293
function synchronizeProgram() {
293294
writeLog(`Synchronizing program`);
294295

295-
if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists)) {
296+
hasInvalidatedResolution = resolutionCache.createHasInvalidatedResolution();
297+
if (isProgramUptoDate(program, rootFileNames, compilerOptions, getSourceVersion, fileExists, hasInvalidatedResolution)) {
296298
return;
297299
}
298300

@@ -306,7 +308,7 @@ namespace ts {
306308

307309
// Compile the program
308310
program = createProgram(rootFileNames, compilerOptions, compilerHost, program);
309-
builder.onProgramUpdateGraph(program);
311+
builder.onProgramUpdateGraph(program, hasInvalidatedResolution);
310312

311313
// Update watches
312314
missingFilesMap = updateMissingFilePathsWatch(program, missingFilesMap, watchMissingFilePath, closeMissingFilePathWatcher);
@@ -351,7 +353,8 @@ namespace ts {
351353
realpath,
352354
resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile),
353355
resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false),
354-
onReleaseOldSourceFile
356+
onReleaseOldSourceFile,
357+
hasInvalidatedResolution
355358
};
356359
}
357360

@@ -569,13 +572,7 @@ namespace ts {
569572
writeLog(`Failed lookup location : ${failedLookupLocation} changed: ${FileWatcherEventKind[eventKind]}, fileName: ${fileName} containingFile: ${containingFile}, name: ${name}`);
570573
const path = toPath(failedLookupLocation);
571574
updateCachedSystem(failedLookupLocation, path);
572-
573-
// TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file
574-
// refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions.
575-
// For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again.
576-
577-
// resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
578-
program = undefined;
575+
resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
579576
scheduleProgramUpdate();
580577
}
581578

src/server/lsHost.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ namespace ts.server {
110110

111111
readonly trace: (s: string) => void;
112112
readonly realpath?: (path: string) => string;
113+
114+
/*@internal*/
115+
hasInvalidatedResolution: HasInvalidatedResolution;
116+
113117
/**
114118
* This is the host that is associated with the project. This is normally same as projectService's host
115119
* except in Configured projects where it is CachedServerHost so that we can cache the results of the

src/server/project.ts

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -241,12 +241,8 @@ namespace ts.server {
241241
if (this.projectKind === ProjectKind.Configured) {
242242
(this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(failedLookupLocation));
243243
}
244-
this.updateTypes();
245-
// TODO: We need more intensive approach wherein we are able to comunicate to the program structure reuser that the even though the source file
246-
// refering to this failed location hasnt changed, it needs to re-evaluate the module resolutions for the invalidated resolutions.
247-
// For now just clear existing program, that should still reuse the source files but atleast compute the resolutions again.
248-
// this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
249-
// this.markAsDirty();
244+
this.resolutionCache.invalidateResolutionOfChangedFailedLookupLocation(failedLookupLocation);
245+
this.markAsDirty();
250246
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
251247
});
252248
}
@@ -605,6 +601,7 @@ namespace ts.server {
605601
*/
606602
updateGraph(): boolean {
607603
this.resolutionCache.startRecordingFilesWithChangedResolutions();
604+
this.lsHost.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution();
608605

609606
let hasChanges = this.updateGraphWorker();
610607

@@ -640,7 +637,7 @@ namespace ts.server {
640637
// otherwise tell it to drop its internal state
641638
if (this.builder) {
642639
if (this.languageServiceEnabled && this.compileOnSaveEnabled) {
643-
this.builder.onProgramUpdateGraph(this.program);
640+
this.builder.onProgramUpdateGraph(this.program, this.lsHost.hasInvalidatedResolution);
644641
}
645642
else {
646643
this.builder.clear();

src/services/services.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1115,8 +1115,11 @@ namespace ts {
11151115
// Get a fresh cache of the host information
11161116
let hostCache = new HostCache(host, getCanonicalFileName);
11171117
const rootFileNames = hostCache.getRootFileNames();
1118+
1119+
const hasInvalidatedResolution: HasInvalidatedResolution = host.hasInvalidatedResolution || noop;
1120+
11181121
// If the program is already up-to-date, we can reuse it
1119-
if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists)) {
1122+
if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache.getVersion(path), fileExists, hasInvalidatedResolution)) {
11201123
return;
11211124
}
11221125

@@ -1155,7 +1158,8 @@ namespace ts {
11551158
getDirectories: path => {
11561159
return host.getDirectories ? host.getDirectories(path) : [];
11571160
},
1158-
onReleaseOldSourceFile
1161+
onReleaseOldSourceFile,
1162+
hasInvalidatedResolution
11591163
};
11601164
if (host.trace) {
11611165
compilerHost.trace = message => host.trace(message);

0 commit comments

Comments
 (0)