如何为 react-doctor 贡献一条新 lint 规则?
【免费下载链接】react-doctorYour agent writes bad React. This catches it项目地址: https://gitcode.com/GitHub_Trending/re/react-doctor
react-doctor 的规则集合持续扩张,贡献新规则的任务路径在仓库文档中是完整定义的:你先用一句话定义规则要抓的代码模式,再对照现有规则和工具函数实现检测器,写对抗性测试,更新生成的注册表,用 OSS 仓库做 eval 验证噪声水平,最后带着 eval 结果提 PR。本文的操作路径来自 docs/HOW_TO_WRITE_A_RULE.md 与 rule-research、rule-writing、rule-validate 三个 skill 文档,参考实现是 PR #491 的no-mutating-reducer-state规则,其源文件与测试都在仓库内可以直接阅读。
开始前的环境要求(来自根 package.json):Node^20.19.0 || >=22.13.0、pnpm >=8,仓库使用 pnpm workspace + turbo,规则代码位于packages/oxlint-plugin-react-doctor包。
第一步:用一句话定义规则
在动手写代码之前,先把规则定义成这种格式:
This rule catches <code pattern> that causes <specific problem>.文档中的正例:
This rule catches React `useReducer` reducers that mutate the current state object and return that same object.反例是模糊的定义("catches bad reducer state updates")——这类定义会让检测器边界失控。定义之外还必须写清运行时原因,例如:React 按引用比较 reducer state,reducer 原地修改旧对象并返回同一引用时,React 可能把这次更新当作无变化。
定义阶段需要回答五个问题,它们直接决定 v1 的范围:
- 哪个框架/库行为让这段代码成为 bug?
- 什么代码形态触发 bug?
- 什么代码形态是修复后的正确写法?
- 哪些长得像但合法的代码必须保持安静?
- v1 有意不覆盖什么?
rule-research skill 要求把以上内容整理成一份"规则契约"(Rule definition / Runtime reason / Detector precision / Evidence / Strong positives / False-positive traps / In scope / Out of scope / Test seeds / Open questions),实现阶段只能按契约写行为,不能临场扩大范围。核心纪律是:把误报当作正确性 bug 处理,诊断信息的范围不能宽于检测器实际证明的行为,相邻的规则想法要拆成独立规则。
第二步:检查现有规则模式与工具
实现前先看仓库里已有的代码结构,避免重复造轮子。需要检查的位置:
- packages/oxlint-plugin-react-doctor/src/plugin/rules/ 下按类别组织的规则目录(
a11y、architecture、correctness、state-and-effects、security-scan等) - utils/ 工具函数目录
- rule-registry.ts
- 与规则同目录放置的
*.test.ts测试文件
文档明确要求"先复用现有 helper,再新增",列出的常用工具包括defineRule、runRule、walkAst、isNodeOfType、findVariableInitializer、stripParenExpression。新增 helper 之前先用 truffler 搜索仓库里是否已有同行为实现:
bunx @rayhanadev/truffler "<symbol-or-behavior>" \ packages/oxlint-plugin-react-doctor/src/plugin \ --kind function,interface,type,constant --limit 20创建 utility 的条件也有明确标准:两个以上调用点需要同行为、行为涉及微妙的 AST 语义、或 review 指出重复逻辑;如果只是隐藏一行简单代码或让名字更含糊,就不要抽。
如果想直接学习 AST 词汇与边界处理,参考实现是 no-mutating-reducer-state.ts 及其同目录测试,它对应文档中反复引用的 PR #491。
第三步:选择检测精度与 v1 范围
实现前先给规则分类,文档给出四种精度:
Syntax-Only(纯语法):bug 是局部的,不需要绑定或路径分析。典型如dangerouslySetInnerHTML={{ __html: value }}。
Scope-Aware(作用域感知):名字必须解析到特定 import 或绑定。例如useReducer必须是 React 的 import 而不是本地函数,本地 shadow 的同名函数必须保持安静。
Path-Aware(路径感知):顺序和分支都重要。文档给出的例子:reducer 的某个分支原地修改后return { ...state }(返回新对象),另一个分支是无副作用的return state——这种组合不应报告,因为修改路径返回的是新对象。
Scan Rules(项目级扫描规则):当信号在文件系统里而不是源码语法里时使用——目标文件根本不会被 lint(shipped bundle、.env、配置文件、SQL、Firebase rules、仓库 secret 文件),且路径上下文比代码形态更重要。如果 bug 是被 lint 的 JS/TS 源码形态,就写普通 AST 规则。Scan 规则放在packages/oxlint-plugin-react-doctor/src/plugin/rules/security-scan/,defineRule调用里声明scan而不是create:
export const firebasePermissiveRules = defineRule({ id: "firebase-permissive-rules", title: "Permissive Firebase security rule", severity: "error", recommendation: "Bind every read/write to `request.auth.uid`, immutable ownership, and tenant membership instead of treating sign-in as authorization.", scan: scanByPattern({ shouldScan: (file) => isFirebaseRulesPath(file.relativePath), pattern: /allow\s+(?:read|write|...)\s*:\s*if\s+(?:true|request\.auth\s*!=\s*null)/i, message: "Firebase rules grant broad access to everyone or to any signed-in user.", }), });scan 的契约:scan(file: ScannedFile): ScanFinding[]替代 AST visitor;ScannedFile携带absolutePath、relativePath、content、isGeneratedBundle;每个ScanFinding有message、line、column,可选severity/title/help按 finding 覆盖注册表元数据,省略则继承规则的severity/title/recommendation。两点执行差异要注意:scan 规则不会出现在生成的 oxlint 配置或 ESLint preset 里,而是由@react-doctor/core的check-security-scan环境检查在整树扫描时运行;id:与severity:必须保持为规则文件里的字面量字段,因为scripts/generate-rule-registry.mjs用正则解析它们。
v1 范围纪律:不要把相邻规则想法混进 v1。PR #491 的 v1 只覆盖:真实的 ReactuseReducer调用、同文件 reducer 函数、对原始 state 或其别名(alias)的修改、同路径返回原始顶层 state 引用。它明确跳过:import 进来的 reducer 函数体、mutate(state)这类 helper 调用、解构别名、复杂循环与 try/catch、嵌套引用修改后浅拷贝、Immer/Redux Toolkit draft reducer。像"修改state.user.name后return { ...state }"这类相邻想法需要单独的措辞和误报处理,不能塞进同一条规则。
第四步:先写对抗性测试
测试套件必须覆盖这些类别(文档清单):直接无效用例、别名无效用例、import 别名下达、命名空间 import、长得像的合法用例、作用域 shadow、import 了但解析不到的情况、框架/库逃生口、review 意见产生的回归测试。文档强调测试要多样化,不要反复复制同一种形态。
以no-mutating-reducer-state为例,文档给出的无效用例包括直接修改加同引用返回(state.count++; return state;)、别名修改加别名返回(const next = state; ...; return next;)、原地数组方法返回(return state.sort(...))、switch fallthrough 后落到return state、以及透明包裹层(return state as State;)。合法用例包括 no-op 的return state分支、clone-first 更新(const next = { ...state }; next.count++; return next;)、修改路径返回新对象、非 React 的Array.prototype.reduce、本地 shadow 的useReducer函数、v1 不覆盖的 imported reducer,以及动态计算属性(state.itemspush不能当静态方法名匹配)。
测试文件与规则同目录放置,命名如no-mutating-reducer-state.test.ts。scan 规则则用内存测试工具 run-scan-rule.ts:构造ScannedFile、断言 findings,写在同目录测试里;端到端覆盖由 packages/core/tests/check-security-scan.test.ts 对着packages/core/tests/fixtures/check-security-scan/下的 fixture 树运行。
第五步:实现检测器
先写伪代码,再写实现。PR #491 的伪代码骨架:
for each file: collect React useReducer imports collect React namespace/default imports for each CallExpression: if callee is not React useReducer: continue reducerFunction = resolve first argument if reducerFunction is not same-file: continue stateName = first reducer parameter analyze reducer body by path path analysis: track original state reference names track mutable state source names track mutations seen on current path when statement mutates original state source: remember mutation when statement returns original state reference: report remembered mutations实现要求(文档原列):
- 检测器必须匹配一句话规则定义;
- 信任标识符名字之前先解析 import;
- shadowed binding 当作不同名字处理;
- 不要把嵌套函数当作立即执行来遍历;
- 只建模规则声明需要的控制流;
- 未知或 import 来的代码跳过,除非规则明确支持;
- 已知的 v2 缺口用 TODO 标出。
文档还要求"对不确定情况保持安静"(keep uncertain cases quiet),诊断信息与检测器证明的条件一致。
命名上,helper 名必须描述精确行为:避免isStateReference、getName、checkMutation,偏好isOriginalReducerStateReference、getStaticMemberPropertyName、collectReducerStateMutationsInExpressionOrStatement;相关 helper 用一致后缀(isOriginalReducerStateReference/isMutableReducerStateSource/isReactUseReducerCall)。注释只用于非显然的控制流或 AST 取舍,解释分支为什么存在、v1 边界在哪,不叙述显而易见的代码。
第六步:更新生成的注册表
规则文件写好后,注册表是生成物。packages/oxlint-plugin-react-doctor的 package.json 里定义了生成脚本(底层是 scripts/generate-rule-registry.mjs):
pnpm --filter oxlint-plugin-react-doctor gen pnpm --filter oxlint-plugin-react-doctor gen:checkgen重新生成规则注册表;gen:check生成后用git diff --exit-code检查 core-rule-registry-data.json、rule-registry.ts 与 security-scan-rule-registry.ts 是否与规则文件一致。
Pre-PR checklist 里有一条"Generated registry is updated if required"——提交前注册表必须与规则文件同步。scan 规则的注册、tag 与 severity 流程与普通规则相同,security-scanbucket 会自动应用Security类别和security-scantag。
第七步:本地验证
仓库脚本通过@antfu/ni的nr命令执行(文档 Verify Locally 一节):
nr test nr lint nr typecheck nr format nr smoke:json-report迭代期间用聚焦命令。PR #491 实际使用的一组:
pnpm exec vp test run packages/oxlint-plugin-react-doctor/src/plugin/rules/state-and-effects/no-mutating-reducer-state.test.ts pnpm --filter oxlint-plugin-react-doctor typecheck pnpm lint注意该包的typecheck与test脚本本身会先跑pnpm gen(见 plugin package.json),所以包级命令会顺带校验注册表生成物。如果宽范围命令因无关的仓库状态失败,文档要求记录四件事:跑了什么命令、失败位置、为什么与本次改动无关、以及通过的聚焦命令是什么。
第八步:用 Evals 在 OSS 仓库上验证
实现完成、聚焦测试通过后,用 RDE(eval harness)把新规则放到大量开源仓库上跑,目标是避免误报、查看真实诊断、测量噪声、发现单元测试漏掉的实现假设。
输入:包含新规则的 react-doctor checkout、RDE eval harness checkout、repo manifest、repo 缓存、目标规则名。输出:JSONL 扫描输出、过滤到目标规则的结果、按 repo/rootDir 的汇总、人工检查过的命中、后续修复或测试。
文档列出的硬性处理要求:
- 扫描的是不同的仓库,不只是 manifest 条目——manifest 条目数(rootDir 数)与 distinct repo 数要分开记录,不要混淆;
- 判断结果前先把输出过滤到目标规则;
- 命中数低时人工检查每一个命中,命中数高时抽样检查;
- eval 发现的每个误报都要补回归测试(并按 rule-validate 的要求加入
fuzzcorpus)。
PR #491 的 eval 记录作为格式参考:目标规则no-mutating-reducer-state,范围是repos.json中 100 个 distinct repos,671 个 rootDir 扫描行,过滤后 1 条诊断,结论是低噪声、该命中经人工检查。
第九步:写 PR 并准备合并
PR 描述用固定结构:
## Why Catches <specific issue>。 运行时原因 1-3 句 + 一个 bad before 示例 + 一个 good after 示例。 ## What changed - Added `<rule-name>`。 - Detects `<main detection surface>`。 - Reports `<exact condition>`。 - Allows `<important valid patterns>`。 - Adds tests for `<edge cases>`。 ## Eval results | Check | Result | | ----------------- | ------------------------------------------ | | Repos scanned | `<distinct repos 数量>` | | RootDir scans | `<manifest/rootDir 条目数>` | | Target rule | `<rule-name>` | | Diagnostics | `<目标规则诊断总数>` | | False positives found | `<人工检查后的数量>` | | Output artifact | `<过滤后的 JSONL / 汇总路径>` | ## Test plan - 聚焦测试命令 - Typecheck 命令 - Lint 命令对发布包的用户可见改动,还要跑nr changeset产生 changeset;规则新增、bug 修复、误报修复默认用 patch changeset,只有私有/文档/测试/工具类改动可以跳过且必须说明原因。rule-validate 还要求:PR head 推送后对每条新规则跑run-parity,两个 Daytona run 都完成才算有 parity,repository 数与 project-root 数分开比较,先看目标规则的 delta 再下分类结论;parity 跑不起来时,PR 里报告具体阻塞原因,并省略 eval 表格。
一条规则 PR 的最终产物清单(Standard Output 一节):规则实现、同目录测试、注册表生成物更新、被多个调用点共享的 utility、带 before/after 示例的 PR 描述、test plan、宽泛或启发式规则的 evals 汇总、带回归测试的 review 修复。
常见失败模式与限制
文档的 Common Failure Modes 一节列出的坑,基本都是"检测器比定义走得更远"导致的:需要 import 解析时用了字符串/名字启发式;没有证明同路径先有修改就报告return state;把 import 来的函数当本地实现;把嵌套函数当立即执行遍历;漏掉别名重赋值、分支路径、switch fallthrough;把动态计算属性当静态名;把相邻 v2 规则混进 v1;测试照抄实现形态而不是真实代码;PR 描述只讲内部实现不讲用户可见的 bug。
Review 分诊也有明确标准:真误报、声明行为的漏报、错误的 AST 语义、scope 解析 bug——立即修;重复 helper、误导性命名——通常修;v1 范围外的漏报覆盖、病态代码的路径爆炸——文档化或延后;扩大规则范围、增加误报的建议——拒绝。每个从 review 修出的真实 bug 都要有回归测试。
至此,一条新规则从一句话定义到 PR 合并的完整路径是:定义契约 → 检查现有模式与工具 → 定精度与 v1 范围 → 先写对抗性测试 → 实现检测器 → 生成注册表 → 本地验证 → OSS eval → 带 eval 表格的 PR。所有命令与检查点都以上述文档为准,规则质量的标准始终是:特定、有据、精确、低噪声、经过对抗性测试、范围克制、可读。
【免费下载链接】react-doctorYour agent writes bad React. This catches it项目地址: https://gitcode.com/GitHub_Trending/re/react-doctor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考