Skip to content

Commit e711238

Browse files
committed
Add api in builder to get changed files and use it to send project changed event
1 parent e65df12 commit e711238

File tree

6 files changed

+226
-144
lines changed

6 files changed

+226
-144
lines changed

src/compiler/builder.ts

Lines changed: 126 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,29 @@ namespace ts {
1818
text: string;
1919
}
2020

21+
export interface ChangedProgramFiles {
22+
/** Minimal set of list of files that require emit */
23+
readonly filesToEmit: ReadonlyArray<string>;
24+
/** File paths of source files changed/added/removed or affected by changed files */
25+
readonly changedFiles: ReadonlyArray<string>;
26+
}
27+
2128
export interface Builder {
2229
/**
2330
* This is the callback when file infos in the builder are updated
2431
*/
2532
onProgramUpdateGraph(program: Program, hasInvalidatedResolution: HasInvalidatedResolution): void;
2633
getFilesAffectedBy(program: Program, path: Path): string[];
2734
emitFile(program: Program, path: Path): EmitOutput;
35+
36+
/** Emit the changed files and clear the cache of the changed files */
2837
emitChangedFiles(program: Program): EmitOutputDetailed[];
38+
/** Get the changed files since last query and then clear the cache of changed files */
39+
getChangedProgramFiles(program: Program): ChangedProgramFiles;
40+
/** When called gets the semantic diagnostics for the program. It also caches the diagnostics and manage them */
2941
getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[];
42+
43+
/** Called to reset the status of the builder */
3044
clear(): void;
3145
}
3246

@@ -73,16 +87,18 @@ namespace ts {
7387
): Builder {
7488
let isModuleEmit: boolean | undefined;
7589
// Last checked shape signature for the file info
76-
type FileInfo = { version: string; signature: string; };
77-
let fileInfos: Map<FileInfo>;
90+
type FileInfo = { fileName: string; version: string; signature: string; };
91+
const fileInfos = createMap<FileInfo>();
7892
const semanticDiagnosticsPerFile = createMap<Diagnostic[]>();
79-
let changedFilesSinceLastEmit: Map<true>;
93+
/** The map has key by source file's path that has been changed */
94+
const changedFileNames = createMap<string>();
8095
let emitHandler: EmitHandler;
8196
return {
8297
onProgramUpdateGraph,
8398
getFilesAffectedBy,
8499
emitFile,
85100
emitChangedFiles,
101+
getChangedProgramFiles,
86102
getSemanticDiagnostics,
87103
clear
88104
};
@@ -92,13 +108,11 @@ namespace ts {
92108
if (isModuleEmit !== currentIsModuleEmit) {
93109
isModuleEmit = currentIsModuleEmit;
94110
emitHandler = isModuleEmit ? getModuleEmitHandler() : getNonModuleEmitHandler();
95-
fileInfos = undefined;
111+
fileInfos.clear();
96112
semanticDiagnosticsPerFile.clear();
97113
}
98-
99-
changedFilesSinceLastEmit = changedFilesSinceLastEmit || createMap<true>();
100114
mutateMap(
101-
fileInfos || (fileInfos = createMap()),
115+
fileInfos,
102116
arrayToMap(program.getSourceFiles(), sourceFile => sourceFile.path),
103117
{
104118
// Add new file info
@@ -111,27 +125,26 @@ namespace ts {
111125
);
112126
}
113127

114-
function registerChangedFile(path: Path) {
115-
changedFilesSinceLastEmit.set(path, true);
128+
function registerChangedFile(path: Path, fileName: string) {
129+
changedFileNames.set(path, fileName);
116130
// All changed files need to re-evaluate its semantic diagnostics
117131
semanticDiagnosticsPerFile.delete(path);
118132
}
119133

120134
function addNewFileInfo(program: Program, sourceFile: SourceFile): FileInfo {
121-
registerChangedFile(sourceFile.path);
135+
registerChangedFile(sourceFile.path, sourceFile.fileName);
122136
emitHandler.addScriptInfo(program, sourceFile);
123-
return { version: sourceFile.version, signature: undefined };
137+
return { fileName: sourceFile.fileName, version: sourceFile.version, signature: undefined };
124138
}
125139

126-
function removeExistingFileInfo(path: Path, _existingFileInfo: FileInfo) {
127-
registerChangedFile(path);
140+
function removeExistingFileInfo(path: Path, existingFileInfo: FileInfo) {
141+
registerChangedFile(path, existingFileInfo.fileName);
128142
emitHandler.removeScriptInfo(path);
129143
}
130144

131145
function updateExistingFileInfo(program: Program, existingInfo: FileInfo, sourceFile: SourceFile, hasInvalidatedResolution: HasInvalidatedResolution) {
132146
if (existingInfo.version !== sourceFile.version || hasInvalidatedResolution(sourceFile.path)) {
133-
registerChangedFile(sourceFile.path);
134-
semanticDiagnosticsPerFile.delete(sourceFile.path);
147+
registerChangedFile(sourceFile.path, sourceFile.fileName);
135148
existingInfo.version = sourceFile.version;
136149
emitHandler.updateScriptInfo(program, sourceFile);
137150
}
@@ -154,7 +167,7 @@ namespace ts {
154167

155168
const sourceFile = program.getSourceFile(path);
156169
const singleFileResult = sourceFile && shouldEmitFile(sourceFile) ? [sourceFile.fileName] : [];
157-
const info = fileInfos && fileInfos.get(path);
170+
const info = fileInfos.get(path);
158171
if (!info || !updateShapeSignature(program, sourceFile, info)) {
159172
return singleFileResult;
160173
}
@@ -165,62 +178,73 @@ namespace ts {
165178

166179
function emitFile(program: Program, path: Path) {
167180
ensureProgramGraph(program);
168-
if (!fileInfos || !fileInfos.has(path)) {
181+
if (!fileInfos.has(path)) {
169182
return { outputFiles: [], emitSkipped: true };
170183
}
171184

172185
return getEmitOutput(program, program.getSourceFileByPath(path), /*emitOnlyDtsFiles*/ false, /*isDetailed*/ false);
173186
}
174187

175-
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
176-
ensureProgramGraph(program);
177-
const result: EmitOutputDetailed[] = [];
178-
if (changedFilesSinceLastEmit) {
179-
const seenFiles = createMap<SourceFile>();
180-
changedFilesSinceLastEmit.forEach((__value, path: Path) => {
181-
const affectedFiles = getFilesAffectedBy(program, path);
182-
for (const file of affectedFiles) {
183-
if (!seenFiles.has(file)) {
184-
const sourceFile = program.getSourceFile(file);
185-
seenFiles.set(file, sourceFile);
186-
if (sourceFile) {
187-
// Any affected file shouldnt have the cached diagnostics
188-
semanticDiagnosticsPerFile.delete(sourceFile.path);
189-
190-
const emitOutput = getEmitOutput(program, sourceFile, /*emitOnlyDtsFiles*/ false, /*isDetailed*/ true) as EmitOutputDetailed;
191-
result.push(emitOutput);
192-
193-
// mark all the emitted source files as seen
194-
if (emitOutput.emittedSourceFiles) {
195-
for (const file of emitOutput.emittedSourceFiles) {
196-
seenFiles.set(file.fileName, file);
197-
}
198-
}
188+
function enumerateChangedFilesSet(
189+
program: Program,
190+
onChangedFile: (fileName: string) => void,
191+
onAffectedFile: (fileName: string, sourceFile: SourceFile) => void
192+
) {
193+
changedFileNames.forEach((fileName, path) => {
194+
onChangedFile(fileName);
195+
const affectedFiles = getFilesAffectedBy(program, path as Path);
196+
for (const file of affectedFiles) {
197+
onAffectedFile(file, program.getSourceFile(file));
198+
}
199+
});
200+
}
201+
202+
function enumerateChangedFilesEmitOutput(
203+
program: Program,
204+
emitOnlyDtsFiles: boolean,
205+
onChangedFile: (fileName: string) => void,
206+
onEmitOutput: (emitOutput: EmitOutputDetailed, sourceFile: SourceFile) => void
207+
) {
208+
const seenFiles = createMap<SourceFile>();
209+
enumerateChangedFilesSet(program, onChangedFile, (fileName, sourceFile) => {
210+
if (!seenFiles.has(fileName)) {
211+
seenFiles.set(fileName, sourceFile);
212+
if (sourceFile) {
213+
// Any affected file shouldnt have the cached diagnostics
214+
semanticDiagnosticsPerFile.delete(sourceFile.path);
215+
216+
const emitOutput = getEmitOutput(program, sourceFile, emitOnlyDtsFiles, /*isDetailed*/ true) as EmitOutputDetailed;
217+
onEmitOutput(emitOutput, sourceFile);
218+
219+
// mark all the emitted source files as seen
220+
if (emitOutput.emittedSourceFiles) {
221+
for (const file of emitOutput.emittedSourceFiles) {
222+
seenFiles.set(file.fileName, file);
199223
}
200224
}
201225
}
202-
});
226+
}
227+
});
228+
}
203229

204-
changedFilesSinceLastEmit = undefined;
205-
}
230+
function emitChangedFiles(program: Program): EmitOutputDetailed[] {
231+
ensureProgramGraph(program);
232+
const result: EmitOutputDetailed[] = [];
233+
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ false,
234+
/*onChangedFile*/ noop, emitOutput => result.push(emitOutput));
235+
changedFileNames.clear();
206236
return result;
207237
}
208238

209239
function getSemanticDiagnostics(program: Program, cancellationToken?: CancellationToken): Diagnostic[] {
210240
ensureProgramGraph(program);
211241

212242
// Ensure that changed files have cleared their respective
213-
if (changedFilesSinceLastEmit) {
214-
changedFilesSinceLastEmit.forEach((__value, path: Path) => {
215-
const affectedFiles = getFilesAffectedBy(program, path);
216-
for (const file of affectedFiles) {
217-
const sourceFile = program.getSourceFile(file);
218-
if (sourceFile) {
219-
semanticDiagnosticsPerFile.delete(sourceFile.path);
220-
}
221-
}
222-
});
223-
}
243+
enumerateChangedFilesSet(program, /*onChangedFile*/ noop, (_affectedFileName, sourceFile) => {
244+
if (sourceFile) {
245+
semanticDiagnosticsPerFile.delete(sourceFile.path);
246+
}
247+
});
224248

225249
let diagnostics: Diagnostic[];
226250
for (const sourceFile of program.getSourceFiles()) {
@@ -240,11 +264,36 @@ namespace ts {
240264
return diagnostics || emptyArray;
241265
}
242266

267+
function getChangedProgramFiles(program: Program): ChangedProgramFiles {
268+
ensureProgramGraph(program);
269+
270+
let filesToEmit: string[];
271+
let changedFiles: string[];
272+
enumerateChangedFilesEmitOutput(program, /*emitOnlyDtsFiles*/ true,
273+
// All the changed files are required to get diagnostics
274+
changedFileName => addFileForDiagnostics(changedFileName),
275+
// Emitted file is for emit as well as diagnostic
276+
(_emitOutput, sourceFile) => {
277+
(filesToEmit || (filesToEmit = [])).push(sourceFile.fileName);
278+
addFileForDiagnostics(sourceFile.fileName);
279+
});
280+
changedFileNames.clear();
281+
return {
282+
filesToEmit: filesToEmit || emptyArray,
283+
changedFiles: changedFiles || emptyArray
284+
};
285+
286+
function addFileForDiagnostics(fileName: string) {
287+
(changedFiles || (changedFiles = [])).push(fileName);
288+
}
289+
}
290+
243291
function clear() {
244292
isModuleEmit = undefined;
245293
emitHandler = undefined;
246-
fileInfos = undefined;
294+
fileInfos.clear();
247295
semanticDiagnosticsPerFile.clear();
296+
changedFileNames.clear();
248297
}
249298

250299
/**
@@ -287,9 +336,7 @@ namespace ts {
287336
}
288337

289338
/**
290-
* Gets the referenced files for a file from the program
291-
* @param program
292-
* @param path
339+
* Gets the referenced files for a file from the program with values for the keys as referenced file's path to be true
293340
*/
294341
function getReferencedFiles(program: Program, sourceFile: SourceFile): Map<true> {
295342
const referencedFiles = createMap<true>();
@@ -371,55 +418,36 @@ namespace ts {
371418

372419
function getModuleEmitHandler(): EmitHandler {
373420
const references = createMap<Map<true>>();
374-
const referencedBy = createMultiMap<Path>();
375421
return {
376-
addScriptInfo: (program, sourceFile) => {
377-
const refs = createMap<true>();
378-
references.set(sourceFile.path, refs);
379-
setReferences(program, sourceFile, refs);
380-
},
422+
addScriptInfo: setReferences,
381423
removeScriptInfo,
382-
updateScriptInfo: (program, sourceFile) => setReferences(program, sourceFile, references.get(sourceFile.path)),
424+
updateScriptInfo: setReferences,
383425
getFilesAffectedByUpdatedShape
384426
};
385427

386-
function setReferences(program: Program, sourceFile: SourceFile, existingReferences: Map<true>) {
387-
const path = sourceFile.path;
388-
mutateMap(
389-
// Existing references
390-
existingReferences,
391-
// Updated references
392-
getReferencedFiles(program, sourceFile),
393-
{
394-
// Creating new Reference: as sourceFile references file with path 'key'
395-
// in other words source file (path) is referenced by 'key'
396-
createNewValue: (key): true => { referencedBy.add(key, path); return true; },
397-
// Remove existing reference by entry: source file doesnt reference file 'key' any more
398-
// in other words source file (path) is not referenced by 'key'
399-
onDeleteValue: (key, _existingValue) => { referencedBy.remove(key, path); }
400-
}
401-
);
428+
function setReferences(program: Program, sourceFile: SourceFile) {
429+
references.set(sourceFile.path, getReferencedFiles(program, sourceFile));
402430
}
403431

404-
function removeScriptInfo(path: Path) {
432+
function removeScriptInfo(removedFilePath: Path) {
405433
// Remove existing references
406-
references.forEach((_value, key) => {
407-
referencedBy.remove(key, path);
408-
});
409-
references.delete(path);
410-
411-
// Delete the entry and add files referencing this file, as chagned files too
412-
const referencedByPaths = referencedBy.get(path);
413-
if (referencedByPaths) {
414-
for (const path of referencedByPaths) {
415-
registerChangedFile(path);
434+
references.forEach((referencesInFile, filePath) => {
435+
if (referencesInFile.has(removedFilePath)) {
436+
// add files referencing the removedFilePath, as changed files too
437+
const referencedByInfo = fileInfos.get(filePath);
438+
if (referencedByInfo) {
439+
registerChangedFile(filePath as Path, referencedByInfo.fileName);
440+
}
416441
}
417-
referencedBy.delete(path);
418-
}
442+
});
443+
// Delete the entry for the removed file path
444+
references.delete(removedFilePath);
419445
}
420446

421-
function getReferencedByPaths(path: Path) {
422-
return referencedBy.get(path) || [];
447+
function getReferencedByPaths(referencedFilePath: Path) {
448+
return mapDefinedIter(references.entries(), ([filePath, referencesInFile]) =>
449+
referencesInFile.has(referencedFilePath) ? filePath as Path : undefined
450+
);
423451
}
424452

425453
function getFilesAffectedByUpdatedShape(program: Program, sourceFile: SourceFile, singleFileResult: string[]): string[] {
@@ -444,7 +472,7 @@ namespace ts {
444472
// Start with the paths this file was referenced by
445473
const path = sourceFile.path;
446474
setSeenFileName(path, sourceFile);
447-
const queue = getReferencedByPaths(path).slice();
475+
const queue = getReferencedByPaths(path);
448476
while (queue.length > 0) {
449477
const currentPath = queue.pop();
450478
if (!seenFileNamesMap.has(currentPath)) {

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ namespace ts.projectSystem {
125125
this.events.push(event);
126126
}
127127

128-
checkEventCountOfType(eventType: "context" | "configFileDiag", expectedCount: number) {
128+
checkEventCountOfType(eventType: "configFileDiag", expectedCount: number) {
129129
const eventsOfType = filter(this.events, e => e.eventName === eventType);
130130
assert.equal(eventsOfType.length, expectedCount, `The actual event counts of type ${eventType} is ${eventsOfType.length}, while expected ${expectedCount}`);
131131
}
@@ -2001,7 +2001,7 @@ namespace ts.projectSystem {
20012001
const session = createSession(host, {
20022002
canUseEvents: true,
20032003
eventHandler: e => {
2004-
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ContextEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
2004+
if (e.eventName === server.ConfigFileDiagEvent || e.eventName === server.ProjectChangedEvent || e.eventName === server.ProjectInfoTelemetryEvent) {
20052005
return;
20062006
}
20072007
assert.equal(e.eventName, server.ProjectLanguageServiceStateEvent);

0 commit comments

Comments
 (0)