Skip to content

Commit 94d54b9

Browse files
committed
Circular reference in the build queue is build stoppable error
1 parent d9ad559 commit 94d54b9

File tree

7 files changed

+222
-44
lines changed

7 files changed

+222
-44
lines changed

src/compiler/tsbuild.ts

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,26 @@ namespace ts {
273273
export interface SolutionBuilderWithWatchHost<T extends BuilderProgram> extends SolutionBuilderHostBase<T>, WatchHost {
274274
}
275275

276+
/*@internal*/
277+
export type BuildOrder = readonly ResolvedConfigFileName[];
278+
/*@internal*/
279+
export interface CircularBuildOrder {
280+
buildOrder: BuildOrder;
281+
circularDiagnostics: readonly Diagnostic[];
282+
}
283+
/*@internal*/
284+
export type AnyBuildOrder = BuildOrder | CircularBuildOrder;
285+
286+
/*@internal*/
287+
export function isCircularBuildOrder(buildOrder: AnyBuildOrder): buildOrder is CircularBuildOrder {
288+
return !!buildOrder && !!(buildOrder as CircularBuildOrder).buildOrder;
289+
}
290+
291+
/*@internal*/
292+
export function getBuildOrderFromAnyBuildOrder(anyBuildOrder: AnyBuildOrder): BuildOrder {
293+
return isCircularBuildOrder(anyBuildOrder) ? anyBuildOrder.buildOrder : anyBuildOrder;
294+
}
295+
276296
export interface SolutionBuilder<T extends BuilderProgram> {
277297
build(project?: string, cancellationToken?: CancellationToken): ExitStatus;
278298
clean(project?: string): ExitStatus;
@@ -281,7 +301,7 @@ namespace ts {
281301
getNextInvalidatedProject(cancellationToken?: CancellationToken): InvalidatedProject<T> | undefined;
282302

283303
// Currently used for testing but can be made public if needed:
284-
/*@internal*/ getBuildOrder(): ReadonlyArray<ResolvedConfigFileName>;
304+
/*@internal*/ getBuildOrder(): AnyBuildOrder;
285305

286306
// Testing only
287307
/*@internal*/ getUpToDateStatusOfProject(project: string): UpToDateStatus;
@@ -379,7 +399,7 @@ namespace ts {
379399
readonly moduleResolutionCache: ModuleResolutionCache | undefined;
380400

381401
// Mutable state
382-
buildOrder: readonly ResolvedConfigFileName[] | undefined;
402+
buildOrder: AnyBuildOrder | undefined;
383403
readFileWithCache: (f: string) => string | undefined;
384404
projectCompilerOptions: CompilerOptions;
385405
cache: SolutionBuilderStateCache | undefined;
@@ -523,16 +543,19 @@ namespace ts {
523543
return resolveConfigFileProjectName(resolvePath(state.currentDirectory, name));
524544
}
525545

526-
function createBuildOrder(state: SolutionBuilderState, roots: readonly ResolvedConfigFileName[]): readonly ResolvedConfigFileName[] {
546+
function createBuildOrder(state: SolutionBuilderState, roots: readonly ResolvedConfigFileName[]): AnyBuildOrder {
527547
const temporaryMarks = createMap() as ConfigFileMap<true>;
528548
const permanentMarks = createMap() as ConfigFileMap<true>;
529549
const circularityReportStack: string[] = [];
530550
let buildOrder: ResolvedConfigFileName[] | undefined;
551+
let circularDiagnostics: Diagnostic[] | undefined;
531552
for (const root of roots) {
532553
visit(root);
533554
}
534555

535-
return buildOrder || emptyArray;
556+
return circularDiagnostics ?
557+
{ buildOrder: buildOrder || emptyArray, circularDiagnostics } :
558+
buildOrder || emptyArray;
536559

537560
function visit(configFileName: ResolvedConfigFileName, inCircularContext?: boolean) {
538561
const projPath = toResolvedConfigFilePath(state, configFileName);
@@ -541,8 +564,12 @@ namespace ts {
541564
// Circular
542565
if (temporaryMarks.has(projPath)) {
543566
if (!inCircularContext) {
544-
// TODO:: Do we report this as error?
545-
reportStatus(state, Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n"));
567+
(circularDiagnostics || (circularDiagnostics = [])).push(
568+
createCompilerDiagnostic(
569+
Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0,
570+
circularityReportStack.join("\r\n")
571+
)
572+
);
546573
}
547574
return;
548575
}
@@ -569,12 +596,11 @@ namespace ts {
569596

570597
function createStateBuildOrder(state: SolutionBuilderState) {
571598
const buildOrder = createBuildOrder(state, state.rootNames.map(f => resolveProjectName(state, f)));
572-
if (arrayIsEqualTo(state.buildOrder, buildOrder)) return state.buildOrder!;
573599

574600
// Clear all to ResolvedConfigFilePaths cache to start fresh
575601
state.resolvedConfigFilePaths.clear();
576602
const currentProjects = arrayToSet(
577-
buildOrder,
603+
getBuildOrderFromAnyBuildOrder(buildOrder),
578604
resolved => toResolvedConfigFilePath(state, resolved)
579605
) as ConfigFileMap<true>;
580606

@@ -611,9 +637,10 @@ namespace ts {
611637
return state.buildOrder = buildOrder;
612638
}
613639

614-
function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined) {
640+
function getBuildOrderFor(state: SolutionBuilderState, project: string | undefined, onlyReferences: boolean | undefined): AnyBuildOrder | undefined {
615641
const resolvedProject = project && resolveProjectName(state, project);
616642
const buildOrderFromState = getBuildOrder(state);
643+
if (isCircularBuildOrder(buildOrderFromState)) return buildOrderFromState;
617644
if (resolvedProject) {
618645
const projectPath = toResolvedConfigFilePath(state, resolvedProject);
619646
const projectIndex = findIndex(
@@ -622,7 +649,8 @@ namespace ts {
622649
);
623650
if (projectIndex === -1) return undefined;
624651
}
625-
const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) : buildOrderFromState;
652+
const buildOrder = resolvedProject ? createBuildOrder(state, [resolvedProject]) as BuildOrder : buildOrderFromState;
653+
Debug.assert(!isCircularBuildOrder(buildOrder));
626654
Debug.assert(!onlyReferences || resolvedProject !== undefined);
627655
Debug.assert(!onlyReferences || buildOrder[buildOrder.length - 1] === resolvedProject);
628656
return onlyReferences ? buildOrder.slice(0, buildOrder.length - 1) : buildOrder;
@@ -702,7 +730,7 @@ namespace ts {
702730
state.allProjectBuildPending = false;
703731
if (state.options.watch) { reportWatchStatus(state, Diagnostics.Starting_compilation_in_watch_mode); }
704732
enableCache(state);
705-
const buildOrder = getBuildOrder(state);
733+
const buildOrder = getBuildOrderFromAnyBuildOrder(getBuildOrder(state));
706734
buildOrder.forEach(configFileName =>
707735
state.projectPendingBuild.set(
708736
toResolvedConfigFilePath(state, configFileName),
@@ -1237,10 +1265,11 @@ namespace ts {
12371265

12381266
function getNextInvalidatedProject<T extends BuilderProgram>(
12391267
state: SolutionBuilderState<T>,
1240-
buildOrder: readonly ResolvedConfigFileName[],
1268+
buildOrder: AnyBuildOrder,
12411269
reportQueue: boolean
12421270
): InvalidatedProject<T> | undefined {
12431271
if (!state.projectPendingBuild.size) return undefined;
1272+
if (isCircularBuildOrder(buildOrder)) return undefined;
12441273
if (state.currentInvalidatedProject) {
12451274
// Only if same buildOrder the currentInvalidated project can be sent again
12461275
return arrayIsEqualTo(state.currentInvalidatedProject.buildOrder, buildOrder) ?
@@ -1763,17 +1792,24 @@ namespace ts {
17631792
reportErrorSummary(state, buildOrder);
17641793
startWatching(state, buildOrder);
17651794

1766-
return errorProjects ?
1767-
successfulProjects ?
1768-
ExitStatus.DiagnosticsPresent_OutputsGenerated :
1769-
ExitStatus.DiagnosticsPresent_OutputsSkipped :
1770-
ExitStatus.Success;
1795+
return isCircularBuildOrder(buildOrder) ?
1796+
ExitStatus.ProjectReferenceCycle_OutputsSkupped :
1797+
errorProjects ?
1798+
successfulProjects ?
1799+
ExitStatus.DiagnosticsPresent_OutputsGenerated :
1800+
ExitStatus.DiagnosticsPresent_OutputsSkipped :
1801+
ExitStatus.Success;
17711802
}
17721803

17731804
function clean(state: SolutionBuilderState, project?: string, onlyReferences?: boolean) {
17741805
const buildOrder = getBuildOrderFor(state, project, onlyReferences);
17751806
if (!buildOrder) return ExitStatus.InvalidProject_OutputsSkipped;
17761807

1808+
if (isCircularBuildOrder(buildOrder)) {
1809+
reportErrors(state, buildOrder.circularDiagnostics);
1810+
return ExitStatus.ProjectReferenceCycle_OutputsSkupped;
1811+
}
1812+
17771813
const { options, host } = state;
17781814
const filesToDelete = options.dry ? [] as string[] : undefined;
17791815
for (const proj of buildOrder) {
@@ -1955,10 +1991,10 @@ namespace ts {
19551991
);
19561992
}
19571993

1958-
function startWatching(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]) {
1994+
function startWatching(state: SolutionBuilderState, buildOrder: AnyBuildOrder) {
19591995
if (!state.watchAllProjectsPending) return;
19601996
state.watchAllProjectsPending = false;
1961-
for (const resolved of buildOrder) {
1997+
for (const resolved of getBuildOrderFromAnyBuildOrder(buildOrder)) {
19621998
const resolvedPath = toResolvedConfigFilePath(state, resolved);
19631999
// Watch this file
19642000
watchConfigFile(state, resolved, resolvedPath);
@@ -2032,24 +2068,33 @@ namespace ts {
20322068
reportAndStoreErrors(state, proj, [state.configFileCache.get(proj) as Diagnostic]);
20332069
}
20342070

2035-
function reportErrorSummary(state: SolutionBuilderState, buildOrder: readonly ResolvedConfigFileName[]) {
2036-
if (!state.needsSummary || (!state.watch && !state.host.reportErrorSummary)) return;
2071+
function reportErrorSummary(state: SolutionBuilderState, buildOrder: AnyBuildOrder) {
2072+
if (!state.needsSummary) return;
20372073
state.needsSummary = false;
2074+
const canReportSummary = state.watch || !!state.host.reportErrorSummary;
20382075
const { diagnostics } = state;
2039-
// Report errors from the other projects
2040-
buildOrder.forEach(project => {
2041-
const projectPath = toResolvedConfigFilePath(state, project);
2042-
if (!state.projectErrorsReported.has(projectPath)) {
2043-
reportErrors(state, diagnostics.get(projectPath) || emptyArray);
2044-
}
2045-
});
20462076
let totalErrors = 0;
2047-
diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors));
2077+
if (isCircularBuildOrder(buildOrder)) {
2078+
reportBuildQueue(state, buildOrder.buildOrder);
2079+
reportErrors(state, buildOrder.circularDiagnostics);
2080+
if (canReportSummary) totalErrors += getErrorCountForSummary(buildOrder.circularDiagnostics);
2081+
}
2082+
else {
2083+
// Report errors from the other projects
2084+
buildOrder.forEach(project => {
2085+
const projectPath = toResolvedConfigFilePath(state, project);
2086+
if (!state.projectErrorsReported.has(projectPath)) {
2087+
reportErrors(state, diagnostics.get(projectPath) || emptyArray);
2088+
}
2089+
});
2090+
if (canReportSummary) diagnostics.forEach(singleProjectErrors => totalErrors += getErrorCountForSummary(singleProjectErrors));
2091+
}
2092+
20482093
if (state.watch) {
20492094
reportWatchStatus(state, getWatchErrorSummaryDiagnosticMessage(totalErrors), totalErrors);
20502095
}
2051-
else {
2052-
state.host.reportErrorSummary!(totalErrors);
2096+
else if (state.host.reportErrorSummary) {
2097+
state.host.reportErrorSummary(totalErrors);
20532098
}
20542099
}
20552100

src/compiler/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3079,6 +3079,9 @@ namespace ts {
30793079

30803080
// When build skipped because passed in project is invalid
30813081
InvalidProject_OutputsSkipped = 3,
3082+
3083+
// When build is skipped because project references form cycle
3084+
ProjectReferenceCycle_OutputsSkupped = 4,
30823085
}
30833086

30843087
export interface EmitResult {

src/testRunner/unittests/tsbuild/demo.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,36 @@ namespace ts {
7676
notExpectedOutputs: emptyArray
7777
});
7878
});
79+
80+
it("in circular branch reports the error about it by stopping build", () => {
81+
verifyBuild({
82+
modifyDiskLayout: fs => replaceText(
83+
fs,
84+
"/src/core/tsconfig.json",
85+
"}",
86+
`},
87+
"references": [
88+
{
89+
"path": "../zoo"
90+
}
91+
]`
92+
),
93+
expectedExitStatus: ExitStatus.ProjectReferenceCycle_OutputsSkupped,
94+
expectedDiagnostics: [
95+
getExpectedDiagnosticForProjectsInBuild("src/animals/tsconfig.json", "src/zoo/tsconfig.json", "src/core/tsconfig.json", "src/tsconfig.json"),
96+
[
97+
Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0,
98+
[
99+
"/src/tsconfig.json",
100+
"/src/core/tsconfig.json",
101+
"/src/zoo/tsconfig.json",
102+
"/src/animals/tsconfig.json"
103+
].join("\r\n")
104+
]
105+
],
106+
expectedOutputs: emptyArray,
107+
notExpectedOutputs: [...coreOutputs(), ...animalOutputs(), ...zooOutputs()]
108+
});
109+
});
79110
});
80111
}

src/testRunner/unittests/tsbuild/graphOrdering.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ namespace ts {
88
["B", "D"],
99
["C", "D"],
1010
["C", "E"],
11-
["F", "E"]
11+
["F", "E"],
12+
["H", "I"],
13+
["I", "J"],
14+
["J", "H"],
15+
["J", "E"]
1216
];
1317

1418
before(() => {
1519
const fs = new vfs.FileSystem(false);
1620
host = new fakes.SolutionBuilderHost(fs);
17-
writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps);
21+
writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"], deps);
1822
});
1923

2024
after(() => {
@@ -37,17 +41,25 @@ namespace ts {
3741
checkGraphOrdering(["F", "C", "A"], ["E", "F", "D", "C", "B", "A"]);
3842
});
3943

40-
function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) {
41-
const builder = createSolutionBuilder(host!, rootNames.map(getProjectFileName), { dry: true, force: false, verbose: false });
42-
const buildQueue = builder.getBuildOrder();
44+
it("returns circular order", () => {
45+
checkGraphOrdering(["H"], ["E", "J", "I", "H"], /*circular*/ true);
46+
checkGraphOrdering(["A", "H"], ["D", "E", "C", "B", "A", "J", "I", "H"], /*circular*/ true);
47+
});
4348

49+
function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[], circular?: true) {
50+
const builder = createSolutionBuilder(host!, rootNames.map(getProjectFileName), { dry: true, force: false, verbose: false });
51+
const buildOrder = builder.getBuildOrder();
52+
assert.equal(isCircularBuildOrder(buildOrder), !!circular);
53+
const buildQueue = getBuildOrderFromAnyBuildOrder(buildOrder);
4454
assert.deepEqual(buildQueue, expectedBuildSet.map(getProjectFileName));
4555

46-
for (const dep of deps) {
47-
const child = getProjectFileName(dep[0]);
48-
if (buildQueue.indexOf(child) < 0) continue;
49-
const parent = getProjectFileName(dep[1]);
50-
assert.isAbove(buildQueue.indexOf(child), buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`);
56+
if (!circular) {
57+
for (const dep of deps) {
58+
const child = getProjectFileName(dep[0]);
59+
if (buildQueue.indexOf(child) < 0) continue;
60+
const parent = getProjectFileName(dep[1]);
61+
assert.isAbove(buildQueue.indexOf(child), buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`);
62+
}
5163
}
5264
}
5365

0 commit comments

Comments
 (0)