ESLint 代码检查的过程是啥?
你有没有遇到过这种场景:CI 上 ESLint 跑了 5 分钟,最后报了一个“no-unused-vars”,你一脸问号:就这?
ESLint 其实干的活比你想的多:找文件、读配置、解析代码、建 AST、跑规则、收集问题、还能顺手给你修一部分。它慢,很多时候不是“它摆烂”,而是“它真在干活”。
一句话版本:ESLint 在做什么?
把 ESLint 想象成“代码体检”就行:
- 把源代码变成 AST(抽象语法树)
- 用一堆规则(rules)去遍历 AST,找到问题
- 输出诊断结果(可选:自动修复一部分)
你看到的每一条报错,本质上就是:“在某个 AST 节点上,这条规则觉得你写得不对。”
ESLint 的完整流水线(从命令到报错)
先上一个总览流程图,后面逐段拆开。
flowchart TD
A[eslint CLI / IDE] --> B[解析参数与工作目录]
B --> C[加载配置<br/>flat config 或 eslintrc]
C --> D[构建规则集/插件/共享配置]
D --> E[收集目标文件<br/>glob/ignore/overrides]
E --> F[读取文件内容]
F --> G[选择 parser & parserOptions]
G --> H[解析为 AST]
H --> I[构建 scope/变量引用]
I --> J[运行 rules 遍历 AST]
J --> K[生成 messages + fixes]
K --> L[--fix 应用修复并回写文件]
K --> M[formatter 输出结果]
M --> N[退出码<br/>error/warn/max-warnings]
1) 入口:CLI / IDE 触发
两种常见入口:
- 你在终端跑:
npx eslint . - 你在编辑器里保存文件,IDE 插件后台跑(你看到红线/黄线)
这俩本质都是“调用 ESLint 引擎”,只是入口不同,默认参数可能不一样(比如 IDE 往往只 lint 当前文件,CLI 会扫整个项目)。
2) 配置加载:flat config vs .eslintrc
ESLint 首先要搞清楚:“你想让它按什么规则检查?”
目前你可能会遇到两种体系:
2.1 传统:.eslintrc.*(层层合并)
比如 .eslintrc.js/.json,特点是:
- 支持
extends一层层叠加 - 支持
overrides按文件类型覆盖 - 支持“从当前目录往上找”,一路找到项目根
2.2 新的:flat config(eslint.config.js)
flat config 的心智更直:
- 配置就是一个数组:从上到下匹配、合并
- 更偏“JS 代码配置”,可组合性更强
- 很多新生态(尤其新版本插件)优先支持它
你不需要纠结“哪种更好”,你只要记住:ESLint 会先把配置解析成“最终规则集”,后面流程才能继续。
3) 文件收集:哪些文件要检查?
这一步决定了“ESLint 要跑多大范围”,也决定了你 lint 为啥慢。
ESLint 会综合这些来源来决定文件列表:
- CLI 传入的路径 / glob(比如
eslint src/eslint "src/**/*.{ts,tsx}") - ignore(
.eslintignore、ignores、默认忽略node_modules) overrides/ flat config 的 file patterns(某些规则只对某些文件生效)
一个很现实的经验:你 lint 的文件越多,ESLint 越慢;你让它去扫大目录,它就会真的去扫。
4) 解析:从文本到 AST
ESLint 不直接“看字符串”,它要先解析成 AST。解析这一步由 parser 决定:
- 默认 parser:Espree(支持 JS)
- TS 常用:
@typescript-eslint/parser - Vue/Svelte 等:往往有各自的 parser 或 processor
大概过程是:
- 读取文件内容(字符串)
- 按
parserOptions(比如ecmaVersion、sourceType)解析 - 得到 AST
如果你看到那种报错:
Parsing error: Unexpected token
十有八九是 parser / parserOptions 不对,或者你 lint 了不该 lint 的文件类型。
5) 建立“变量与作用域”信息(scope)
很多规则并不只是“看一眼节点就结束”,它需要知道:
- 这个变量在哪声明?
- 在哪被引用?
- 有没有引用但没用?
- 是不是 shadow 了外层变量?
所以 ESLint 会在 AST 基础上构建 scope 信息(变量/引用关系)。
这也是为什么 no-unused-vars 这种规则看起来简单,实际上要做不少分析工作。
6) 跑规则:rules 如何工作?
核心来了:ESLint 的 rule 本质是一个“AST 监听器”。
它一般长这样:
1 | module.exports = { |
你可以把它理解成:
- ESLint 遍历 AST
- 走到某种节点(比如
CallExpression)时,把节点扔给对应 rule 的处理函数 - rule 决定要不要报错、报什么、能不能修
rule 的输出是什么?
每条问题通常包含:
- 文件名、行列号
- rule 名(比如
no-undef) - message(告诉你哪里不对)
- severity(warn / error)
- 可选:fix(怎么改)
7) 修复:–fix 到底做了什么?
当你跑:
1 | eslint . --fix |
ESLint 会在拿到规则给出的 fix 后,按一定策略把修复应用到源文本上,然后回写文件。
注意:不是所有规则都能 fix。
- 有些是“可安全自动修复”的(比如补分号、调整引号、简单替换)
- 有些是“需要你做决定”的(比如逻辑改写、可能影响行为)
所以 --fix 更像“能修的我帮你修”,不是“一键全修复”。
8) 输出:formatter + 退出码
最后 ESLint 会把结果格式化输出(终端表格、json、stylish 等),然后决定退出码:
- 0:没有 error(注意:有 warn 也可能是 0,取决于配置)
- 非 0:有 error,或触发了
--max-warnings
这就是为什么 CI 里经常会配:
1 | eslint . --max-warnings 0 |
意思是:别给我“警告也算过”,要么干净,要么挂。
ESLint 为什么会慢?(最常见 4 个原因)
- 扫的文件太多:路径太大、glob 太宽、ignore 没写好
- TS 语义规则开太多:尤其是带 type 信息的规则,会读
tsconfig,成本很高 - 插件规则太重:有些 rule 本身就复杂
- 没用缓存:同样的文件每次都全量 lint
如果你是大项目,建议至少了解一下缓存:
1 | eslint . --cache |
它能让“没变的文件”少跑很多。
结尾
ESLint 的检查过程其实就是一条流水线:配置 → 找文件 → 解析 AST → 跑规则 → 输出/修复。
你把这条链路想明白了之后:
- 遇到误报:你会去查 parser/config/rule,而不是盲目关规则
- 遇到慢:你会先收敛文件范围、再看 TS type-aware 规则、再上 cache
- 需要定制:你也知道“写 rule”本质就是“写 AST 监听器”
Happy Coding!
