Skip to content

Commit c3b2aea

Browse files
authored
Do not write files directly from builder when noEmitOnError is true (microsoft#34832)
* Add tests for noEmitOnError * Do not write files directly from builder when noEmitOnError is true Fixes microsoft#34823 * make linter happy * Instead of generating output in memory, check errors before doing the emit in case of noEmitOnError
1 parent ac5e10b commit c3b2aea

File tree

16 files changed

+271
-53
lines changed

16 files changed

+271
-53
lines changed

src/compiler/builder.ts

+14-12
Original file line numberDiff line numberDiff line change
@@ -882,36 +882,36 @@ namespace ts {
882882
oldProgram = undefined;
883883
oldState = undefined;
884884

885-
const result = createRedirectedBuilderProgram(state, configFileParsingDiagnostics);
886-
result.getState = () => state;
887-
result.backupState = () => {
885+
const builderProgram = createRedirectedBuilderProgram(state, configFileParsingDiagnostics);
886+
builderProgram.getState = () => state;
887+
builderProgram.backupState = () => {
888888
Debug.assert(backupState === undefined);
889889
backupState = cloneBuilderProgramState(state);
890890
};
891-
result.restoreState = () => {
891+
builderProgram.restoreState = () => {
892892
state = Debug.assertDefined(backupState);
893893
backupState = undefined;
894894
};
895-
result.getAllDependencies = sourceFile => BuilderState.getAllDependencies(state, Debug.assertDefined(state.program), sourceFile);
896-
result.getSemanticDiagnostics = getSemanticDiagnostics;
897-
result.emit = emit;
898-
result.releaseProgram = () => {
895+
builderProgram.getAllDependencies = sourceFile => BuilderState.getAllDependencies(state, Debug.assertDefined(state.program), sourceFile);
896+
builderProgram.getSemanticDiagnostics = getSemanticDiagnostics;
897+
builderProgram.emit = emit;
898+
builderProgram.releaseProgram = () => {
899899
releaseCache(state);
900900
backupState = undefined;
901901
};
902902

903903
if (kind === BuilderProgramKind.SemanticDiagnosticsBuilderProgram) {
904-
(result as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile;
904+
(builderProgram as SemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile;
905905
}
906906
else if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) {
907-
(result as EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile;
908-
(result as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile;
907+
(builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).getSemanticDiagnosticsOfNextAffectedFile = getSemanticDiagnosticsOfNextAffectedFile;
908+
(builderProgram as EmitAndSemanticDiagnosticsBuilderProgram).emitNextAffectedFile = emitNextAffectedFile;
909909
}
910910
else {
911911
notImplemented();
912912
}
913913

914-
return result;
914+
return builderProgram;
915915

916916
/**
917917
* Emits the next affected file's emit result (EmitResult and sourceFiles emitted) or returns undefined if iteration is complete
@@ -987,6 +987,8 @@ namespace ts {
987987
function emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult {
988988
if (kind === BuilderProgramKind.EmitAndSemanticDiagnosticsBuilderProgram) {
989989
assertSourceFileOkWithoutNextAffectedCall(state, targetSourceFile);
990+
const result = handleNoEmitOptions(builderProgram, targetSourceFile, cancellationToken);
991+
if (result) return result;
990992
if (!targetSourceFile) {
991993
// Emit and report any errors we ran into.
992994
let sourceMaps: SourceMapEmitResult[] = [];

src/compiler/program.ts

+29-30
Original file line numberDiff line numberDiff line change
@@ -1570,37 +1570,9 @@ namespace ts {
15701570
}
15711571

15721572
function emitWorker(program: Program, sourceFile: SourceFile | undefined, writeFileCallback: WriteFileCallback | undefined, cancellationToken: CancellationToken | undefined, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers, forceDtsEmit?: boolean): EmitResult {
1573-
let declarationDiagnostics: readonly Diagnostic[] = [];
1574-
15751573
if (!forceDtsEmit) {
1576-
if (options.noEmit) {
1577-
return { diagnostics: declarationDiagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true };
1578-
}
1579-
1580-
// If the noEmitOnError flag is set, then check if we have any errors so far. If so,
1581-
// immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we
1582-
// get any preEmit diagnostics, not just the ones
1583-
if (options.noEmitOnError) {
1584-
const diagnostics = [
1585-
...program.getOptionsDiagnostics(cancellationToken),
1586-
...program.getSyntacticDiagnostics(sourceFile, cancellationToken),
1587-
...program.getGlobalDiagnostics(cancellationToken),
1588-
...program.getSemanticDiagnostics(sourceFile, cancellationToken)
1589-
];
1590-
1591-
if (diagnostics.length === 0 && getEmitDeclarations(program.getCompilerOptions())) {
1592-
declarationDiagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken);
1593-
}
1594-
1595-
if (diagnostics.length > 0 || declarationDiagnostics.length > 0) {
1596-
return {
1597-
diagnostics: concatenate(diagnostics, declarationDiagnostics),
1598-
sourceMaps: undefined,
1599-
emittedFiles: undefined,
1600-
emitSkipped: true
1601-
};
1602-
}
1603-
}
1574+
const result = handleNoEmitOptions(program, sourceFile, cancellationToken);
1575+
if (result) return result;
16041576
}
16051577

16061578
// Create the emit resolver outside of the "emitTime" tracking code below. That way
@@ -3442,6 +3414,33 @@ namespace ts {
34423414
}
34433415
}
34443416

3417+
/*@internal*/
3418+
export function handleNoEmitOptions(program: ProgramToEmitFilesAndReportErrors, sourceFile: SourceFile | undefined, cancellationToken: CancellationToken | undefined): EmitResult | undefined {
3419+
const options = program.getCompilerOptions();
3420+
if (options.noEmit) {
3421+
return { diagnostics: emptyArray, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true };
3422+
}
3423+
3424+
// If the noEmitOnError flag is set, then check if we have any errors so far. If so,
3425+
// immediately bail out. Note that we pass 'undefined' for 'sourceFile' so that we
3426+
// get any preEmit diagnostics, not just the ones
3427+
if (!options.noEmitOnError) return undefined;
3428+
let diagnostics: readonly Diagnostic[] = [
3429+
...program.getOptionsDiagnostics(cancellationToken),
3430+
...program.getSyntacticDiagnostics(sourceFile, cancellationToken),
3431+
...program.getGlobalDiagnostics(cancellationToken),
3432+
...program.getSemanticDiagnostics(sourceFile, cancellationToken)
3433+
];
3434+
3435+
if (diagnostics.length === 0 && getEmitDeclarations(program.getCompilerOptions())) {
3436+
diagnostics = program.getDeclarationDiagnostics(/*sourceFile*/ undefined, cancellationToken);
3437+
}
3438+
3439+
return diagnostics.length > 0 ?
3440+
{ diagnostics, sourceMaps: undefined, emittedFiles: undefined, emitSkipped: true } :
3441+
undefined;
3442+
}
3443+
34453444
/*@internal*/
34463445
interface CompilerHostLike {
34473446
useCaseSensitiveFileNames(): boolean;

src/compiler/watch.ts

+12-10
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ namespace ts {
124124
getOptionsDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[];
125125
getGlobalDiagnostics(cancellationToken?: CancellationToken): readonly Diagnostic[];
126126
getSemanticDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly Diagnostic[];
127+
getDeclarationDiagnostics(sourceFile?: SourceFile, cancellationToken?: CancellationToken): readonly DiagnosticWithLocation[];
127128
getConfigFileParsingDiagnostics(): readonly Diagnostic[];
128129
emit(targetSourceFile?: SourceFile, writeFile?: WriteFileCallback, cancellationToken?: CancellationToken, emitOnlyDtsFiles?: boolean, customTransformers?: CustomTransformers): EmitResult;
129130
}
@@ -152,20 +153,20 @@ namespace ts {
152153
const isListFilesOnly = !!program.getCompilerOptions().listFilesOnly;
153154

154155
// First get and report any syntactic errors.
155-
const diagnostics = program.getConfigFileParsingDiagnostics().slice();
156-
const configFileParsingDiagnosticsLength = diagnostics.length;
157-
addRange(diagnostics, program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
156+
const allDiagnostics = program.getConfigFileParsingDiagnostics().slice();
157+
const configFileParsingDiagnosticsLength = allDiagnostics.length;
158+
addRange(allDiagnostics, program.getSyntacticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
158159

159160
// If we didn't have any syntactic errors, then also try getting the global and
160161
// semantic errors.
161-
if (diagnostics.length === configFileParsingDiagnosticsLength) {
162-
addRange(diagnostics, program.getOptionsDiagnostics(cancellationToken));
162+
if (allDiagnostics.length === configFileParsingDiagnosticsLength) {
163+
addRange(allDiagnostics, program.getOptionsDiagnostics(cancellationToken));
163164

164165
if (!isListFilesOnly) {
165-
addRange(diagnostics, program.getGlobalDiagnostics(cancellationToken));
166+
addRange(allDiagnostics, program.getGlobalDiagnostics(cancellationToken));
166167

167-
if (diagnostics.length === configFileParsingDiagnosticsLength) {
168-
addRange(diagnostics, program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
168+
if (allDiagnostics.length === configFileParsingDiagnosticsLength) {
169+
addRange(allDiagnostics, program.getSemanticDiagnostics(/*sourceFile*/ undefined, cancellationToken));
169170
}
170171
}
171172
}
@@ -175,9 +176,10 @@ namespace ts {
175176
? { emitSkipped: true, diagnostics: emptyArray }
176177
: program.emit(/*targetSourceFile*/ undefined, writeFile, cancellationToken, emitOnlyDtsFiles, customTransformers);
177178
const { emittedFiles, diagnostics: emitDiagnostics } = emitResult;
178-
addRange(diagnostics, emitDiagnostics);
179+
addRange(allDiagnostics, emitDiagnostics);
179180

180-
sortAndDeduplicateDiagnostics(diagnostics).forEach(reportDiagnostic);
181+
const diagnostics = sortAndDeduplicateDiagnostics(allDiagnostics);
182+
diagnostics.forEach(reportDiagnostic);
181183
if (writeFileName) {
182184
const currentDir = program.getCurrentDirectory();
183185
forEach(emittedFiles, file => {

src/testRunner/tsconfig.json

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@
118118
"unittests/tsbuild/lateBoundSymbol.ts",
119119
"unittests/tsbuild/missingExtendedFile.ts",
120120
"unittests/tsbuild/moduleSpecifiers.ts",
121+
"unittests/tsbuild/noEmitOnError.ts",
121122
"unittests/tsbuild/outFile.ts",
122123
"unittests/tsbuild/referencesWithRootDirInParent.ts",
123124
"unittests/tsbuild/resolveJsonModule.ts",

src/testRunner/unittests/tsbuild/exitCodeOnBogusFile.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ namespace ts {
44
verifyTsc({
55
scenario: "exitCodeOnBogusFile",
66
subScenario: `test exit code`,
7-
fs: () => loadProjectFromFiles({}, symbolLibContent),
7+
fs: () => loadProjectFromFiles({}),
88
commandLineArgs: ["-b", "bogus.json"]
99
});
1010
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace ts {
2+
describe("unittests:: tsbuild - with noEmitOnError", () => {
3+
let projFs: vfs.FileSystem;
4+
before(() => {
5+
projFs = loadProjectFromDisk("tests/projects/noEmitOnError");
6+
});
7+
after(() => {
8+
projFs = undefined!;
9+
});
10+
verifyTsc({
11+
scenario: "noEmitOnError",
12+
subScenario: "has empty files diagnostic when files is empty and no references are provided",
13+
fs: () => projFs,
14+
commandLineArgs: ["--b", "/src/tsconfig.json"],
15+
});
16+
});
17+
}

src/testRunner/unittests/tsbuild/watchMode.ts

+41
Original file line numberDiff line numberDiff line change
@@ -1389,4 +1389,45 @@ ${coreFiles[1].content}`);
13891389
);
13901390
}
13911391
});
1392+
1393+
describe("unittests:: tsbuild:: watchMode:: with noEmitOnError", () => {
1394+
it("does not emit any files on error", () => {
1395+
const projectLocation = `${projectsLocation}/noEmitOnError`;
1396+
const host = createTsBuildWatchSystem([
1397+
...["tsconfig.json", "shared/types/db.ts", "src/main.ts", "src/other.ts"]
1398+
.map(f => getFileFromProject("noEmitOnError", f)),
1399+
{ path: libFile.path, content: libContent }
1400+
], { currentDirectory: projectLocation });
1401+
createSolutionBuilderWithWatch(host, ["tsconfig.json"], { verbose: true, watch: true });
1402+
checkOutputErrorsInitial(host, [
1403+
`src/main.ts(4,1): error TS1005: ',' expected.\n`,
1404+
], /*disableConsoleClears*/ undefined, [
1405+
`Projects in this build: \r\n * tsconfig.json\n\n`,
1406+
`Project 'tsconfig.json' is out of date because output file 'dev-build/shared/types/db.js' does not exist\n\n`,
1407+
`Building project '/user/username/projects/noEmitOnError/tsconfig.json'...\n\n`,
1408+
]);
1409+
assert.equal(host.writtenFiles.size, 0, `Expected not to write any files: ${arrayFrom(host.writtenFiles.keys())}`);
1410+
1411+
// Make changes
1412+
host.writeFile(`${projectLocation}/src/main.ts`, `import { A } from "../shared/types/db";
1413+
const a = {
1414+
lastName: 'sdsd'
1415+
};`);
1416+
host.writtenFiles.clear();
1417+
host.checkTimeoutQueueLengthAndRun(1); // build project
1418+
host.checkTimeoutQueueLength(0);
1419+
checkOutputErrorsIncremental(host, emptyArray, /*disableConsoleClears*/ undefined, /*logsBeforeWatchDiagnostics*/ undefined, [
1420+
`Project 'tsconfig.json' is out of date because output file 'dev-build/shared/types/db.js' does not exist\n\n`,
1421+
`Building project '/user/username/projects/noEmitOnError/tsconfig.json'...\n\n`,
1422+
]);
1423+
assert.equal(host.writtenFiles.size, 3, `Expected to write 3 files: Actual:: ${arrayFrom(host.writtenFiles.keys())}`);
1424+
for (const f of [
1425+
`${projectLocation}/dev-build/shared/types/db.js`,
1426+
`${projectLocation}/dev-build/src/main.js`,
1427+
`${projectLocation}/dev-build/src/other.js`,
1428+
]) {
1429+
assert.isTrue(host.writtenFiles.has(f.toLowerCase()), `Expected to write file: ${f}:: Actual:: ${arrayFrom(host.writtenFiles.keys())}`);
1430+
}
1431+
});
1432+
});
13921433
}

src/testRunner/unittests/tsc/incremental.ts

+16
Original file line numberDiff line numberDiff line change
@@ -72,5 +72,21 @@ namespace ts {
7272
commandLineArgs: ["--p", "src/project"],
7373
incrementalScenarios: [noChangeRun]
7474
});
75+
76+
verifyTscIncrementalEdits({
77+
scenario: "incremental",
78+
subScenario: "with noEmitOnError",
79+
fs: () => loadProjectFromDisk("tests/projects/noEmitOnError"),
80+
commandLineArgs: ["--incremental", "-p", "src"],
81+
incrementalScenarios: [
82+
{
83+
buildKind: BuildKind.IncrementalDtsUnchanged,
84+
modifyFs: fs => fs.writeFileSync("/src/src/main.ts", `import { A } from "../shared/types/db";
85+
const a = {
86+
lastName: 'sdsd'
87+
};`, "utf-8")
88+
}
89+
]
90+
});
7591
});
7692
}

src/testRunner/unittests/tscWatch/emitAndErrorUpdates.ts

+42
Original file line numberDiff line numberDiff line change
@@ -392,5 +392,47 @@ export class Data2 {
392392
verifyTransitiveExports(lib2Data, lib2Data2);
393393
});
394394
});
395+
396+
it("with noEmitOnError", () => {
397+
const projectLocation = `${TestFSWithWatch.tsbuildProjectsLocation}/noEmitOnError`;
398+
const allFiles = ["tsconfig.json", "shared/types/db.ts", "src/main.ts", "src/other.ts"]
399+
.map(f => TestFSWithWatch.getTsBuildProjectFile("noEmitOnError", f));
400+
const host = TestFSWithWatch.changeToHostTrackingWrittenFiles(
401+
createWatchedSystem(
402+
[...allFiles, { path: libFile.path, content: libContent }],
403+
{ currentDirectory: projectLocation }
404+
)
405+
);
406+
const watch = createWatchOfConfigFile("tsconfig.json", host);
407+
const mainFile = allFiles.find(f => f.path === `${projectLocation}/src/main.ts`)!;
408+
checkOutputErrorsInitial(host, [
409+
getDiagnosticOfFileFromProgram(
410+
watch(),
411+
mainFile.path,
412+
mainFile.content.lastIndexOf(";"),
413+
1,
414+
Diagnostics._0_expected,
415+
","
416+
)
417+
]);
418+
assert.equal(host.writtenFiles.size, 0, `Expected not to write any files: ${arrayFrom(host.writtenFiles.keys())}`);
419+
420+
// Make changes
421+
host.writeFile(mainFile.path, `import { A } from "../shared/types/db";
422+
const a = {
423+
lastName: 'sdsd'
424+
};`);
425+
host.writtenFiles.clear();
426+
host.checkTimeoutQueueLengthAndRun(1); // build project
427+
checkOutputErrorsIncremental(host, emptyArray);
428+
assert.equal(host.writtenFiles.size, 3, `Expected to write 3 files: Actual:: ${arrayFrom(host.writtenFiles.keys())}`);
429+
for (const f of [
430+
`${projectLocation}/dev-build/shared/types/db.js`,
431+
`${projectLocation}/dev-build/src/main.js`,
432+
`${projectLocation}/dev-build/src/other.js`,
433+
]) {
434+
assert.isTrue(host.writtenFiles.has(f.toLowerCase()), `Expected to write file: ${f}:: Actual:: ${arrayFrom(host.writtenFiles.keys())}`);
435+
}
436+
});
395437
});
396438
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
//// [/lib/initial-buildOutput.txt]
2+
/lib/tsc --b /src/tsconfig.json
3+
src/src/main.ts(4,1): error TS1005: ',' expected.
4+
exitCode:: ExitStatus.DiagnosticsPresent_OutputsSkipped
5+
6+

0 commit comments

Comments
 (0)