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:
Johannes Rieken
2024-12-11 11:26:59 +01:00
committed by GitHub
parent 1b4662bc51
commit a40e594f25
18 changed files with 359 additions and 309 deletions

View File

@@ -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;
}
}