lint 规则与注册表

本页讲一条 lint 规则的结构、它如何注册进全局注册表、家族如何组织、类型感知规则的特殊处理、以及规则配置怎么从 lint.config.ts 解析进引擎。源码 packages/lint/linthost/engine.gorules_*.goconfig.go

Rule 接口

type Rule interface {
  Name() string                              // 用户配置里的 id, 用 eslint/@typescript-eslint 同名
  Visits() []shimast.Kind                    // 关心的 AST kind
  Check(ctx *Context, node *shimast.Node)    // 每个相关节点调一次
}

Name()engine.go:37)刻意复用 ESLint/@typescript-eslint 的 kebab-case id——这个插件是宿主不是改名练习,迁移项目能直接粘贴 rule severity 进 lint.config.ts 而不必改名。

Visits() 返回规则关心的节点 kind;引擎只把这些 kind 的节点分派给它(按 Kind 索引的切片,见 引擎)。Checkctx.Report* 发 finding。

两个可选标记接口

FormatRule

type FormatRule interface {
  Rule
  IsFormat() bool
}

FormatRuleengine.go:62)把规则标为格式化器。ttsc fix 是"全跑"入口——应用 lint 类与 FormatRule 类两者的 edit。ttsc format 是只格式化的便利命令——过滤到 FormatRule finding,跳过 lint 类重写。标记让 format 过滤器挑出正确的一半;fix 不需过滤。IsFormat() 必须无条件返回 true(结构标记,非运行时开关)。

typeAwareRule

type typeAwareRule interface {
  NeedsTypeChecker() bool
}

typeAwareRuleengine.go:69)标记需要活 checker 的规则。任一活跃规则实现它且返回 true,引擎就 needsTypeChecker = true,进而强制整个引擎串行(单共享 checker 非并发安全,见 引擎)。不实现它的规则被假定为 AST-only。

这对应 README 里那些 typescript/no-floating-promisestypescript/no-unsafe-* 等需要类型信息的规则——它们是把引擎钉到串行的原因。

注册表

var registered = &registry{rules: map[string]Rule{}}

func Register(rule Rule) {
  if rule == nil { panic("...nil rule") }
  if _, exists := registered.rules[rule.Name()]; exists {
    panic("...rule " + rule.Name() + " registered twice")
  }
  registered.rules[rule.Name()] = rule
}

Registerengine.go:223)从每条规则的 init() 调,重名 panic(编程错误)。AllRuleNamesengine.go:238)返回排序后的注册表,给 --list-rules 风格内省与稳定测试快照用。LookupRule 按名查。

这意味着规则集是编译期固定的:每个 rules_*.go 文件在 init()Register 自己的规则,二进制里有哪些规则由链接进去的源决定。第三方规则经贡献者机制链接(见 贡献者插件)。

家族组织

文件名编码家族(linthost/ 扁平目录):

README(packages/lint/README.md)列出 21 家族,每个有专门的"Source:"行指向上游 ESLint 插件。规则 id 用 ESLint 风格 kebab-case + slash 命名空间:core 规则裸名(no-var),家族规则带前缀(react/jsx-keytypescript/no-explicit-anyunicorn/no-null)。@ttsc/lint 不接受 legacy 裸名或 @typescript-eslint/* 别名给那些已归到 typescript/* 的规则。

配置解析

规则从 lint.config.tsrules map 读,severity 值 "error"(失败构建)/ "warning"(打印不影响退出码)/ "off"(禁用)。RuleConfig / RuleResolverengine.goconfig.go)把它解析进引擎:

  • RuleConfig.ActiveRuleNames():列出启用的规则名。
  • RuleConfig.RuleOptions(name):返回规则配置 tuple 第二槽的原始 JSON(["warning", { ... }] 里的 {...}),规则用 Context.DecodeOptions 解进自己的 struct。
  • RuleResolver.ResolveRules(fileName):支持 per-file 变化的 severity(如某些文件忽略某规则)。

格式化配置走单独的 format block(不是 rules map),见 格式化器

Context 的 Options 解码

func (c *Context) DecodeOptions(out interface{}) error {
  if c == nil || len(c.Options) == 0 { return nil }
  return json.Unmarshal(c.Options, out)
}

DecodeOptionsengine.go:109)在规则用裸 severity 配置时无副作用返回 nil,让规则写:

var opts myRuleOptions   // 零值默认
ctx.DecodeOptions(&opts) // 现在持用户设置或零值

不变量

  • 规则名全局唯一(重名 panic)。
  • 规则集编译期固定(init 注册)。
  • 类型感知规则把整个引擎钉到串行。
  • FormatRule.IsFormat 必须无条件 true(结构标记)。

维护者提示

  • 加规则:在 linthost/rules_<家族>_<名>.go,实现 Rule(必要时 FormatRule/typeAwareRule),init()Register。同步更新 README.mdwebsite/src/content/docs/lint/rules/<家族>.mdx(README 的 AGENT INSTRUCTIONS 注释规定了 bullet 形状)。
  • 加 fixture:tests/test-lint/src/cases/<rule-id>.ts(命名空间家族用 <家族>-<rule-id>.ts(x))。每条规则需要"转换方向 + 负向 twin + 边界 + oracle 派生期望"的覆盖,不能只喂 happy path(见 .codex/skills/development/SKILL.md)。
  • 规则需要类型信息时实现 NeedsTypeChecker,知道这会让引擎对该配置串行。

接下来