Skip to content

Commit a4b9bba

Browse files
Handle noEmit and noEmitOnError with SemanticDiagnosticsBuilder (microsoft#40880)
* Add test that fails * Handle noEmit on semantic builder's emit as well * Add test for tsbuildinfo text verification * Fix noEmit handling for tsbuildinfo emit with SemanticDiagnosticBuilder * Add test for noEmitOnError with SemanticDiagnosticsBuilder * Fix tsbuildinfo emit with SemanticDiagnosticsBuilder on noEmitOnError * Update src/compiler/builder.ts Co-authored-by: Nathan Shively-Sanders <[email protected]> * Update src/compiler/builder.ts Co-authored-by: Nathan Shively-Sanders <[email protected]>
1 parent a109b5d commit a4b9bba

File tree

4 files changed

+174
-23
lines changed

4 files changed

+174
-23
lines changed

src/compiler/builder.ts

Lines changed: 50 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,31 +1019,57 @@ namespace ts {
10191019
* in that order would be used to write the files
10201020
*/
10211021
function emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult {
1022+
let restorePendingEmitOnHandlingNoEmitSuccess = false;
1023+
let savedAffectedFilesPendingEmit;
1024+
let savedAffectedFilesPendingEmitKind;
1025+
let savedAffectedFilesPendingEmitIndex;
1026+
// Backup and restore affected pendings emit state for non emit Builder if noEmitOnError is enabled and emitBuildInfo could be written in case there are errors
1027+
// This ensures pending files to emit is updated in tsbuildinfo
1028+
// Note that when there are no errors, emit proceeds as if everything is emitted as it is callers reponsibility to write the files to disk if at all (because its builder that doesnt track files to emit)
1029+
if (kind !== BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram &&
1030+
!targetSourceFile &&
1031+
!outFile(state.compilerOptions) &&
1032+
!state.compilerOptions.noEmit &&
1033+
state.compilerOptions.noEmitOnError) {
1034+
restorePendingEmitOnHandlingNoEmitSuccess = true;
1035+
savedAffectedFilesPendingEmit = state.affectedFilesPendingEmit && state.affectedFilesPendingEmit.slice();
1036+
savedAffectedFilesPendingEmitKind = state.affectedFilesPendingEmitKind && new Map(state.affectedFilesPendingEmitKind);
1037+
savedAffectedFilesPendingEmitIndex = state.affectedFilesPendingEmitIndex;
1038+
}
1039+
10221040
if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) {
10231041
assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile);
1024-
const result = handleNoEmitOptions(builderProgram, targetSourceFile, writeFile, cancellationToken);
1025-
if (result) return result;
1026-
if (!targetSourceFile) {
1027-
// Emit and report any errors we ran into.
1028-
let sourceMaps: SourceMapEmitResult[] = [];
1029-
let emitSkipped = false;
1030-
let diagnostics: Diagnostic[] | undefined;
1031-
let emittedFiles: string[] = [];
1032-
1033-
let affectedEmitResult: AffectedFileResult<EmitResult>;
1034-
while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) {
1035-
emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped;
1036-
diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics);
1037-
emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles);
1038-
sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps);
1039-
}
1040-
return {
1041-
emitSkipped,
1042-
diagnostics: diagnostics || emptyArray,
1043-
emittedFiles,
1044-
sourceMaps
1045-
};
1042+
}
1043+
const result = handleNoEmitOptions(builderProgram, targetSourceFile, writeFile, cancellationToken);
1044+
if (result) return result;
1045+
1046+
if (restorePendingEmitOnHandlingNoEmitSuccess) {
1047+
state.affectedFilesPendingEmit = savedAffectedFilesPendingEmit;
1048+
state.affectedFilesPendingEmitKind = savedAffectedFilesPendingEmitKind;
1049+
state.affectedFilesPendingEmitIndex = savedAffectedFilesPendingEmitIndex;
1050+
}
1051+
1052+
// Emit only affected files if using builder for emit
1053+
if (!targetSourceFile && kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) {
1054+
// Emit and report any errors we ran into.
1055+
let sourceMaps: SourceMapEmitResult[] = [];
1056+
let emitSkipped = false;
1057+
let diagnostics: Diagnostic[] | undefined;
1058+
let emittedFiles: string[] = [];
1059+
1060+
let affectedEmitResult: AffectedFileResult<EmitResult>;
1061+
while (affectedEmitResult = emitNextAffectedFile(writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers)) {
1062+
emitSkipped = emitSkipped || affectedEmitResult.result.emitSkipped;
1063+
diagnostics = addRange(diagnostics, affectedEmitResult.result.diagnostics);
1064+
emittedFiles = addRange(emittedFiles, affectedEmitResult.result.emittedFiles);
1065+
sourceMaps = addRange(sourceMaps, affectedEmitResult.result.sourceMaps);
10461066
}
1067+
return {
1068+
emitSkipped,
1069+
diagnostics: diagnostics || emptyArray,
1070+
emittedFiles,
1071+
sourceMaps
1072+
};
10471073
}
10481074
return Debug.checkDefined(state.program).emit(targetSourceFile, writeFile || maybeBind(host, host.writeFile), cancellationToken, emitOnlyDtsFiles, customTransformers);
10491075
}
@@ -1069,7 +1095,8 @@ namespace ts {
10691095
}
10701096

10711097
// Add file to affected file pending emit to handle for later emit time
1072-
if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) {
1098+
// Apart for emit builder do this for tsbuildinfo, do this for non emit builder when noEmit is set as tsbuildinfo is written and reused between emitters
1099+
if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram || state.compilerOptions.noEmit || state.compilerOptions.noEmitOnError) {
10731100
addToAffectedFilesPendingEmit(state, (affected as SourceFile).resolvedPath, BuilderFileEmit.Full);
10741101
}
10751102

src/compiler/program.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,7 @@ namespace ts {
937937
getOptionsDiagnostics,
938938
getGlobalDiagnostics,
939939
getSemanticDiagnostics,
940+
getCachedSemanticDiagnostics,
940941
getSuggestionDiagnostics,
941942
getDeclarationDiagnostics,
942943
getBindAndCheckDiagnostics,
@@ -1693,6 +1694,12 @@ namespace ts {
16931694
return getDiagnosticsHelper(sourceFile, getSemanticDiagnosticsForFile, cancellationToken);
16941695
}
16951696

1697+
function getCachedSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] | undefined {
1698+
return sourceFile
1699+
? cachedBindAndCheckDiagnosticsForFile.perFile?.get(sourceFile.path)
1700+
: cachedBindAndCheckDiagnosticsForFile.allDiagnostics;
1701+
}
1702+
16961703
function getBindAndCheckDiagnostics(sourceFile: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[] {
16971704
return getBindAndCheckDiagnosticsForFile(sourceFile, cancellationToken);
16981705
}

src/compiler/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3762,6 +3762,8 @@ namespace ts {
37623762
/* @internal */ getDiagnosticsProducingTypeChecker(): TypeChecker;
37633763
/* @internal */ dropDiagnosticsProducingTypeChecker(): void;
37643764

3765+
/* @internal */ getCachedSemanticDiagnostics(sourceFile?: SourceFile): readonly Diagnostic[] | undefined;
3766+
37653767
/* @internal */ getClassifiableNames(): Set<__String>;
37663768

37673769
getTypeCatalog(): readonly Type[];

src/testRunner/unittests/tscWatch/watchApi.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,4 +128,119 @@ namespace ts.tscWatch {
128128
checkProgramActualFiles(watch.getProgram().getProgram(), [mainFile.path, otherFile.path, libFile.path, other2]);
129129
});
130130
});
131+
132+
describe("unittests:: tsc-watch:: watchAPI:: when watchHost uses createSemanticDiagnosticsBuilderProgram", () => {
133+
function getWatch<T extends BuilderProgram>(config: File, optionsToExtend: CompilerOptions | undefined, sys: System, createProgram: CreateProgram<T>) {
134+
const watchCompilerHost = createWatchCompilerHost(config.path, optionsToExtend, sys, createProgram);
135+
return createWatchProgram(watchCompilerHost);
136+
}
137+
138+
function setup<T extends BuilderProgram>(createProgram: CreateProgram<T>, configText: string) {
139+
const config: File = {
140+
path: `${projectRoot}/tsconfig.json`,
141+
content: configText
142+
};
143+
const mainFile: File = {
144+
path: `${projectRoot}/main.ts`,
145+
content: "export const x = 10;"
146+
};
147+
const otherFile: File = {
148+
path: `${projectRoot}/other.ts`,
149+
content: "export const y = 10;"
150+
};
151+
const sys = createWatchedSystem([config, mainFile, otherFile, libFile]);
152+
const watch = getWatch(config, { noEmit: true }, sys, createProgram);
153+
return { sys, watch, mainFile, otherFile, config };
154+
}
155+
156+
function verifyOutputs(sys: System, emitSys: System) {
157+
for (const output of [`${projectRoot}/main.js`, `${projectRoot}/main.d.ts`, `${projectRoot}/other.js`, `${projectRoot}/other.d.ts`, `${projectRoot}/tsconfig.tsbuildinfo`]) {
158+
assert.strictEqual(sys.readFile(output), emitSys.readFile(output), `Output file text for ${output}`);
159+
}
160+
}
161+
162+
function verifyBuilder<T extends BuilderProgram, U extends BuilderProgram>(config: File, sys: System, emitSys: System, createProgram: CreateProgram<T>, createEmitProgram: CreateProgram<U>, optionsToExtend?: CompilerOptions) {
163+
const watch = getWatch(config, /*optionsToExtend*/ optionsToExtend, sys, createProgram);
164+
const emitWatch = getWatch(config, /*optionsToExtend*/ optionsToExtend, emitSys, createEmitProgram);
165+
verifyOutputs(sys, emitSys);
166+
watch.close();
167+
emitWatch.close();
168+
}
169+
170+
it("verifies that noEmit is handled on createSemanticDiagnosticsBuilderProgram and typechecking happens only on affected files", () => {
171+
const { sys, watch, mainFile, otherFile } = setup(createSemanticDiagnosticsBuilderProgram, "{}");
172+
checkProgramActualFiles(watch.getProgram().getProgram(), [mainFile.path, otherFile.path, libFile.path]);
173+
sys.appendFile(mainFile.path, "\n// SomeComment");
174+
sys.runQueuedTimeoutCallbacks();
175+
const program = watch.getProgram().getProgram();
176+
assert.deepEqual(program.getCachedSemanticDiagnostics(program.getSourceFile(mainFile.path)), []);
177+
// Should not retrieve diagnostics for other file thats not changed
178+
assert.deepEqual(program.getCachedSemanticDiagnostics(program.getSourceFile(otherFile.path)), /*expected*/ undefined);
179+
});
180+
181+
it("noEmit with composite writes the tsbuildinfo with pending affected files correctly", () => {
182+
const configText = JSON.stringify({ compilerOptions: { composite: true } });
183+
const { sys, watch, config, mainFile } = setup(createSemanticDiagnosticsBuilderProgram, configText);
184+
const { sys: emitSys, watch: emitWatch } = setup(createEmitAndSemanticDiagnosticsBuilderProgram, configText);
185+
verifyOutputs(sys, emitSys);
186+
187+
watch.close();
188+
emitWatch.close();
189+
190+
// Emit on both sys should result in same output
191+
verifyBuilder(config, sys, emitSys, createEmitAndSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram);
192+
193+
// Change file
194+
sys.appendFile(mainFile.path, "\n// SomeComment");
195+
emitSys.appendFile(mainFile.path, "\n// SomeComment");
196+
197+
// Verify noEmit results in same output
198+
verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmit: true });
199+
200+
// Emit on both sys should result in same output
201+
verifyBuilder(config, sys, emitSys, createEmitAndSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram);
202+
203+
// Change file
204+
sys.appendFile(mainFile.path, "\n// SomeComment");
205+
emitSys.appendFile(mainFile.path, "\n// SomeComment");
206+
207+
// Emit on both the builders should result in same files
208+
verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram);
209+
});
210+
211+
it("noEmitOnError with composite writes the tsbuildinfo with pending affected files correctly", () => {
212+
const config: File = {
213+
path: `${projectRoot}/tsconfig.json`,
214+
content: JSON.stringify({ compilerOptions: { composite: true } })
215+
};
216+
const mainFile: File = {
217+
path: `${projectRoot}/main.ts`,
218+
content: "export const x: string = 10;"
219+
};
220+
const otherFile: File = {
221+
path: `${projectRoot}/other.ts`,
222+
content: "export const y = 10;"
223+
};
224+
const sys = createWatchedSystem([config, mainFile, otherFile, libFile]);
225+
const emitSys = createWatchedSystem([config, mainFile, otherFile, libFile]);
226+
227+
// Verify noEmit results in same output
228+
verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmitOnError: true });
229+
230+
// Change file
231+
sys.appendFile(mainFile.path, "\n// SomeComment");
232+
emitSys.appendFile(mainFile.path, "\n// SomeComment");
233+
234+
// Verify noEmit results in same output
235+
verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmitOnError: true });
236+
237+
// Fix error
238+
const fixed = "export const x = 10;";
239+
sys.appendFile(mainFile.path, fixed);
240+
emitSys.appendFile(mainFile.path, fixed);
241+
242+
// Emit on both the builders should result in same files
243+
verifyBuilder(config, sys, emitSys, createSemanticDiagnosticsBuilderProgram, createEmitAndSemanticDiagnosticsBuilderProgram, { noEmitOnError: true });
244+
});
245+
});
131246
}

0 commit comments

Comments
 (0)