lint 贡献者插件

@ttsc/lint 允许其他 npm 包提供 lint 规则,这些规则编译进同一个 @ttsc/lint 二进制、经同一条诊断流上报。本页讲贡献者机制的两端:JS 侧的 contributors 配置如何把规则源链接进来、Go 侧公共 rule 包如何让规则注册进同一分派表。源码 packages/lint/rule/packages/lint/linthost/contrib_adapter.gopackages/lint/plugin/main.go

整体机制

README(packages/lint/README.md)的用法:

import demoPlugin from "ttsc-lint-plugin-demo";
export default {
  plugins: { demo: demoPlugin },
  rules: { "demo/no-todo-comment": "error" },
} satisfies ITtscLintConfig;

ttsc 把每个声明的贡献者 Go 源拷进 @ttsc/lint 模块的子包,结果二进制在 main 前就注册了内建 + 贡献者规则。

两层注册表

lint 有两个注册表,对应内建规则与贡献者规则:

注册表谁用位置
内部 registeredengine.go:219内建规则(package main init 里 Registerlinthost 包私有
公共 rule.Registered()贡献者规则(initrule.Registerpackages/lint/rule/rule.go

内建规则活在 package main、直接经内部 Register 分派。贡献者活在兄弟包、从 init() 调公共 rule.Registercontrib_adapter.go 把公共 rule.Rule 桥接到引擎内部 Rule 接口,让引擎对两者看到同一表面。

init 顺序问题(一个微妙点)

contrib_adapter.go:8 注释记录了一个 Go init 顺序坑:

每个贡献者包的 initpackage main 的 init 之前跑(Go spec)。但内建规则也从 package main 的 init 函数注册,同包内文件相对顺序按字母序——所以不能盲目从文件级 init() 跑贡献者接线、指望内建注册已完成。

解法:registerContributorscontrib_adapter.go:30显式main.run 调,在所有内建 init 都安顿后,这样碰撞检查才有意义。

碰撞策略:贡献者与已有规则(内建或先 init 的另一贡献者)同名时被丢弃并打 stderr 警告。宿主偏好确定、可调试的结果,而非启动时 panic。

公共 rule API

packages/lint/rule/rule.go 是给第三方规则作者的公共 Go API。它提供:

  • rule.Rule 接口(公共版的 Rule 契约)。
  • rule.Register(贡献者从 init 调)。
  • rule.Context adapter(公共版的 Context,含 ReportFix / ReportRangeFix)。
  • rule.DeclarationFileRule 标记接口(VisitsDeclarationFiles() bool)。

rule/astutil 包再导出内建规则用的字节范围 helper:NodeTextKeywordStartFindKeywordTokenRange,让贡献者规则发 autofix 的方式与内建一致(README 的 "contributor autofix path" 节)。

贡献者的 autofix 路径

贡献者规则发 autofix 的方式与内建相同:ctx.ReportFix(node, message, edits...)ctx.ReportRangeFix(pos, end, message, edits...)rule/astutil 提供构造 byte-range edit 的 helper。这让贡献者规则的修复经同一 Finding.Fix 机制流回引擎、被 ttsc fix 应用。

声明文件优化的 opt-out

贡献者规则默认在声明文件(.d.ts)上运行。引擎跳过自己的值级规则(可执行语法不能出现在声明文件),但它推不出第三方规则的形状,所以贡献者保持保守默认。只检查可执行代码的规则可实现可选的 rule.DeclarationFileRule 标记(VisitsDeclarationFiles() bool { return false })拿到同样的跳过,在声明密集项目上省下分派(README 末尾)。

供应链约束

贡献者机制建在 go build 缓存contributors 之上,继承其供应链约束:

  • 贡献者 ship Go 源作为(无 go.mod);宿主插件的模块提供每个传递 Go 依赖。这也是供应链特性——贡献者不能在构建时拉任意 Go 模块。
  • 贡献者源路径必须绝对(宿主的 JS 工厂通常经 require.resolve 解析)。
  • 贡献者名作为子包 import 后缀、单次构建内唯一。

@ttsc/lint 的 JS 工厂(packages/lint/src/index.ts::createTtscPlugin)从 lint.config.tsplugins 发现贡献者包,把它们的 Go 源解析成 ITtscPlugin.contributors

二进制的多入口

packages/lint/plugin/main.go@ttsc/lint 原生后端的薄包装,宿主用以下子命令 spawn 它(规范列表在 linthost/dispatch.go):

子命令作用
check类型检查 + lint,不 emit,有 error 即非零退出
fix应用 lint autofix,再类型检查 + lint
format只应用 format 规则 edit,write-only,不重查
build类型检查 + lint,再跑 tsgo emit
transform --file=PATH单文件 emit + lint pass
lsp-*LSP 协议子命令(命令 id、code action kind、诊断、code action、execute-command)

行为住在兄弟 linthost 库包;二进制是薄包装,让进程外消费者(这个原生 CLI)与进程内消费者(ttsc.dev playground 的 wasm)共享同一实现。

不变量

  • 贡献者注册显式从 main.run 调,在内建 init 之后。
  • 同名贡献者被丢弃 + 警告,不 panic。
  • 贡献者无 go.mod,活在宿主模块内(供应链)。
  • 贡献者默认跑声明文件,除非实现 DeclarationFileRule opt-out。

接下来