mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-11 10:00:13 -06:00
Use better toposorting algorithm
This commit is contained in:
parent
1863d3fd48
commit
a7fcbcd3a4
@ -4,7 +4,7 @@ namespace ts {
|
||||
* specified like "./blah" to an absolute path to an actual
|
||||
* tsconfig file, e.g. "/root/blah/tsconfig.json"
|
||||
*/
|
||||
type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never };
|
||||
export type ResolvedConfigFileName = string & { _isResolvedConfigFileName: never };
|
||||
|
||||
const minimumDate = new Date(-8640000000000000);
|
||||
const maximumDate = new Date(8640000000000000);
|
||||
@ -42,7 +42,7 @@ namespace ts {
|
||||
|
||||
type Mapper = ReturnType<typeof createDependencyMapper>;
|
||||
interface DependencyGraph {
|
||||
buildQueue: ResolvedConfigFileName[][];
|
||||
buildQueue: ResolvedConfigFileName[];
|
||||
dependencyMap: Mapper;
|
||||
}
|
||||
|
||||
@ -409,7 +409,8 @@ namespace ts {
|
||||
getUpToDateStatusOfFile,
|
||||
buildProjects,
|
||||
cleanProjects,
|
||||
resetBuildContext
|
||||
resetBuildContext,
|
||||
getBuildGraph
|
||||
};
|
||||
|
||||
function resetBuildContext(opts = defaultOptions) {
|
||||
@ -420,6 +421,13 @@ namespace ts {
|
||||
return getUpToDateStatus(configFileCache.parseConfigFile(configFileName));
|
||||
}
|
||||
|
||||
function getBuildGraph(configFileNames: string[]) {
|
||||
const resolvedNames: ResolvedConfigFileName[] | undefined = resolveProjectNames(configFileNames);
|
||||
if (resolvedNames === undefined) return;
|
||||
|
||||
return createDependencyGraph(resolvedNames);
|
||||
}
|
||||
|
||||
function getUpToDateStatus(project: ParsedCommandLine | undefined): UpToDateStatus {
|
||||
if (project === undefined) {
|
||||
return { type: UpToDateStatusType.Unbuildable, reason: "File deleted mid-build" };
|
||||
@ -583,66 +591,62 @@ namespace ts {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Use the better algorithm
|
||||
function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph {
|
||||
// This is a list of list of projects that need to be built.
|
||||
// The ordering here is "backwards", i.e. the first entry in the array is the last set of projects that need to be built;
|
||||
// and the last entry is the first set of projects to be built.
|
||||
// Each subarray is effectively unordered.
|
||||
// We traverse the reference graph from each root, then "clean" the list by removing
|
||||
// any entry that is duplicated to its right.
|
||||
const buildQueue: ResolvedConfigFileName[][] = [];
|
||||
const dependencyMap = createDependencyMapper();
|
||||
let buildQueuePosition = 0;
|
||||
function createDependencyGraph(roots: ResolvedConfigFileName[]): DependencyGraph | undefined {
|
||||
const temporaryMarks: { [path: string]: true } = {};
|
||||
const permanentMarks: { [path: string]: true } = {};
|
||||
const circularityReportStack: string[] = [];
|
||||
const buildOrder: ResolvedConfigFileName[] = [];
|
||||
const graph = createDependencyMapper();
|
||||
|
||||
let hadError = false;
|
||||
|
||||
for (const root of roots) {
|
||||
const config = configFileCache.parseConfigFile(root);
|
||||
if (config === undefined) {
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_does_not_exist, root));
|
||||
continue;
|
||||
}
|
||||
enumerateReferences(normalizePath(root) as ResolvedConfigFileName, config);
|
||||
visit(root);
|
||||
}
|
||||
|
||||
if (hadError) {
|
||||
return undefined;
|
||||
}
|
||||
removeDuplicatesFromBuildQueue(buildQueue);
|
||||
|
||||
return {
|
||||
buildQueue,
|
||||
dependencyMap
|
||||
buildQueue: buildOrder,
|
||||
dependencyMap: graph
|
||||
};
|
||||
|
||||
function enumerateReferences(fileName: ResolvedConfigFileName, root: ParsedCommandLine): void {
|
||||
const myBuildLevel = buildQueue[buildQueuePosition] = buildQueue[buildQueuePosition] || [];
|
||||
if (myBuildLevel.indexOf(fileName) < 0) {
|
||||
myBuildLevel.push(fileName);
|
||||
}
|
||||
|
||||
const refs = root.projectReferences;
|
||||
if (refs === undefined) return;
|
||||
buildQueuePosition++;
|
||||
for (const ref of refs) {
|
||||
const actualPath = resolveProjectReferencePath(host, ref) as ResolvedConfigFileName;
|
||||
dependencyMap.addReference(fileName, actualPath);
|
||||
const resolvedRef = configFileCache.parseConfigFile(actualPath);
|
||||
if (resolvedRef === undefined) continue;
|
||||
enumerateReferences(normalizePath(actualPath) as ResolvedConfigFileName, resolvedRef);
|
||||
}
|
||||
buildQueuePosition--;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes entries from arrays which appear in later arrays.
|
||||
*/
|
||||
function removeDuplicatesFromBuildQueue(queue: string[][]): void {
|
||||
// No need to check the last array
|
||||
for (let i = 0; i < queue.length - 1; i++) {
|
||||
queue[i] = queue[i].filter(fn => !occursAfter(fn, i + 1));
|
||||
}
|
||||
|
||||
function occursAfter(s: string, start: number) {
|
||||
for (let i = start; i < queue.length; i++) {
|
||||
if (queue[i].indexOf(s) >= 0) return true;
|
||||
function visit(projPath: ResolvedConfigFileName, inCircularContext = false) {
|
||||
// Already visited
|
||||
if (permanentMarks[projPath]) return;
|
||||
// Circular
|
||||
if (temporaryMarks[projPath]) {
|
||||
if (!inCircularContext) {
|
||||
hadError = true;
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.Project_references_may_not_form_a_circular_graph_Cycle_detected_Colon_0, circularityReportStack.join("\r\n")));
|
||||
return;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
temporaryMarks[projPath] = true;
|
||||
circularityReportStack.push(projPath);
|
||||
const parsed = configFileCache.parseConfigFile(projPath);
|
||||
if (parsed === undefined) {
|
||||
hadError = true;
|
||||
return;
|
||||
}
|
||||
if (parsed.projectReferences) {
|
||||
for (const ref of parsed.projectReferences) {
|
||||
const resolvedRefPath = resolveProjectName(ref.path);
|
||||
if (resolvedRefPath === undefined) {
|
||||
hadError = true;
|
||||
break;
|
||||
}
|
||||
visit(resolvedRefPath, inCircularContext || ref.circular);
|
||||
graph.addReference(projPath, resolvedRefPath);
|
||||
}
|
||||
}
|
||||
|
||||
circularityReportStack.pop();
|
||||
permanentMarks[projPath] = true;
|
||||
buildOrder.push(projPath);
|
||||
}
|
||||
}
|
||||
|
||||
@ -762,20 +766,19 @@ namespace ts {
|
||||
|
||||
// Get the same graph for cleaning we'd use for building
|
||||
const graph = createDependencyGraph(resolvedNames);
|
||||
if (graph === undefined) return undefined;
|
||||
|
||||
const filesToDelete: string[] = [];
|
||||
for (const level of graph.buildQueue) {
|
||||
for (const proj of level) {
|
||||
const parsed = configFileCache.parseConfigFile(proj);
|
||||
if (parsed === undefined) {
|
||||
// File has gone missing; fine to ignore here
|
||||
continue;
|
||||
}
|
||||
const outputs = getAllProjectOutputs(parsed);
|
||||
for (const output of outputs) {
|
||||
if (host.fileExists(output)) {
|
||||
filesToDelete.push(output);
|
||||
}
|
||||
for (const proj of graph.buildQueue) {
|
||||
const parsed = configFileCache.parseConfigFile(proj);
|
||||
if (parsed === undefined) {
|
||||
// File has gone missing; fine to ignore here
|
||||
continue;
|
||||
}
|
||||
const outputs = getAllProjectOutputs(parsed);
|
||||
for (const output of outputs) {
|
||||
if (host.fileExists(output)) {
|
||||
filesToDelete.push(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -805,21 +808,27 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function resolveProjectName(name: string): ResolvedConfigFileName | undefined {
|
||||
let fullPath = resolvePath(host.getCurrentDirectory(), name);
|
||||
if (host.fileExists(fullPath)) {
|
||||
return fullPath as ResolvedConfigFileName;
|
||||
}
|
||||
fullPath = combinePaths(fullPath, "tsconfig.json");
|
||||
if (host.fileExists(fullPath)) {
|
||||
return fullPath as ResolvedConfigFileName;
|
||||
}
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveProjectNames(configFileNames: string[]): ResolvedConfigFileName[] | undefined {
|
||||
const resolvedNames: ResolvedConfigFileName[] = [];
|
||||
for (const name of configFileNames) {
|
||||
let fullPath = resolvePath(host.getCurrentDirectory(), name);
|
||||
if (host.fileExists(fullPath)) {
|
||||
resolvedNames.push(fullPath as ResolvedConfigFileName);
|
||||
continue;
|
||||
const resolved = resolveProjectName(name);
|
||||
if (resolved === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
fullPath = combinePaths(fullPath, "tsconfig.json");
|
||||
if (host.fileExists(fullPath)) {
|
||||
resolvedNames.push(fullPath as ResolvedConfigFileName);
|
||||
continue;
|
||||
}
|
||||
reportDiagnostic(createCompilerDiagnostic(Diagnostics.File_0_not_found, fullPath));
|
||||
return undefined;
|
||||
resolvedNames.push(resolved);
|
||||
}
|
||||
return resolvedNames;
|
||||
}
|
||||
@ -830,12 +839,12 @@ namespace ts {
|
||||
|
||||
// Establish what needs to be built
|
||||
const graph = createDependencyGraph(resolvedNames);
|
||||
if (graph === undefined) return;
|
||||
|
||||
const queue = graph.buildQueue;
|
||||
reportBuildQueue(graph);
|
||||
|
||||
let next: ResolvedConfigFileName | undefined;
|
||||
while (next = getNext()) {
|
||||
for (const next of queue) {
|
||||
const proj = configFileCache.parseConfigFile(next);
|
||||
if (proj === undefined) {
|
||||
break;
|
||||
@ -866,21 +875,6 @@ namespace ts {
|
||||
|
||||
buildSingleProject(next);
|
||||
}
|
||||
|
||||
function getNext(): ResolvedConfigFileName | undefined {
|
||||
if (queue.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
while (queue.length > 0) {
|
||||
const last = queue[queue.length - 1];
|
||||
if (last.length === 0) {
|
||||
queue.pop();
|
||||
continue;
|
||||
}
|
||||
return last.pop()!;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -890,12 +884,9 @@ namespace ts {
|
||||
if (!context.options.verbose) return;
|
||||
|
||||
const names: string[] = [];
|
||||
for (const level of graph.buildQueue) {
|
||||
for (const el of level) {
|
||||
names.push(el);
|
||||
}
|
||||
for (const name of graph.buildQueue) {
|
||||
names.push(name);
|
||||
}
|
||||
names.reverse();
|
||||
context.verbose(Diagnostics.Sorted_list_of_input_projects_Colon_0, names.map(s => "\r\n * " + s).join(""));
|
||||
}
|
||||
|
||||
|
||||
@ -207,25 +207,49 @@ namespace ts {
|
||||
});
|
||||
|
||||
describe("tsbuild - graph-ordering", () => {
|
||||
it("orders the graph correctly", () => {
|
||||
const fs = new vfs.FileSystem(false);
|
||||
const host = new fakes.CompilerHost(fs);
|
||||
const deps: [string, string][] = [
|
||||
["A", "B"],
|
||||
["B", "C"],
|
||||
["A", "C"],
|
||||
["B", "D"],
|
||||
["C", "D"],
|
||||
["C", "E"],
|
||||
["F", "E"]
|
||||
];
|
||||
const fs = new vfs.FileSystem(false);
|
||||
const host = new fakes.CompilerHost(fs);
|
||||
const deps: [string, string][] = [
|
||||
["A", "B"],
|
||||
["B", "C"],
|
||||
["A", "C"],
|
||||
["B", "D"],
|
||||
["C", "D"],
|
||||
["C", "E"],
|
||||
["F", "E"]
|
||||
];
|
||||
|
||||
writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps);
|
||||
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false });
|
||||
builder.buildProjects(["/project/A", "/project/G"]);
|
||||
printDiagnostics();
|
||||
writeProjects(fs, ["A", "B", "C", "D", "E", "F", "G"], deps);
|
||||
|
||||
const builder = createSolutionBuilder(host, reportDiagnostic, { dry: true, force: false, verbose: false });
|
||||
|
||||
it("orders the graph correctly - specify two roots", () => {
|
||||
checkGraphOrdering(["A", "G"], ["A", "B", "C", "D", "E", "G"]);
|
||||
});
|
||||
|
||||
it("orders the graph correctly - multiple parts of the same graph in various orders", () => {
|
||||
// TODO add cases here
|
||||
});
|
||||
|
||||
function checkGraphOrdering(rootNames: string[], expectedBuildSet: string[]) {
|
||||
const projFileNames = rootNames.map(getProjectFileName);
|
||||
const graph = builder.getBuildGraph(projFileNames);
|
||||
if (graph === undefined) throw new Error("Graph shouldn't be undefined");
|
||||
|
||||
assert.sameMembers(graph.buildQueue, expectedBuildSet.map(getProjectFileName));
|
||||
|
||||
for (const dep of deps) {
|
||||
const child = getProjectFileName(dep[0]);
|
||||
if (graph.buildQueue.indexOf(child) < 0) continue;
|
||||
const parent = getProjectFileName(dep[1]);
|
||||
assert.isAbove(graph.buildQueue.indexOf(child), graph.buildQueue.indexOf(parent), `Expecting child ${child} to be built after parent ${parent}`);
|
||||
}
|
||||
}
|
||||
|
||||
function getProjectFileName(proj: string) {
|
||||
return `/project/${proj}/tsconfig.json` as ResolvedConfigFileName;
|
||||
}
|
||||
|
||||
function writeProjects(fileSystem: vfs.FileSystem, projectNames: string[], deps: [string, string][]): string[] {
|
||||
const projFileNames: string[] = [];
|
||||
for (const dep of deps) {
|
||||
@ -235,7 +259,7 @@ namespace ts {
|
||||
for (const proj of projectNames) {
|
||||
fileSystem.mkdirpSync(`/project/${proj}`);
|
||||
fileSystem.writeFileSync(`/project/${proj}/${proj}.ts`, "export {}");
|
||||
const configFileName = `/project/${proj}/tsconfig.json`;
|
||||
const configFileName = getProjectFileName(proj);
|
||||
const configContent = JSON.stringify({
|
||||
compilerOptions: { composite: true },
|
||||
files: [`./${proj}.ts`],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user