较import-next/no-cycle与eslint-plugin-import/no-cycle及oxlint所译之Rust版于next.js(星标131K,源文件14,556)为衡。二ESLint插件意同:0周而复始。oxlint异议:17周而复始。
吾等信从众议。复以吾之规于同仓之33文件小集(packages/next/src/client/components/router-reducer/**)试之。其得立时5+ 轮。
规同也,配置同也,文件同也,范围异也,应答亦异也。
谬误藏于缓存之层,深六十行——此故广范围寂然无声。
隐谬之设
凡检测循环之术,形皆同也:
- 于检错之域内,每文件__JHSNS_SEG_96313024_15__F。
- 于其导入图行深度有界之深度优先遍历
- 若深度优先遍历复归F,则得循环
- 否则,F为无环,当记之,以备后用
第四步,方见缓存之效。有N文件,平均图深为D,则愚直之循环检测为O(N²·D)。有"已知无环"之缓存,则重访为O(1)。于实代码库,缓存命中率逾70%——无之,则此规过缓,难行于持续集成。
缓存之形:
interface FileSystemCache {
// ...
nonCyclicFiles: Set<string>; // files known not to be in any cycle
}
及其用之域:
function dfs(file: string, depth: number, visited: Set<string>) {
if (file === sourceFile) {
allCycles.push([...pathStack, file]);
return;
}
if (depth >= maxDepth) return; // <-- early return on depth limit
if (visited.has(file)) return;
if (cache.nonCyclicFiles.has(file)) return;
// ... recurse into imports
}
dfs(targetFile, 1, new Set());
if (allCycles.length === 0) {
cache.nonCyclicFiles.add(targetFile); // <-- cache the result
}
察其弊乎?在彼二行之间// <--。
缓存何以自戕
当DFS至时depth >= maxDepth者,返,若已穷尽探索而无环。呼者不能辨"吾已遍察无所见"与"吾止于深十"之异
。故文件惟环在深十二(十二> maxDepth=十)者,得:
- DFS止于深十
allCycles.length === 0-
cache.nonCyclicFiles.add(targetFile)— 误标为已知无环
。今后任何未来之深度优先遍历,若经此文件,则因if (cache.nonCyclicFiles.has(file)) return;而中断。毒化蔓延:同强连通分量子树之每文件,皆因关联而标为无环。
于微尘之域,不见其流布之象——无足多之文书,不足以掩其余。于十四万卷之域,一早失而继以缓存,则群集尽毁。
狭域与广域之辨,其证在此
此乃验之之术。同法,同制,同--no-cache 此旗以避 ESLint 之缓存——然吾之进程缓存仍存于运行期间:
# Wide scope: 2,363 files, includes everything in packages/
$ eslint --config flagship.config.mjs 'packages/**/*.{ts,tsx,js}'
# 0 import-next/no-cycle findings
# Narrow scope: 33 files, just the router-reducer directory
$ eslint --config flagship.config.mjs 'packages/next/src/client/components/router-reducer/**/*.ts'
# 5+ import-next/no-cycle findings
狭行发现循环。广行,自新进程与新缓存始,亦生新缓存——然 ESLint 依序限文件,及处理二千三百六十三文件,渐积之。nonCyclicFiles缓存。及至绒絮通道达于文件之时,行属乎循环,彼循环者,乃为伪作非循环之名,由层叠所致。
oxlint,其为一异法,有自之实,不共吾之缓存。乃用oxlint之自。ModuleGraphVisitorBuilder乃得十七周而复始。
其补
察DFS是否截断,勿缓存截断之运行:
let depthLimitHit = false;
function dfs(file: string, depth: number, visited: Set<string>) {
if (file === sourceFile) {
allCycles.push([...pathStack, file]);
return;
}
if (depth >= maxDepth) {
depthLimitHit = true; // <-- record the truncation
return;
}
// ... rest unchanged
}
dfs(targetFile, 1, new Set());
// Only cache as acyclic when DFS COMPLETED and found nothing.
// A depth-truncated DFS isn't proof of acyclicity.
if (allCycles.length === 0 && !depthLimitHit) {
cache.nonCyclicFiles.add(targetFile);
}
五行。于next.js再运行:0 → 245独文件于循环,914独(文件,行)对。广域之正误今合于狭域之正误.
eslint-plugin-import所为之事何哉
既得真谬,当察同域之侪如何拟此患。恒久之eslint-plugin-import/no-cycle法,其术迥异:
// from eslint-plugin-import/src/rules/no-cycle.js:73
const scc = options.disableScc
? {}
: StronglyConnectedComponentsBuilder.get(myPath, context);
// ...
// If we're in different SCCs, we can't have a circular dependency
const hasDependencyCycle =
options.disableScc || scc[myPath] === scc[imported.path];
if (!hasDependencyCycle) return;
彼辈构强连通分量之图,每检校一遭即成之,则逐文之环核验,时恒一也。"此二文件同属一SCC乎?"。SCC之图,以Tarjan之算法,计之需O(V+E)时
。此法全避深度之限。SCC者,答"何为循环之簇"之确解也——无截断,无近似,无缓存之毒。其缓存SCC之果于全域,并于Program:exit时清之。
oxlint之道,更进一层:于解析之际,立显明之模组图,继而环视者直行于图。无复需强连通分量,盖图已具结构矣。
二法共具一性,吾之缓存深度优先法所无:算法为精确,非约略。缓存以部分计算易取正确——此正吾误行之道也。
下次我将有所不同
三得之於诊:
缓存之不可欺也。一缓存条目,当唯编码汝所。已证,非汝所知之信息也未能证伪吾等nonCyclicFiles 缓存编码"DFS未发现环"为"无环存在"。此非同义之语。
当于部署之域,试此算法。 我等单元测试得通,盖因测试之具小而深有界。此弊惟于2K以上之文件显,时缓存已盈,遂启级联。吾辈需一压力测试,以拟生产之境。
精算法可避缓存所引之谬。 基于强连通分量之循环检测(eslint-plugin-import)与模块图遍历(oxlint)于构造上即避深度限制之交互。吾持深度优先遍历之法有由——逐文件缓存之增量分析实受其益——然深度限制与缓存之交互恰为强连通分量之法所不能有之弊。当再评估增量是否值得此交易.
修复已就包/eslint-devkit/src/解析器/依赖分析.ts曝之凳也benchmarks/suites/ilb-flagship。
此乃三律之弊,同此一扫而得。其伴文如下:何真境为单元测试所遗(烟门之器)与熵不足时 (807 假证之发见于vercel/ai).
📊 有关作者
吾乃Ofri Peretz,筑Interlace ESLint之生态——JavaScript静态分析之目录,行于ESLint与Oxlint,以CI所强制之均等.












