上个月我干了件在团队里挺得罪人的事:把项目根目录下的 eslint 配置从 312 条规则砍到 47 条,顺手删掉了三个社区 config 包。改完之后 CI 上 lint 这一步从 2 分 40 秒降到 38 秒(monorepo,约 8 万行 TS),但真正让我下决心的不是速度。
事情的起因是周一早上 CI 红了一片。查了半天,不是业务代码的问题,是有人升级了 eslint-plugin-import,新版本对 import 顺序的判断变了,几百个文件同时报 warning。那天我们花了两个小时讨论 import 到底该按字母序还是按依赖层级排——顺便说一句,这个讨论在团队里已经不是第一次了,上一次是 2022 年。
两条路线:剥夺讨论权,还是保留讨论权
讨论参数之前得先看两种完全相反的思路。
gofmt 是前者。Go 官方从 2009 年就把它塞进工具链,而且它几乎没有配置项:没有 printWidth 开关,没有 tab/space 选择,tab 宽度就是 8。Russ Cox 在一次访谈里说过大意是,gofmt 不是为了让大家写好看的代码,是为了让 Go 代码长得都一样,这样 diff 里只剩语义变化。Google 内部的 Java 风格和 Linux 内核的 8 空格 tab 是完全不同的两套东西,但 gofmt 不在乎,它只提供一个答案。
Black 是同一路线的 Python 版本,2018 年由 Łukasz Langa 发布,它自己管自己叫「不留情面的格式化器」。它的 printWidth 默认 88 而不是 80,这个数字怎么来的呢?Langa 在 README 里解释过:80 太窄,90 又没必要,88 是个看起来不整但好用的折中。重点是,这个参数虽然存在,但官方把它扔在「你有本事别改」的角落里。
ESLint 是另一条路线,而且是极端的那种。它把一切都暴露出来:几百条规则、每条三档、warn 还是 error 你自己定、加不加 --fix 你自己定、要不要类型信息你自己定。社区 config 包就是在这个基础上叠起来的。airbnb 的 config 有 200 多条规则(分 JS/TS/React 几个包),里面像 no-plusplus 这种直接把 i++ 判成 error 的规则,理由写在文档里是「一元自增会带来隐式类型转换」。说得没错,但 10 年里我们团队因为 i++ 出过一次线上问题吗?没有。
规则膨胀的真实代价,不在报错本身
大部分人对规则太多的抱怨是「烦」。烦其实是小问题,真正的问题是摩擦会让人绕过工具。
我们加了 husky + lint-staged,配的是只对暂存文件跑 lint,本来应该很快。实测下来一个改了 3 个文件的小 commit,pre-commit 要跑 20 到 40 秒,因为打开类型感知规则之后,tsconfig 下的类型图得整个拉起来。我翻了下 git 记录,从 2023 年 8 月到今年 3 月,团队一共提了 4000 多次 commit,其中有 200 多次带 --no-verify。这个比例看着不高,但集中在两个人身上——也就是说,绕过动作是会被传染的,一个人用得顺手,旁边的人就会学。
更隐蔽的代价在 review。一条 warning 堆在 PR 里,reviewer 要花时间判断这到底是新引入的问题还是历史遗留,然后可能顺手又加了一条 disable 注释。我们统计过一次,代码库里 // eslint-disable-next-line 出现了 1300 多次,最集中的两类是 react-hooks/exhaustive-deps 和 @typescript-eslint/no-explicit-any。
所以规则数量不是免费的。每加一条,你都要付三笔账:CI 时间、开发者本地等待时间、以及「这条到底该不该修」的沟通成本。第三笔最贵,而且不会出现在任何报表里。
砍规则的三个标准,以及我留下了什么
我最后用的标准很简单,三条。
第一,Prettier 或 Biome 能做的换行、引号、分号、缩进,全部从 ESLint 里删掉。这是重复建设,而且两边的边界会不断漂移,2023 年 ESLint 官方干脆把 stylistic 规则拆到 @stylistic/eslint-plugin,理由写得很直白:不要再和 formatter 抢地盘。
第二,TypeScript 编译器能查的交给 tsc。strict: true 之后,no-undef、no-unused-vars、no-implicit-any 一大半场景都被覆盖了,而且检查发生在 IDE 里、在编译时,比 lint 早得多。typescript-eslint 官方文档也建议关掉 ESLint 核心的同名规则,改用带类型的版本。
第三,剩下每一条都问:这条规则抓过真实的高频 bug 吗?抓过的留下,抓不到的删。
最后留下的 47 条大概长这样:react-hooks/rules-of-hooks(真抓过 useEffect 写在条件分支里的 bug)、@typescript-eslint/no-floating-promises(抓过至少三处忘记 await 的异步)、import/no-cycle(循环依赖确实坑过一次构建顺序)、no-console 只在 src 目录下开 error 档,外加一批和 eval、dangerouslySetInnerHTML、target=_blank 相关的安全规则。
删掉的有:no-plusplus、react/jsx-props-no-spreading、sort-imports、import/order、各种 max-len、no-magic-numbers,以及 airbnb 里那一长串 prefer-xxx。
一个反直觉的结果
我原本以为规则越多越安全,砍完之后数据打脸了:删掉那 265 条之后,代码库里的 eslint-disable 注释少了七成,--no-verify 基本消失,但报出的真实 error 数量反而涨了——因为大家不再习惯性无视黄色波浪线了。
所以规则遵守率不是被数量堆出来的。规范的有效区间大概是个倒 U 型:0 条的时候各写各的,适度的时候能挡住一批低级错误,超过某个点之后,每加一条都在稀释前面那些规则的可信度。拐点在哪,跟团队规模、代码年龄、CI 速度都有关系,没什么通用答案。你让一个 5 人小组扛 300 条规则和一个 200 人组织扛 300 条规则,完全是两件事。
我现在特别推崇那些「不可配置」的工具。Ruff 在 Python 生态里两三年吃掉 Flake8 + isort + pyupgrade 的活,Biome 在 JS 里往同一个方向走,它们的共同点是把配置面收窄。你可能不同意它某一个默认值——Black 的 88 列、Biome 的缩进宽度——但代价换来的是一件事:团队里再也不会有人因为分号该不该写开一个 40 分钟的会。
我上一份工作在那次会里花了 40 分钟。这次我把配置文件提交上去,总共用了 6 分钟。