Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/checkCyclicDependencies.ts
13379 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import fs from 'fs';
7
import path from 'path';
8
import * as ts from 'typescript';
9
10
// --- Graph (extracted from build/lib/tsb/utils.ts) ---
11
12
export class Node {
13
readonly incoming = new Map<string, Node>();
14
readonly outgoing = new Map<string, Node>();
15
16
readonly data: string;
17
18
constructor(data: string) {
19
this.data = data;
20
}
21
}
22
23
export class Graph {
24
private _nodes = new Map<string, Node>();
25
26
inertEdge(from: string, to: string): void {
27
const fromNode = this.lookupOrInsertNode(from);
28
const toNode = this.lookupOrInsertNode(to);
29
fromNode.outgoing.set(toNode.data, toNode);
30
toNode.incoming.set(fromNode.data, fromNode);
31
}
32
33
lookupOrInsertNode(data: string): Node {
34
let node = this._nodes.get(data);
35
if (!node) {
36
node = new Node(data);
37
this._nodes.set(data, node);
38
}
39
return node;
40
}
41
42
lookup(data: string): Node | undefined {
43
return this._nodes.get(data);
44
}
45
46
findCycles(allData: string[]): Map<string, string[] | undefined> {
47
const result = new Map<string, string[] | undefined>();
48
const checked = new Set<string>();
49
for (const data of allData) {
50
const node = this.lookup(data);
51
if (!node) {
52
continue;
53
}
54
const cycle = this._findCycle(node, checked, new Set());
55
result.set(node.data, cycle);
56
}
57
return result;
58
}
59
60
private _findCycle(node: Node, checked: Set<string>, seen: Set<string>): string[] | undefined {
61
if (checked.has(node.data)) {
62
return undefined;
63
}
64
for (const child of node.outgoing.values()) {
65
if (seen.has(child.data)) {
66
const seenArr = Array.from(seen);
67
const idx = seenArr.indexOf(child.data);
68
seenArr.push(child.data);
69
return idx > 0 ? seenArr.slice(idx) : seenArr;
70
}
71
seen.add(child.data);
72
const result = this._findCycle(child, checked, seen);
73
seen.delete(child.data);
74
if (result) {
75
return result;
76
}
77
}
78
checked.add(node.data);
79
return undefined;
80
}
81
}
82
83
// --- Dependency scanning & cycle detection ---
84
85
export function normalize(p: string): string {
86
return p.replace(/\\/g, '/');
87
}
88
89
export function collectJsFiles(dir: string): string[] {
90
const results: string[] = [];
91
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
92
const full = path.join(dir, entry.name);
93
if (entry.isDirectory()) {
94
results.push(...collectJsFiles(full));
95
} else if (entry.isFile() && entry.name.endsWith('.js')) {
96
results.push(full);
97
}
98
}
99
return results;
100
}
101
102
export function processFile(filename: string, graph: Graph): void {
103
const content = fs.readFileSync(filename, 'utf-8');
104
const info = ts.preProcessFile(content, true);
105
106
for (const ref of info.importedFiles) {
107
if (!ref.fileName.startsWith('.')) {
108
continue; // skip node_modules
109
}
110
if (ref.fileName.endsWith('.css')) {
111
continue;
112
}
113
114
const dir = path.dirname(filename);
115
let resolvedPath = path.resolve(dir, ref.fileName);
116
if (resolvedPath.endsWith('.js')) {
117
resolvedPath = resolvedPath.slice(0, -3);
118
}
119
const normalizedResolved = normalize(resolvedPath);
120
121
if (fs.existsSync(normalizedResolved + '.js')) {
122
graph.inertEdge(normalize(filename), normalizedResolved + '.js');
123
} else if (fs.existsSync(normalizedResolved + '.ts')) {
124
graph.inertEdge(normalize(filename), normalizedResolved + '.ts');
125
}
126
}
127
}
128
129
function main(): void {
130
const folder = process.argv[2];
131
if (!folder) {
132
console.error('Usage: node build/lib/checkCyclicDependencies.ts <folder>');
133
process.exit(1);
134
}
135
136
const rootDir = path.resolve(folder);
137
if (!fs.existsSync(rootDir) || !fs.statSync(rootDir).isDirectory()) {
138
console.error(`Not a directory: ${rootDir}`);
139
process.exit(1);
140
}
141
142
const files = collectJsFiles(rootDir);
143
const graph = new Graph();
144
145
for (const file of files) {
146
processFile(file, graph);
147
}
148
149
const allNormalized = files.map(normalize).sort((a, b) => a.localeCompare(b));
150
const cycles = graph.findCycles(allNormalized);
151
152
const cyclicPaths = new Set<string>();
153
for (const [_filename, cycle] of cycles) {
154
if (cycle) {
155
const path = cycle.join(' -> ');
156
if (cyclicPaths.has(path)) {
157
continue;
158
}
159
cyclicPaths.add(path);
160
console.error(`CYCLIC dependency: ${path}`);
161
}
162
}
163
164
if (cyclicPaths.size > 0) {
165
process.exit(1);
166
} else {
167
console.log(`No cyclic dependencies found in ${files.length} files.`);
168
}
169
}
170
171
if (process.argv[1] && normalize(path.resolve(process.argv[1])).endsWith('checkCyclicDependencies.ts')) {
172
main();
173
}
174
175