Skip to content

Commit ef2024a

Browse files
committed
Handle circular project references
1 parent 5553f36 commit ef2024a

File tree

2 files changed

+97
-62
lines changed

2 files changed

+97
-62
lines changed

src/compiler/tsbuild.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ namespace ts {
6161
OutOfDateWithUpstream,
6262
UpstreamOutOfDate,
6363
UpstreamBlocked,
64+
ComputingUpstream,
6465

6566
/**
6667
* Projects with no outputs (i.e. "solution" files)
@@ -76,6 +77,7 @@ namespace ts {
7677
| Status.OutOfDateWithUpstream
7778
| Status.UpstreamOutOfDate
7879
| Status.UpstreamBlocked
80+
| Status.ComputingUpstream
7981
| Status.ContainerOnly;
8082

8183
export namespace Status {
@@ -145,6 +147,13 @@ namespace ts {
145147
upstreamProjectName: string;
146148
}
147149

150+
/**
151+
* Computing status of upstream projects referenced
152+
*/
153+
export interface ComputingUpstream {
154+
type: UpToDateStatusType.ComputingUpstream;
155+
}
156+
148157
/**
149158
* One or more of the project's outputs is older than the newest output of
150159
* an upstream project.
@@ -689,11 +698,17 @@ namespace ts {
689698
let usesPrepend = false;
690699
let upstreamChangedProject: string | undefined;
691700
if (project.projectReferences) {
701+
projectStatus.setValue(project.options.configFilePath as ResolvedConfigFileName, { type: UpToDateStatusType.ComputingUpstream });
692702
for (const ref of project.projectReferences) {
693703
usesPrepend = usesPrepend || !!(ref.prepend);
694704
const resolvedRef = resolveProjectReferencePath(ref);
695705
const refStatus = getUpToDateStatus(parseConfigFile(resolvedRef));
696706

707+
// Its a circular reference ignore the status of this project
708+
if (refStatus.type === UpToDateStatusType.ComputingUpstream) {
709+
continue;
710+
}
711+
697712
// An upstream project is blocked
698713
if (refStatus.type === UpToDateStatusType.Unbuildable) {
699714
return {
@@ -928,9 +943,10 @@ namespace ts {
928943
// Circular
929944
if (temporaryMarks.hasKey(projPath)) {
930945
if (!inCircularContext) {
946+
// TODO:: Do we report this as error?
931947
reportStatus(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n"));
932-
return;
933948
}
949+
return;
934950
}
935951

936952
temporaryMarks.setValue(projPath, true);
@@ -1263,6 +1279,8 @@ namespace ts {
12631279
status.reason);
12641280
case UpToDateStatusType.ContainerOnly:
12651281
// Don't report status on "solution" projects
1282+
case UpToDateStatusType.ComputingUpstream:
1283+
// Should never leak from getUptoDateStatusWorker
12661284
break;
12671285
default:
12681286
assertType<never>(status);

src/testRunner/unittests/tsbuildWatchMode.ts

Lines changed: 78 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ namespace ts.tscWatch {
9595
const allFiles: ReadonlyArray<File> = [libFile, ...core, ...logic, ...tests, ...ui];
9696
const testProjectExpectedWatchedFiles = [core[0], core[1], core[2], ...logic, ...tests].map(f => f.path);
9797

98-
function createSolutionInWatchMode() {
98+
function createSolutionInWatchMode(allFiles: ReadonlyArray<File>) {
9999
const host = createWatchedSystem(allFiles, { currentDirectory: projectsLocation });
100100
createSolutionBuilderWithWatch(host, [`${project}/${SubProject.tests}`]);
101101
verifyWatches(host);
@@ -114,7 +114,7 @@ namespace ts.tscWatch {
114114
}
115115

116116
it("creates solution in watch mode", () => {
117-
createSolutionInWatchMode();
117+
createSolutionInWatchMode(allFiles);
118118
});
119119

120120
describe("validates the changes and watched files", () => {
@@ -124,82 +124,99 @@ namespace ts.tscWatch {
124124
content: `export const newFileConst = 30;`
125125
};
126126

127-
function createSolutionInWatchModeToVerifyChanges(additionalFiles?: ReadonlyArray<[SubProject, string]>) {
128-
const host = createSolutionInWatchMode();
129-
return { host, verifyChangeWithFile, verifyChangeAfterTimeout, verifyWatches };
127+
function verifyProjectChanges(allFiles: ReadonlyArray<File>) {
128+
function createSolutionInWatchModeToVerifyChanges(additionalFiles?: ReadonlyArray<[SubProject, string]>) {
129+
const host = createSolutionInWatchMode(allFiles);
130+
return { host, verifyChangeWithFile, verifyChangeAfterTimeout, verifyWatches };
130131

131-
function verifyChangeWithFile(fileName: string, content: string) {
132-
const outputFileStamps = getOutputFileStamps(host, additionalFiles);
133-
host.writeFile(fileName, content);
134-
verifyChangeAfterTimeout(outputFileStamps);
135-
}
132+
function verifyChangeWithFile(fileName: string, content: string) {
133+
const outputFileStamps = getOutputFileStamps(host, additionalFiles);
134+
host.writeFile(fileName, content);
135+
verifyChangeAfterTimeout(outputFileStamps);
136+
}
136137

137-
function verifyChangeAfterTimeout(outputFileStamps: OutputFileStamp[]) {
138-
host.checkTimeoutQueueLengthAndRun(1); // Builds core
139-
const changedCore = getOutputFileStamps(host, additionalFiles);
140-
verifyChangedFiles(changedCore, outputFileStamps, [
141-
...getOutputFileNames(SubProject.core, "anotherModule"), // This should not be written really
142-
...getOutputFileNames(SubProject.core, "index"),
143-
...(additionalFiles ? getOutputFileNames(SubProject.core, newFileWithoutExtension) : emptyArray)
144-
]);
145-
host.checkTimeoutQueueLengthAndRun(1); // Builds logic
146-
const changedLogic = getOutputFileStamps(host, additionalFiles);
147-
verifyChangedFiles(changedLogic, changedCore, [
148-
...getOutputFileNames(SubProject.logic, "index") // Again these need not be written
149-
]);
150-
host.checkTimeoutQueueLengthAndRun(1); // Builds tests
151-
const changedTests = getOutputFileStamps(host, additionalFiles);
152-
verifyChangedFiles(changedTests, changedLogic, [
153-
...getOutputFileNames(SubProject.tests, "index") // Again these need not be written
154-
]);
155-
host.checkTimeoutQueueLength(0);
156-
checkOutputErrorsIncremental(host, emptyArray);
157-
verifyWatches();
158-
}
138+
function verifyChangeAfterTimeout(outputFileStamps: OutputFileStamp[]) {
139+
host.checkTimeoutQueueLengthAndRun(1); // Builds core
140+
const changedCore = getOutputFileStamps(host, additionalFiles);
141+
verifyChangedFiles(changedCore, outputFileStamps, [
142+
...getOutputFileNames(SubProject.core, "anotherModule"), // This should not be written really
143+
...getOutputFileNames(SubProject.core, "index"),
144+
...(additionalFiles ? getOutputFileNames(SubProject.core, newFileWithoutExtension) : emptyArray)
145+
]);
146+
host.checkTimeoutQueueLengthAndRun(1); // Builds logic
147+
const changedLogic = getOutputFileStamps(host, additionalFiles);
148+
verifyChangedFiles(changedLogic, changedCore, [
149+
...getOutputFileNames(SubProject.logic, "index") // Again these need not be written
150+
]);
151+
host.checkTimeoutQueueLengthAndRun(1); // Builds tests
152+
const changedTests = getOutputFileStamps(host, additionalFiles);
153+
verifyChangedFiles(changedTests, changedLogic, [
154+
...getOutputFileNames(SubProject.tests, "index") // Again these need not be written
155+
]);
156+
host.checkTimeoutQueueLength(0);
157+
checkOutputErrorsIncremental(host, emptyArray);
158+
verifyWatches();
159+
}
159160

160-
function verifyWatches() {
161-
checkWatchedFiles(host, additionalFiles ? testProjectExpectedWatchedFiles.concat(newFile.path) : testProjectExpectedWatchedFiles);
162-
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
163-
checkWatchedDirectories(host, [projectPath(SubProject.core), projectPath(SubProject.logic)], /*recursive*/ true);
161+
function verifyWatches() {
162+
checkWatchedFiles(host, additionalFiles ? testProjectExpectedWatchedFiles.concat(newFile.path) : testProjectExpectedWatchedFiles);
163+
checkWatchedDirectories(host, emptyArray, /*recursive*/ false);
164+
checkWatchedDirectories(host, [projectPath(SubProject.core), projectPath(SubProject.logic)], /*recursive*/ true);
165+
}
164166
}
165-
}
166167

167-
it("change builds changes and reports found errors message", () => {
168-
const { host, verifyChangeWithFile, verifyChangeAfterTimeout } = createSolutionInWatchModeToVerifyChanges();
169-
verifyChange(`${core[1].content}
168+
it("change builds changes and reports found errors message", () => {
169+
const { host, verifyChangeWithFile, verifyChangeAfterTimeout } = createSolutionInWatchModeToVerifyChanges();
170+
verifyChange(`${core[1].content}
170171
export class someClass { }`);
171172

172-
// Another change requeues and builds it
173-
verifyChange(core[1].content);
173+
// Another change requeues and builds it
174+
verifyChange(core[1].content);
174175

175-
// Two changes together report only single time message: File change detected. Starting incremental compilation...
176-
const outputFileStamps = getOutputFileStamps(host);
177-
const change1 = `${core[1].content}
176+
// Two changes together report only single time message: File change detected. Starting incremental compilation...
177+
const outputFileStamps = getOutputFileStamps(host);
178+
const change1 = `${core[1].content}
178179
export class someClass { }`;
179-
host.writeFile(core[1].path, change1);
180-
host.writeFile(core[1].path, `${change1}
180+
host.writeFile(core[1].path, change1);
181+
host.writeFile(core[1].path, `${change1}
181182
export class someClass2 { }`);
182-
verifyChangeAfterTimeout(outputFileStamps);
183+
verifyChangeAfterTimeout(outputFileStamps);
183184

184-
function verifyChange(coreContent: string) {
185-
verifyChangeWithFile(core[1].path, coreContent);
186-
}
187-
});
185+
function verifyChange(coreContent: string) {
186+
verifyChangeWithFile(core[1].path, coreContent);
187+
}
188+
});
188189

189-
it("builds when new file is added, and its subsequent updates", () => {
190-
const additinalFiles: ReadonlyArray<[SubProject, string]> = [[SubProject.core, newFileWithoutExtension]];
191-
const { verifyChangeWithFile } = createSolutionInWatchModeToVerifyChanges(additinalFiles);
192-
verifyChange(newFile.content);
190+
it("builds when new file is added, and its subsequent updates", () => {
191+
const additinalFiles: ReadonlyArray<[SubProject, string]> = [[SubProject.core, newFileWithoutExtension]];
192+
const { verifyChangeWithFile } = createSolutionInWatchModeToVerifyChanges(additinalFiles);
193+
verifyChange(newFile.content);
193194

194-
// Another change requeues and builds it
195-
verifyChange(`${newFile.content}
195+
// Another change requeues and builds it
196+
verifyChange(`${newFile.content}
196197
export class someClass2 { }`);
197198

198-
function verifyChange(newFileContent: string) {
199-
verifyChangeWithFile(newFile.path, newFileContent);
200-
}
199+
function verifyChange(newFileContent: string) {
200+
verifyChangeWithFile(newFile.path, newFileContent);
201+
}
202+
});
203+
}
204+
205+
describe("with simple project reference graph", () => {
206+
verifyProjectChanges(allFiles);
201207
});
202208

209+
describe("with circular project reference", () => {
210+
const [coreTsconfig, ...otherCoreFiles] = core;
211+
const circularCoreConfig: File = {
212+
path: coreTsconfig.path,
213+
content: JSON.stringify({
214+
compilerOptions: { composite: true, declaration: true },
215+
references: [{ path: "../tests", circular: true }]
216+
})
217+
};
218+
verifyProjectChanges([libFile, circularCoreConfig, ...otherCoreFiles, ...logic, ...tests]);
219+
});
203220
});
204221

205222
it("watches config files that are not present", () => {

0 commit comments

Comments
 (0)