add-a-linter

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
Add a linter to vigiles's cross-referencing engine (the
enforce("eslint/...")
moat). This is a contributor task, not a third-party extension point: the
LINTERS
registry is a
Record<BuiltinLinter, LinterAdapter>
— a closed set baked into core — so a linter is added by editing vigiles itself, and the type system + the conformance test make the parity un-forgettable.
The whole reason this skill exists: a linter used to be smeared across ~7 scattered sites (existence check, config checker, CLI-tool map, suggestion enumerator, generate-types discoverer, docs, site) with nothing enforcing that you touched all of them — miss one and it failed silently. Now
tsc
fails if the registry entry is missing, and
src/core/linter-contract.test.ts
fails if the docs table or the marketing site drifts. Follow the steps; let the gates catch what you forget. See
research/linter-adapter-architecture.md
.
为vigiles的交叉引用引擎(即
enforce("eslint/...")
防护层)添加一个linter。这是一项贡献者任务,而非第三方扩展点:
LINTERS
注册表是一个
Record<BuiltinLinter, LinterAdapter>
——是一个内置的封闭集合,因此添加linter需要直接编辑vigiles本身,而类型系统和一致性测试会确保内容一致性,不会出现遗漏
这项技能存在的核心原因:过去添加一个linter需要在约7个分散的位置进行配置(存在性检查、配置校验器、CLI工具映射、建议枚举器、generate-types发现器、文档、站点),没有任何机制确保你修改了所有位置——遗漏一处就会导致静默失败。现在,如果缺少注册表条目,
tsc
会报错;如果文档表格或营销站点内容不一致,
src/core/linter-contract.test.ts
会失败。遵循步骤即可;校验机制会帮你捕捉遗漏的内容。详情请查看
research/linter-adapter-architecture.md

The one invariant

核心不变原则

A linter is one
LinterAdapter
in one registry. Everything else — existence, config-enabled, suggestions, type-gen, docs, site — is a field or a method on that adapter, cross-checked by the conformance test. You never again hunt for "the other place this linter is registered."
一个linter对应一个
LinterAdapter
,且仅存在于一个注册表中。其他所有内容——存在性、配置启用状态、建议、类型生成、文档、站点——都是该适配器的字段或方法,由一致性测试进行交叉校验。你无需再四处寻找‘这个linter还在哪些地方注册过’。

Steps

操作步骤

Work in this order — each step's gate tells you the next is needed.
  1. Name it (the single source). Add the lowercase name to
    BUILTIN_LINTERS
    in
    src/core/spec.ts
    .
    BuiltinLinter
    derives from this array, so the moment you save,
    tsc
    fails on
    LINTERS
    in
    linters.ts
    with "property
    <name>
    is missing" — that error is your to-do list.
  2. Pick the existence-check kind (
    LinterCapabilities.existenceCheck
    in
    src/core/linter-adapter.ts
    ) — this decides which helper builds the adapter:
    • node-api
      — the rule set is resolved from an installed npm package (eslint, stylelint). Use
      nodeApiAdapter(name, resolver, configEnabled, discover)
      .
    • cli
      — a real command asks the tool whether a rule exists (ruff, clippy, pylint, rubocop, detekt, ktlint, checkstyle, golangci-lint). Use
      cliAdapter(name, cliTool, checkExists, configEnabled, discover, enumerate?)
      .
    • filesystem
      — presence in a project file counts, no tool (cedar). Write a literal adapter (see cedar in
      linters.ts
      ).
    • format-only
      — only the reference shape is validated, no tool exists to list rules (ktlint's catalog is unlistable). Still a
      cli
      adapter, just omit the
      enumerate
      arg; the existence check is the qualified-shape rule.
  3. Implement the discoverer
    discover<Name>Rules(basePath): DiscoveredRules | null
    in
    linters.ts
    — reads the project's real linter config and returns its enabled rules for
    generate-types
    (fail open: return
    null
    , never flag every rule, when you can't enumerate). If it's a
    cli
    linter, also write its
    <name>CheckExists
    existence probe (throws when the rule is unknown) and, for a real config-enabled read, its
    <name>ConfigEnabled
    checker — plain named functions the adapter references directly in the
    LINTERS
    registry (there is no separate map to touch). Parse structured config with a real parser (js-yaml / @iarna/toml / the shared markdown-it helper), never a hand-rolled regex — see the
    parse-structured-input-with-a-real-parser
    rule; detekt's
    parseDetektConfig
    (js-yaml) is the model.
  4. Register it in
    LINTERS
    (
    linters.ts
    ) via the matching helper.
    tsc
    goes green here — the registry is now complete.
  5. Document it
    docs/linter-support.md
    : add a row to the
    ## Supported Linters
    table AND a
    ## <Linter>
    section (config conventions, rule-prefix, any capability caveat like "format-only" or "whitelist-only"). The conformance test set-matches the table against the registry, so a missing row fails CI.
  6. The site updates itself — the vigiles.sh chip strip (
    Wedge.tsx
    ) DERIVES from
    BUILTIN_LINTERS
    , so a new linter appears automatically; there's no array to edit. Optionally add a display label to
    LINTER_LABELS
    in
    Wedge.tsx
    if it needs special casing (e.g.
    ESLint
    ,
    RuboCop
    ); with no entry it renders under its lowercase name. The conformance test guards that the derivation stays in place (a revert to a hand-typed list fails CI).
  7. If it's a
    cli
    linter, make CI actually run it — no silent skips.
    The real-binary tests are
    describe.skipIf(!hasBinary("<tool>"))
    in
    src/core/linters.test.ts
    ; a binary absent from CI means those tests skip silently (a hidden gap — the
    no-silent-skips
    rule). Install the tool in the
    test
    job of
    .github/workflows/ci.yml
    (pin a version via a job
    env
    , cache it) AND add it to the
    command -v
    sanity loop so a missing binary fails the build instead of skipping. Then write the two complementary tests: a real-binary test (
    describe.skipIf(!hasBinary)
    ) and a missing-binary honest-error test (
    it.skipIf(hasBinary)
    ) — one always runs, the pair is loud either way.
  8. Add the parity test data. The conformance loop in
    src/core/linter-contract.test.ts
    is generic (it iterates the registry), so it covers the new linter automatically — but add a targeted config-parse/discover unit test in
    linters.test.ts
    for the new linter's own parser, and a per-linter capability assertion if it has an unusual variance (e.g.
    format-only
    ,
    alwaysEnabled
    ).
按以下顺序操作——每一步的校验机制会提示下一步的需求。
  1. 命名(单一数据源)。在
    src/core/spec.ts
    BUILTIN_LINTERS
    中添加小写名称。
    BuiltinLinter
    由该数组派生,因此保存后,
    linters.ts
    中的
    LINTERS
    会触发
    tsc
    报错:“缺少属性
    <name>
    ”——这个错误就是你的待办清单
  2. 选择存在性检查类型
    src/core/linter-adapter.ts
    中的
    LinterCapabilities.existenceCheck
    )——这将决定使用哪个辅助函数构建适配器:
    • node-api
      ——规则集从已安装的npm包(如eslint、stylelint)解析而来。使用
      nodeApiAdapter(name, resolver, configEnabled, discover)
    • cli
      ——通过真实命令检查工具是否存在某条规则(如ruff、clippy、pylint、rubocop、detekt、ktlint、checkstyle、golangci-lint)。使用
      cliAdapter(name, cliTool, checkExists, configEnabled, discover, enumerate?)
    • filesystem
      ——通过项目文件的存在性判断,无需调用工具(如cedar)。直接编写字面量适配器(可参考
      linters.ts
      中的cedar实现)。
    • format-only
      ——仅验证引用格式,没有工具可列出规则(如ktlint的目录无法枚举)。仍使用
      cli
      适配器,只需省略
      enumerate
      参数;存在性检查基于合格格式规则。
  3. 实现发现器:在
    linters.ts
    中实现
    discover<Name>Rules(basePath): DiscoveredRules | null
    ——读取项目的真实linter配置,并返回
    generate-types
    所需的启用规则(开放失败:当无法枚举时,返回
    null
    ,切勿标记所有规则)。如果是
    cli
    类型的linter,还需编写其
    <name>CheckExists
    存在性探测函数(当规则未知时抛出错误),以及用于读取真实配置启用状态的
    <name>ConfigEnabled
    校验器——这些都是普通命名函数,由适配器直接在
    LINTERS
    注册表中引用(无需修改单独的映射表)。使用真实解析器(如js-yaml / @iarna/toml / 共享的markdown-it辅助工具)解析结构化配置,绝不要手动编写正则表达式——请遵循
    parse-structured-input-with-a-real-parser
    规则;detekt的
    parseDetektConfig
    (基于js-yaml)是参考实现。
  4. 注册适配器:在
    linters.ts
    LINTERS
    中通过匹配的辅助函数注册。此时
    tsc
    会恢复正常——注册表已完整。
  5. 编写文档——在
    docs/linter-support.md
    中:在
    ## Supported Linters
    表格中添加一行条目,并新增一个
    ## <Linter>
    章节(包含配置约定、规则前缀、任何能力限制,如“format-only”或“whitelist-only”)。一致性测试会将表格内容与注册表进行集合匹配,因此缺少条目会导致CI失败。
  6. 站点自动更新——vigiles.sh的芯片栏(
    Wedge.tsx
    BUILTIN_LINTERS
    派生
    ,因此新linter会自动显示;无需编辑额外数组。如果需要特殊命名(如
    ESLint
    RuboCop
    ),可在
    Wedge.tsx
    LINTER_LABELS
    中添加显示标签;若无条目,则会以小写名称显示。一致性测试会确保这种派生机制保持有效(若恢复为手动输入列表,CI会失败)。
  7. 如果是
    cli
    类型的linter,确保CI实际运行它——禁止静默跳过
    。真实二进制测试在
    src/core/linters.test.ts
    中为
    describe.skipIf(!hasBinary("<tool>"))
    ;若CI中缺少该二进制文件,这些测试会静默跳过(隐藏漏洞——违反
    no-silent-skips
    规则)。在
    .github/workflows/ci.yml
    test
    任务中安装该工具(通过任务
    env
    固定版本,并缓存),并将其添加到
    command -v
    检查循环中,这样如果缺少二进制文件,构建会直接失败而非跳过。然后编写两个互补测试:一个真实二进制测试(
    describe.skipIf(!hasBinary)
    )和一个缺失二进制文件时的诚实错误测试(
    it.skipIf(hasBinary)
    )——两者中总有一个会运行,无论哪种情况都会明确反馈结果。
  8. 添加一致性测试数据
    src/core/linter-contract.test.ts
    中的一致性循环是通用的(它会遍历注册表),因此会自动覆盖新linter——但需在
    linters.test.ts
    中为新linter的解析器添加针对性的配置解析/发现单元测试,若存在特殊差异(如
    format-only
    alwaysEnabled
    ),还需添加针对该linter的能力断言。

The gates that make this safe

确保操作安全的校验机制

Run
npm test
(or at least
npx vitest run src/core/linter-contract.test.ts src/core/linters.test.ts
+
tsc --noEmit
). You are done only when:
  • tsc
    is clean — the registry entry exists (completeness).
  • linter-contract.test.ts
    is green — key ===
    name
    , every capability flag matches its method's presence,
    existenceCheck === "cli"
    cliTool
    present, and the registry keys set-match
    BUILTIN_LINTERS
    and
    docs/linter-support.md
    and the site chip list (docs + site parity).
  • The new linter's config-parse unit test passes with no binary, and its real-binary test runs in CI (installed + sanity-gated), not skipped.
运行
npm test
(或至少运行
npx vitest run src/core/linter-contract.test.ts src/core/linters.test.ts
+
tsc --noEmit
)。只有满足以下条件才算完成:
  • **
    tsc
    **无报错——注册表条目已存在(完整性)。
  • **
    linter-contract.test.ts
    **测试通过——key与
    name
    匹配,每个能力标志与其方法的存在性匹配,
    existenceCheck === "cli"
    cliTool
    存在,且注册表键与
    BUILTIN_LINTERS
    docs/linter-support.md
    以及站点芯片列表完全匹配(文档与站点内容一致)。
  • 新linter的配置解析单元测试在无二进制文件的情况下通过,且其真实二进制测试在CI中运行(已安装并通过完整性校验),未被跳过。