lint 引擎
packages/lint/linthost/engine.go 是 lint 的核心遍历器:它把规则配置绑定到一个 Program,对每个源文件遍历一次 AST,把每个访问到的节点分派给关心它的规则。本页讲分派结构、并行/串行决策、per-file Context 复用、以及 panic 隔离——这些都是为大项目性能精心设计的。
三个核心抽象
engine.go:33 起定义了分层:
Rule是每条规则实现的契约,init 时注册、之后不可变。Engine对每个源文件遍历一次 AST,把每个访问节点分派给经Visits()opt-in 的规则。Context是规则触发时收到的句柄,拥有回引擎的报告通道。
规则是跨文件无状态的:每次调用拿到全新 Context,不能保留对上一个文件的引用。这让引擎对并发友好。
按 Kind 索引的分派(性能核心)
Engine.rules(engine.go:256)是一个按 shimast.Kind 值索引的定长切片 [][]Rule,而非 map:
注释(engine.go:248)解释:KindCount(~350)小且有界,切片去掉了 per-node 的 map hash——一个 50k 节点文件的 walk 会做 50k 次分派查找。切片让热路径线性于活跃规则数而非总规则数。未用 kind 的条目是 nil。
NewEngine(engine.go:291)构建时把每条启用规则的 Visits() 去重后追加进对应 kind 槽(engine.go:320,去重防止贡献者在 Visits() 里列重同一 kind 导致每节点触发两次)。
并行 vs 串行
Engine.Run(engine.go:427)默认按 runtime.NumCPU() 并行 per-file,但在两种情况退到串行(runsSerial,engine.go:283):
- 调用方
SetSerial(true)(--singleThreaded时宿主调); - 有类型感知规则(
needsTypeChecker)。
为什么类型感知规则强制串行:类型感知规则通过 Context.Checker 拿到单个共享 checker,而那个 checker 不是并发安全的。所以只要有一条规则要 checker,整个引擎退串行(engine.go:270 注释),调用方不必自己清 serial 标志。
并行路径(engine.go:439)用 runtime.NumCPU() 个 worker 的信号量,per-file 结果存进 perFile[idx],最后按源文件顺序合并,让诊断流跨运行确定(即使 per-file 工作乱序完成)。
per-file Context 复用
runFile(engine.go:518)有一个关键性能设计:每个文件给每条活跃规则绑一个 Context,而非每个 (节点, 规则) 对一个。Context 的字段(File、Checker、文件解析后的 Severity、规则 Options blob、format 标志)在文件内不变,所以引擎在这里构建一次(engine.go:553)。
注释(engine.go:530)说明:早先形态给每个 (节点, 规则) 对分配新 Context,在大程序上意味着数百万短命堆分配与对应的 GC 压力。规则从不改自己的 Context,所以复用安全。ctxByRule map 还 memo 了"该文件此规则关闭"(nil 条目),让注册了多个 kind 的规则只解析一次 severity。
walker:缓存的 ForEachChild 回调
lintFileWalker(engine.go:487)存在是为了让 ForEachChild 回调能作为缓存的方法值(childCB)。注释(engine.go:481):朴素的嵌套闭包 walker 每次递归调用都重分配一个回调(它捕获 walking 函数变量),50k 节点文件就是 50k 次一次性闭包分配。缓存方法值把它降到每文件一次分配——pre-Opt-4 约 38% CPU 花在内层 ForEachChild 闭包上。
walk(engine.go:495)后序遍历:先分派当前节点给注册该 kind 的规则,再经 childCB 递归子节点,让父节点看到已检查的子树。
声明文件优化
runFile(engine.go:541)对声明文件(.d.ts)只绑定 opt-in 的规则(ruleVisitsDeclarationFiles)。值级规则在声明文件里永不触发(可执行语法不能出现在声明文件),所以分派给它们纯属开销。这对声明密集的树是显著优化。
panic 隔离
runRuleCheck(engine.go:638)用 recover() 屏障包住每条规则的 Check,让一条崩溃的规则不中止整个 ttsc fix/check run。内建规则不预期崩,但第三方贡献规则经公共 rule.Context adapter 由任何人编写;保护引擎是 bound 一条坏规则爆炸半径的唯一方式。恢复的 panic 被表面成一条带规则名的 SeverityError finding,用户在正常诊断流里看到失败。
Finding 与报告
规则经 Context.Report / ReportFix / ReportRange / ReportRangeFix(engine.go:148)报 Finding。Report 的 pos 会跳过 leading trivia(shimscanner.SkipTrivia),让渲染的 path:line:col banner 指向冒犯 token 而非缩进起点。Finding(engine.go:122)带 IsFormat 标志,镜像分派规则的类别,让 format 子命令的过滤器无需重查注册表就能路由(fix 子命令两类都应用,不过滤)。
inline-disable 过滤(filterInlineDisabledFindingsWithDirectives,engine.go:618)即使文件无语句列表也应用——SourceFile 级规则在 // ttsc-lint-disable 注释上触发时也要被尊重。collectUnknownDirectiveRules 记录指向未注册规则的指令名,作为配置警告。
不变量
- 规则跨文件无状态,每次调用全新 Context。
- 类型感知规则集永远串行(单共享 checker)。
- 诊断流按源文件顺序确定,无论并行与否。
Context.Severity == Off的 finding 在Report最终门被丢(防御性,引擎已先按 severity 过滤)。