ESLint 规则渐进式升级:从 0 error 到全量 strict 的过渡策略
一、一次性开启所有 strict 规则,项目直接跑不动
在 200k 行代码的 monorepo 上一次性开启 ESLint 的全部 strict 规则,结果:3000+ error、1500+ warning。CI 直接红掉——所有 PR 都无法合并。开发者要么花一周时间修复所有 lint 问题(期间不能合并任何代码),要么关闭 ESLint(回到没有规则的状态)。
这两种选择都不对。正确的做法是渐进式升级:从当前状态出发,逐步收紧规则,每一步都确保 CI 绿色。具体策略:先把所有现有 error 改为 warning(CI 仍然绿),再每周把 1-2 个 warning 升级为 error(开发者每周只需要修几个问题)。3 个月后,所有目标规则都变成了 error——项目从"没有 lint"过渡到了"全量 strict",但过程中没有任何一天 CI 是红的。
渐进式升级的核心原则:永远不让 CI 红掉。CI 红掉意味着所有开发工作被阻塞——这是不可接受的代价。渐进式升级保证每一步都是向前推进,没有回退。
flowchart TD A[现状: 0 条 lint 规则] --> B[阶段1: 全部 warning] B --> C[阶段2: 每周升级 2 条为 error] C --> D[阶段3: 80% strict] D --> E[阶段4: 100% strict] B --> B1[3000 warning<br/>CI 仍然绿] C --> C1[每周修 20-50 个问题<br/>单次 PR 改动量可控] D --> D1[500 warning + 2500 error<br/>核心规则已强制] E --> E1[0 warning + 全量 error<br/>新代码 100% 合规]二、规则的四象限分类
不是所有规则都值得立即升级。按两个维度分类:bug 发现能力(这条规则能否发现真实 bug)和变更频率(违反这条规则的代码多久变一次)。
| 象限 | bug 发现能力 | 变更频率 | 升级策略 | 示例规则 |
|---|---|---|---|---|
| 优先升级 | 高 | 低 | 立即升为 error | no-undef,no-unused-vars,no-console |
| 渐进升级 | 高 | 高 | 每周升 1 条为 error | no-shadow,eqeqeq,no-implicit-coercion |
| 暂缓升级 | 低 | 低 | 先 warning,3 个月后评估 | prefer-const,no-var,camelcase |
| 不升级 | 低 | 高 | 永远 warning 或关闭 | max-lines,complexity,no-magic-numbers |
优先升级象限的规则是那些"发现真实 bug 且修改成本低"的规则。no-undef(使用了未定义的变量)是最典型的——违反这条规则几乎 100% 是 bug,且修复只需要加一个声明或 import。这类规则应该立即升为 error——没有任何理由容忍未定义变量在生产代码中存在。
不升级象限的规则是那些"发现不了 bug 且修改成本高"的规则。max-lines(文件最大行数)是个例子——一个 300 行的文件不一定比 100 行的文件差,强行拆分可能反而降低可读性。这类规则永远保持 warning 或直接关闭。
规则排序的优先级公式
优先级 = bug 发现能力 × 违反频率。bug 发现能力高的规则如果违反频率极低(如no-undef,新代码很少违反),优先级反而不高——因为不修也不影响太多文件。真正需要优先处理的是 bug 发现能力高 + 违反频率中等的规则——如eqeqeq(用===而非==),大量代码用==且确实可能导致类型比较 bug。
三、lint-staged 限制修改范围
渐进式升级的关键工程手段:lint-staged。只在 staged 文件(git add 的文件)上运行 lint——而非全量扫描 200k 行代码。这样做的好处:
- 修改范围可控:开发者每次 PR 只需要修自己改动的文件中的 lint 问题,不需要修整个项目
- 反馈速度快:只 lint 几个文件(约 100-500 行),耗时 < 5 秒。全量 lint 200k 行需要 30-60 秒
- 新代码先合规:staged 文件通常是开发者新写或修改的代码——这些代码先达到 strict 标准,旧代码暂时容忍
lint-staged 的配置:
{ "lint-staged": { "*.{js,ts}": [ "eslint --fix", "eslint --max-warnings=0" ], "*.{css,scss}": [ "stylelint --fix" ] } }--max-warnings=0的含义:staged 文件中不允许有任何 warning——新代码必须完全合规。这是一个渐进策略:旧代码可以有 warning(CI 不红),但新代码连 warning 都不允许——逐步收紧,直到所有代码都达到 strict 标准。
lint-staged 的常见配置错误
错误一:在 lint-staged 中运行全量 lint(eslint .)。这会导致每次 commit 都检查整个项目——开发者改一个文件却要等全量扫描结果。正确做法:lint-staged 只运行eslinton staged files。
错误二:在 lint-staged 中加了git add(自动 add fix 后的文件)。这个操作会修改 commit 内容——开发者不知道 lint-fix 改了什么。推荐:eslint --fix 只做自动修复(如加分号、调顺序),不做逻辑修改。开发者应该 review fix 结果后再手动 add。
四、ESLint 配置渐进升级实现
// eslint.config.mjs —— 渐进式 ESLint 配置 // 使用 ESLint Flat Config (v9+) const ERROR = "error"; const WARN = "warn"; const OFF = "off"; // ============================================================ // 渐进式升级追踪表 // 记录每条规则的当前状态和升级计划 // ============================================================ const RULE_UPGRADE_TRACKER = { // 优先升级——已在阶段1升为 error "no-undef": { status: ERROR, since: "2024-01", reason: "100% 是 bug" }, "no-unused-vars": { status: ERROR, since: "2024-01", reason: "dead code 是浪费" }, // 渐进升级——正在从 warning 升为 error "eqeqeq": { status: WARN, plannedError: "2024-03", reason: "== 导致类型比较 bug" }, "no-shadow": { status: WARN, plannedError: "2024-03", reason: "变量遮蔽导致逻辑混乱" }, "no-implicit-coercion": { status: WARN, plannedError: "2024-04", reason: "隐式类型转换是 bug 源头" }, // 暂缓升级——先 warning,3 个月后评估 "prefer-const": { status: WARN, plannedError: "2024-06", reason: "const 是好习惯但不是 bug" }, "no-var": { status: WARN, plannedError: "2024-06", reason: "var 应淘汰但修改量大" }, "camelcase": { status: OFF, reason: "后端 API 字段可能用 snake_case" }, // 不升级——永远 warning 或关闭 "max-lines": { status: OFF, reason: "文件长度不代表质量" }, "complexity": { status: WARN, reason: "复杂度是信号而非规则" }, "no-magic-numbers": { status: OFF, reason: "0、1、-1 不是 magic number" }, }; // 根据追踪表生成规则配置 function buildRules(tracker) { const rules = {}; for (const [ruleName, config] of Object.entries(tracker)) { rules[ruleName] = config.status; } return rules; } const config = [ { files: ["**/*.{js,ts,jsx,tsx}"], languageOptions: { ecmaVersion: 2022, sourceType: "module", }, rules: buildRules(RULE_UPGRADE_TRACKER), }, // 测试文件——允许更宽松的规则 // 测试代码的 bug 发现能力要求低于业务代码 { files: ["**/*.test.{js,ts,jsx,tsx}", "**/*.spec.{js,ts,jsx,tsx}"], rules: { "no-console": OFF, // 测试中允许 console.log "max-lines": OFF, // 测试文件可以更长 "no-magic-numbers": OFF, // 测试中硬编码数字是合理的 }, }, // 配置文件——更宽松 { files: ["*.config.{js,ts}", ".*.config.{js,ts}"], rules: { "no-console": OFF, "no-magic-numbers": OFF, "camelcase": OFF, // 外部配置可能用 snake_case }, }, ]; export default config;每周升级流程
每周选择 1-2 条 warning 规则升为 error。升级步骤:
- 在 RULE_UPGRADE_TRACKER 中把该规则的状态从 WARN 改为 ERROR
- 运行
eslint . --rule "rule-name: error"统计该规则在全项目中的违反次数 - 如果违反次数 < 50,直接升级——开发者一周内可以修完
- 如果违反次数 > 50,拆分为两周升级——第一周只修一半文件
- 提 PR 修改 eslint 配置 + 修复所有违反该规则的代码
- PR review 并合并——CI 绿色确认
降级策略
如果某条规则升为 error 后导致大量开发摩擦(如camelcase强制驼峰命名,但项目中大量后端 API 返回 snake_case 字段),可以降回 warning。降级不是"放弃"——而是承认当前约束不适合项目现实。降级应该在配置注释中说明原因和重新评估的时间。
五、渐进式升级的代价
配置维护成本
RULE_UPGRADE_TRACKER 需要定期更新——每周至少改一次(升级新规则)。如果团队有多个 ESLint 配置(monorepo 中不同子项目有不同规则),每个配置都需要独立的追踪表。
开发者体验摩擦
每次升级新规则,开发者需要修改自己的代码以适应新约束。如果升级频率太高(每周 5 条),开发者会感到"规则总是在变"——产生抵触情绪。推荐:每周最多升级 1-2 条——慢比快好。
规则冲突
某些规则之间有冲突。如prefer-const要求变量声明为 const,但如果变量需要重新赋值(如循环中的计数器),prefer-const会报 warning。这时候no-plusplus(禁止i++)又和循环计数器的写法冲突。解决方案:规则应该有合理的例外配置(如prefer-const: ["error", { destructuring: "all" }]),而非一刀切。
Trade-off 讨论
- 升级速度 vs 开发者接受度:每周升 2 条规则,3 个月达到 strict——开发者每次只修少量问题。每周升 5 条规则,1 个月达到 strict——但开发者可能一次要修数百个问题,产生抵触。推荐:慢比快好——宁可花 3 个月渐进升级,也不要花 1 个月暴力升级后团队集体关闭 ESLint。
- lint-staged vs 全量 lint:lint-staged 只检查新代码,反馈快但旧代码的 lint 问题一直积累。全量 lint 每次检查所有代码,反馈慢但确保整体合规。推荐:lint-staged 为日常开发,全量 lint 为每周定期扫描——两者结合。
- 自动修复 vs 手动修复:
eslint --fix可以自动修复约 60% 的 lint 问题(如加分号、调顺序、改引号)。但 40% 的问题需要手动判断(如no-shadow需要开发者决定重命名方案)。推荐:先用 --fix 自动修复可修复的问题,再手动处理需要判断的问题。
六、总结
ESLint 规则的渐进式升级不是"妥协",是工程策略。在 200k 行代码上一次性开启所有 strict 规则,结果是 CI 红掉、开发停滞、团队绕过 lint——最后比没有 lint 更差。渐进式升级保证每一步 CI 都是绿色,每一步都在向前推进。核心方法论:按 bug 发现能力和变更频率做四象限分类,优先升级高 bug 发现 + 低变更频率的规则,暂缓低 bug 发现 + 高变更频率的规则。lint-staged 是渐进式升级的工程保障——新代码先达到 strict 标准,旧代码逐步跟进。最终的收益不是"规则数量",是"每条规则都有存在的理由"——一个 50 条合理规则的配置远比 200 条盲目开启的配置有价值。