Skip to content

Commit ac72803

Browse files
authored
Merge pull request #16684 from amcasey/Vsts434619
Watch for the creation of missing files
2 parents 179a3e1 + 569ecab commit ac72803

18 files changed

+287
-47
lines changed

Jakefile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ var harnessSources = harnessCoreSources.concat([
138138
"telemetry.ts",
139139
"transform.ts",
140140
"customTransforms.ts",
141+
"programMissingFiles.ts",
141142
].map(function (f) {
142143
return path.join(unittestsDirectory, f);
143144
})).concat([

src/compiler/core.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1098,6 +1098,15 @@ namespace ts {
10981098
return result;
10991099
}
11001100

1101+
/**
1102+
* Creates a set from the elements of an array.
1103+
*
1104+
* @param array the array of input elements.
1105+
*/
1106+
export function arrayToSet<T>(array: T[], makeKey: (value: T) => string): Map<true> {
1107+
return arrayToMap<T, true>(array, makeKey, () => true);
1108+
}
1109+
11011110
export function cloneMap<T>(map: Map<T>) {
11021111
const clone = createMap<T>();
11031112
copyEntries(map, clone);

src/compiler/program.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ namespace ts {
472472
resolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile) => loadWithLocalCache(typeReferenceDirectiveNames, containingFile, loader);
473473
}
474474

475-
const filesByName = createMap<SourceFile>();
475+
const filesByName = createMap<SourceFile | undefined>();
476476
// stores 'filename -> file association' ignoring case
477477
// used to track cases when two file names differ only in casing
478478
const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createFileMap<SourceFile>(fileName => fileName.toLowerCase()) : undefined;
@@ -513,6 +513,8 @@ namespace ts {
513513
}
514514
}
515515

516+
const missingFilePaths = arrayFrom(filesByName.keys(), p => <Path>p).filter(p => !filesByName.get(p));
517+
516518
// unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks
517519
moduleResolutionCache = undefined;
518520

@@ -524,6 +526,7 @@ namespace ts {
524526
getSourceFile,
525527
getSourceFileByPath,
526528
getSourceFiles: () => files,
529+
getMissingFilePaths: () => missingFilePaths,
527530
getCompilerOptions: () => options,
528531
getSyntacticDiagnostics,
529532
getOptionsDiagnostics,
@@ -862,6 +865,21 @@ namespace ts {
862865
return oldProgram.structureIsReused;
863866
}
864867

868+
// If a file has ceased to be missing, then we need to discard some of the old
869+
// structure in order to pick it up.
870+
// Caution: if the file has created and then deleted between since it was discovered to
871+
// be missing, then the corresponding file watcher will have been closed and no new one
872+
// will be created until we encounter a change that prevents complete structure reuse.
873+
// During this interval, creation of the file will go unnoticed. We expect this to be
874+
// both rare and low-impact.
875+
if (oldProgram.getMissingFilePaths().some(missingFilePath => host.fileExists(missingFilePath))) {
876+
return oldProgram.structureIsReused = StructureIsReused.SafeModules;
877+
}
878+
879+
for (const p of oldProgram.getMissingFilePaths()) {
880+
filesByName.set(p, undefined);
881+
}
882+
865883
// update fileName -> file mapping
866884
for (let i = 0; i < newSourceFiles.length; i++) {
867885
filesByName.set(filePaths[i], newSourceFiles[i]);

src/compiler/sys.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ declare function setTimeout(handler: (...args: any[]) => void, timeout: number):
44
declare function clearTimeout(handle: any): void;
55

66
namespace ts {
7-
export type FileWatcherCallback = (fileName: string, removed?: boolean) => void;
7+
export enum FileWatcherEventKind {
8+
Created,
9+
Changed,
10+
Deleted
11+
}
12+
13+
export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind) => void;
814
export type DirectoryWatcherCallback = (fileName: string) => void;
915
export interface WatchedFile {
1016
fileName: string;
@@ -174,7 +180,7 @@ namespace ts {
174180
const callbacks = fileWatcherCallbacks.get(fileName);
175181
if (callbacks) {
176182
for (const fileCallback of callbacks) {
177-
fileCallback(fileName);
183+
fileCallback(fileName, FileWatcherEventKind.Changed);
178184
}
179185
}
180186
}
@@ -340,11 +346,22 @@ namespace ts {
340346
}
341347

342348
function fileChanged(curr: any, prev: any) {
343-
if (+curr.mtime <= +prev.mtime) {
349+
const isCurrZero = +curr.mtime === 0;
350+
const isPrevZero = +prev.mtime === 0;
351+
const created = !isCurrZero && isPrevZero;
352+
const deleted = isCurrZero && !isPrevZero;
353+
354+
const eventKind = created
355+
? FileWatcherEventKind.Created
356+
: deleted
357+
? FileWatcherEventKind.Deleted
358+
: FileWatcherEventKind.Changed;
359+
360+
if (eventKind === FileWatcherEventKind.Changed && +curr.mtime <= +prev.mtime) {
344361
return;
345362
}
346363

347-
callback(fileName);
364+
callback(fileName, eventKind);
348365
}
349366
},
350367
watchDirectory: (directoryName, callback, recursive) => {

src/compiler/tsc.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ namespace ts {
285285

286286
setCachedProgram(compileResult.program);
287287
reportWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Compilation_complete_Watching_for_file_changes));
288+
289+
const missingPaths = compileResult.program.getMissingFilePaths();
290+
missingPaths.forEach(path => {
291+
const fileWatcher = sys.watchFile(path, (_fileName, eventKind) => {
292+
if (eventKind === FileWatcherEventKind.Created) {
293+
fileWatcher.close();
294+
startTimerForRecompilation();
295+
}
296+
});
297+
});
288298
}
289299

290300
function cachedFileExists(fileName: string): boolean {
@@ -308,7 +318,7 @@ namespace ts {
308318
const sourceFile = hostGetSourceFile(fileName, languageVersion, onError);
309319
if (sourceFile && isWatchSet(compilerOptions) && sys.watchFile) {
310320
// Attach a file watcher
311-
sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName: string, removed?: boolean) => sourceFileChanged(sourceFile, removed));
321+
sourceFile.fileWatcher = sys.watchFile(sourceFile.fileName, (_fileName, eventKind) => sourceFileChanged(sourceFile, eventKind));
312322
}
313323
return sourceFile;
314324
}
@@ -330,10 +340,10 @@ namespace ts {
330340
}
331341

332342
// If a source file changes, mark it as unwatched and start the recompilation timer
333-
function sourceFileChanged(sourceFile: SourceFile, removed?: boolean) {
343+
function sourceFileChanged(sourceFile: SourceFile, eventKind: FileWatcherEventKind) {
334344
sourceFile.fileWatcher.close();
335345
sourceFile.fileWatcher = undefined;
336-
if (removed) {
346+
if (eventKind === FileWatcherEventKind.Deleted) {
337347
unorderedRemoveItem(rootFileNames, sourceFile.fileName);
338348
}
339349
startTimerForRecompilation();

src/compiler/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2426,6 +2426,13 @@ namespace ts {
24262426
*/
24272427
getSourceFiles(): SourceFile[];
24282428

2429+
/**
2430+
* Get a list of file names that were passed to 'createProgram' or referenced in a
2431+
* program source file but could not be located.
2432+
*/
2433+
/* @internal */
2434+
getMissingFilePaths(): Path[];
2435+
24292436
/**
24302437
* Emits the JavaScript and declaration files. If targetSourceFile is not specified, then
24312438
* the JavaScript and declaration files will be produced for all the files in this program.

src/harness/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
"./unittests/transform.ts",
130130
"./unittests/customTransforms.ts",
131131
"./unittests/textChanges.ts",
132-
"./unittests/telemetry.ts"
132+
"./unittests/telemetry.ts",
133+
"./unittests/programMissingFiles.ts"
133134
]
134135
}

src/harness/unittests/cachingInServerLSHost.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ namespace ts {
7575
const rootScriptInfo = projectService.getOrCreateScriptInfo(rootFile, /* openedByClient */ true, /*containingProject*/ undefined);
7676

7777
const project = projectService.createInferredProjectWithRootFileIfNecessary(rootScriptInfo);
78-
project.setCompilerOptions({ module: ts.ModuleKind.AMD } );
78+
project.setCompilerOptions({ module: ts.ModuleKind.AMD, noLib: true } );
7979
return {
8080
project,
8181
rootScriptInfo

src/harness/unittests/compileOnSave.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ namespace ts.projectSystem {
208208

209209
file1Consumer1.content = `let y = 10;`;
210210
host.reloadFS([moduleFile1, file1Consumer1, file1Consumer2, configFile, libFile]);
211-
host.triggerFileWatcherCallback(file1Consumer1.path, /*removed*/ false);
211+
host.triggerFileWatcherCallback(file1Consumer1.path, FileWatcherEventKind.Changed);
212212

213213
session.executeCommand(changeModuleFile1ShapeRequest1);
214214
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer2] }]);
@@ -225,7 +225,7 @@ namespace ts.projectSystem {
225225
session.executeCommand(changeModuleFile1ShapeRequest1);
226226
// Delete file1Consumer2
227227
host.reloadFS([moduleFile1, file1Consumer1, configFile, libFile]);
228-
host.triggerFileWatcherCallback(file1Consumer2.path, /*removed*/ true);
228+
host.triggerFileWatcherCallback(file1Consumer2.path, FileWatcherEventKind.Deleted);
229229
sendAffectedFileRequestAndCheckResult(session, moduleFile1FileListRequest, [{ projectFileName: configFile.path, files: [moduleFile1, file1Consumer1] }]);
230230
});
231231

@@ -475,7 +475,7 @@ namespace ts.projectSystem {
475475

476476
openFilesForSession([referenceFile1], session);
477477
host.reloadFS([referenceFile1, configFile]);
478-
host.triggerFileWatcherCallback(moduleFile1.path, /*removed*/ true);
478+
host.triggerFileWatcherCallback(moduleFile1.path, FileWatcherEventKind.Deleted);
479479

480480
const request = makeSessionRequest<server.protocol.FileRequestArgs>(CommandNames.CompileOnSaveAffectedFileList, { file: referenceFile1.path });
481481
sendAffectedFileRequestAndCheckResult(session, request, [
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/// <reference path="..\harness.ts" />
2+
3+
namespace ts {
4+
describe("Program.getMissingFilePaths", () => {
5+
6+
const options: CompilerOptions = {
7+
noLib: true,
8+
};
9+
10+
const emptyFileName = "empty.ts";
11+
const emptyFileRelativePath = "./" + emptyFileName;
12+
13+
const emptyFile: Harness.Compiler.TestFile = {
14+
unitName: emptyFileName,
15+
content: ""
16+
};
17+
18+
const referenceFileName = "reference.ts";
19+
const referenceFileRelativePath = "./" + referenceFileName;
20+
21+
const referenceFile: Harness.Compiler.TestFile = {
22+
unitName: referenceFileName,
23+
content:
24+
"/// <reference path=\"d:/imaginary/nonexistent1.ts\"/>\n" + // Absolute
25+
"/// <reference path=\"./nonexistent2.ts\"/>\n" + // Relative
26+
"/// <reference path=\"nonexistent3.ts\"/>\n" + // Unqualified
27+
"/// <reference path=\"nonexistent4\"/>\n" // No extension
28+
};
29+
30+
const testCompilerHost = Harness.Compiler.createCompilerHost(
31+
/*inputFiles*/ [emptyFile, referenceFile],
32+
/*writeFile*/ undefined,
33+
/*scriptTarget*/ undefined,
34+
/*useCaseSensitiveFileNames*/ false,
35+
/*currentDirectory*/ "d:\\pretend\\",
36+
/*newLineKind*/ NewLineKind.LineFeed,
37+
/*libFiles*/ undefined
38+
);
39+
40+
it("handles no missing root files", () => {
41+
const program = createProgram([emptyFileRelativePath], options, testCompilerHost);
42+
const missing = program.getMissingFilePaths();
43+
assert.isDefined(missing);
44+
assert.deepEqual(missing, []);
45+
});
46+
47+
it("handles missing root file", () => {
48+
const program = createProgram(["./nonexistent.ts"], options, testCompilerHost);
49+
const missing = program.getMissingFilePaths();
50+
assert.isDefined(missing);
51+
assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]); // Absolute path
52+
});
53+
54+
it("handles multiple missing root files", () => {
55+
const program = createProgram(["./nonexistent0.ts", "./nonexistent1.ts"], options, testCompilerHost);
56+
const missing = program.getMissingFilePaths().sort();
57+
assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
58+
});
59+
60+
it("handles a mix of present and missing root files", () => {
61+
const program = createProgram(["./nonexistent0.ts", emptyFileRelativePath, "./nonexistent1.ts"], options, testCompilerHost);
62+
const missing = program.getMissingFilePaths().sort();
63+
assert.deepEqual(missing, ["d:/pretend/nonexistent0.ts", "d:/pretend/nonexistent1.ts"]);
64+
});
65+
66+
it("handles repeatedly specified root files", () => {
67+
const program = createProgram(["./nonexistent.ts", "./nonexistent.ts"], options, testCompilerHost);
68+
const missing = program.getMissingFilePaths();
69+
assert.isDefined(missing);
70+
assert.deepEqual(missing, ["d:/pretend/nonexistent.ts"]);
71+
});
72+
73+
it("normalizes file paths", () => {
74+
const program0 = createProgram(["./nonexistent.ts", "./NONEXISTENT.ts"], options, testCompilerHost);
75+
const program1 = createProgram(["./NONEXISTENT.ts", "./nonexistent.ts"], options, testCompilerHost);
76+
const missing0 = program0.getMissingFilePaths();
77+
const missing1 = program1.getMissingFilePaths();
78+
assert.equal(missing0.length, 1);
79+
assert.deepEqual(missing0, missing1);
80+
});
81+
82+
it("handles missing triple slash references", () => {
83+
const program = createProgram([referenceFileRelativePath], options, testCompilerHost);
84+
const missing = program.getMissingFilePaths().sort();
85+
assert.isDefined(missing);
86+
assert.deepEqual(missing, [
87+
// From absolute reference
88+
"d:/imaginary/nonexistent1.ts",
89+
90+
// From relative reference
91+
"d:/pretend/nonexistent2.ts",
92+
93+
// From unqualified reference
94+
"d:/pretend/nonexistent3.ts",
95+
96+
// From no-extension reference
97+
"d:/pretend/nonexistent4.d.ts",
98+
"d:/pretend/nonexistent4.ts",
99+
"d:/pretend/nonexistent4.tsx"
100+
]);
101+
});
102+
});
103+
}

0 commit comments

Comments
 (0)