mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 20:26:08 +00:00
Check for cyclic dependencies during compile (#235808)
* Check for cyclic dependencies during compile Changes gulp-tsb to check the emitted JS code for cyclic dependencies. Historically we never cared about cycles between TS files as long as they dissappeared after compile (e.g type-dependencies, not runtime dependencies) https://github.com/microsoft/vscode-internalbacklog/issues/5271 * fix cycling dependencies fyi @aeschli @aiday-mar * remove cyclic dependency with unused `BasedTextEdit` fyi @hediet * remove cycle between chatEditService and chatEditingSession fyi @alexdima * remove cyclic dependency between chatSetup and chatViewPane fyi @roblourens * better cycle detection * don't check cycles when not needed * clear graph when reprocessing file dependencies * remove cycle between with `notebookChatEditController` fyi @DonJayamanne * modernize and cleanup tsb/utils
This commit is contained in:
@@ -42,6 +42,10 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
|
||||
const _log = config.logFn;
|
||||
|
||||
const host = new LanguageServiceHost(cmd, projectFile, _log);
|
||||
|
||||
const outHost = new LanguageServiceHost({ ...cmd, options: { ...cmd.options, sourceRoot: cmd.options.outDir } }, cmd.options.outDir ?? '', _log);
|
||||
let lastCycleCheckVersion: string;
|
||||
|
||||
const service = ts.createLanguageService(host, ts.createDocumentRegistry());
|
||||
const lastBuildVersion: { [path: string]: string } = Object.create(null);
|
||||
const lastDtsHash: { [path: string]: string } = Object.create(null);
|
||||
@@ -305,6 +309,13 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
|
||||
lastDtsHash[fileName] = value.signature;
|
||||
filesWithChangedSignature.push(fileName);
|
||||
}
|
||||
|
||||
// line up for cycle check
|
||||
const jsValue = value.files.find(candidate => candidate.basename.endsWith('.js'));
|
||||
if (jsValue) {
|
||||
outHost.addScriptSnapshot(jsValue.path, new ScriptSnapshot(String(jsValue.contents), new Date()));
|
||||
}
|
||||
|
||||
}).catch(e => {
|
||||
// can't just skip this or make a result up..
|
||||
host.error(`ERROR emitting ${fileName}`);
|
||||
@@ -389,6 +400,7 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// (last) done
|
||||
else {
|
||||
resolve();
|
||||
@@ -410,16 +422,40 @@ export function createTypeScriptBuilder(config: IConfiguration, projectFile: str
|
||||
workOnNext();
|
||||
|
||||
}).then(() => {
|
||||
// check for cyclic dependencies
|
||||
const thisCycleCheckVersion = outHost.getProjectVersion();
|
||||
if (thisCycleCheckVersion === lastCycleCheckVersion) {
|
||||
return;
|
||||
}
|
||||
const oneCycle = outHost.hasCyclicDependency();
|
||||
lastCycleCheckVersion = thisCycleCheckVersion;
|
||||
delete oldErrors[projectFile];
|
||||
|
||||
if (oneCycle) {
|
||||
const cycleError: ts.Diagnostic = {
|
||||
category: ts.DiagnosticCategory.Error,
|
||||
code: 1,
|
||||
file: undefined,
|
||||
start: undefined,
|
||||
length: undefined,
|
||||
messageText: `CYCLIC dependency between ${oneCycle}`
|
||||
};
|
||||
onError(cycleError);
|
||||
newErrors[projectFile] = [cycleError];
|
||||
}
|
||||
|
||||
}).then(() => {
|
||||
|
||||
// store the build versions to not rebuilt the next time
|
||||
newLastBuildVersion.forEach((value, key) => {
|
||||
lastBuildVersion[key] = value;
|
||||
});
|
||||
|
||||
// print old errors and keep them
|
||||
utils.collections.forEach(oldErrors, entry => {
|
||||
entry.value.forEach(diag => onError(diag));
|
||||
newErrors[entry.key] = entry.value;
|
||||
});
|
||||
for (const [key, value] of Object.entries(oldErrors)) {
|
||||
value.forEach(diag => onError(diag));
|
||||
newErrors[key] = value;
|
||||
}
|
||||
oldErrors = newErrors;
|
||||
|
||||
// print stats
|
||||
@@ -503,7 +539,7 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
|
||||
this._snapshots = Object.create(null);
|
||||
this._filesInProject = new Set(_cmdLine.fileNames);
|
||||
this._filesAdded = new Set();
|
||||
this._dependencies = new utils.graph.Graph<string>(s => s);
|
||||
this._dependencies = new utils.graph.Graph<string>();
|
||||
this._dependenciesRecomputeList = [];
|
||||
this._fileNameToDeclaredModule = Object.create(null);
|
||||
|
||||
@@ -576,10 +612,6 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
|
||||
}
|
||||
if (!old || old.getVersion() !== snapshot.getVersion()) {
|
||||
this._dependenciesRecomputeList.push(filename);
|
||||
const node = this._dependencies.lookup(filename);
|
||||
if (node) {
|
||||
node.outgoing = Object.create(null);
|
||||
}
|
||||
|
||||
// (cheap) check for declare module
|
||||
LanguageServiceHost._declareModule.lastIndex = 0;
|
||||
@@ -628,10 +660,21 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
|
||||
filename = normalize(filename);
|
||||
const node = this._dependencies.lookup(filename);
|
||||
if (node) {
|
||||
utils.collections.forEach(node.incoming, entry => target.push(entry.key));
|
||||
node.incoming.forEach(entry => target.push(entry.data));
|
||||
}
|
||||
}
|
||||
|
||||
hasCyclicDependency(): string | undefined {
|
||||
// Ensure dependencies are up to date
|
||||
while (this._dependenciesRecomputeList.length) {
|
||||
this._processFile(this._dependenciesRecomputeList.pop()!);
|
||||
}
|
||||
const cycle = this._dependencies.findCycle();
|
||||
return cycle
|
||||
? cycle.join(' -> ')
|
||||
: undefined;
|
||||
}
|
||||
|
||||
_processFile(filename: string): void {
|
||||
if (filename.match(/.*\.d\.ts$/)) {
|
||||
return;
|
||||
@@ -644,6 +687,9 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
|
||||
}
|
||||
const info = ts.preProcessFile(snapshot.getText(0, snapshot.getLength()), true);
|
||||
|
||||
// (0) clear out old dependencies
|
||||
this._dependencies.resetNode(filename);
|
||||
|
||||
// (1) ///-references
|
||||
info.referencedFiles.forEach(ref => {
|
||||
const resolvedPath = path.resolve(path.dirname(filename), ref.fileName);
|
||||
@@ -654,10 +700,18 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
|
||||
|
||||
// (2) import-require statements
|
||||
info.importedFiles.forEach(ref => {
|
||||
|
||||
if (!ref.fileName.startsWith('.') || path.extname(ref.fileName) === '') {
|
||||
// node module?
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const stopDirname = normalize(this.getCurrentDirectory());
|
||||
let dirname = filename;
|
||||
let found = false;
|
||||
|
||||
|
||||
while (!found && dirname.indexOf(stopDirname) === 0) {
|
||||
dirname = path.dirname(dirname);
|
||||
let resolvedPath = path.resolve(dirname, ref.fileName);
|
||||
@@ -673,6 +727,10 @@ class LanguageServiceHost implements ts.LanguageServiceHost {
|
||||
} else if (this.getScriptSnapshot(normalizedPath + '.d.ts')) {
|
||||
this._dependencies.inertEdge(filename, normalizedPath + '.d.ts');
|
||||
found = true;
|
||||
|
||||
} else if (this.getScriptSnapshot(normalizedPath + '.js')) {
|
||||
this._dependencies.inertEdge(filename, normalizedPath + '.js');
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user