Search K
Appearance
Appearance
前端的命令行工具太多了,比如 webpack、vite、babel、tsc、eslint 等等,每天我们都会用各种命令行工具。
这些命令行工具都提供了两种入口:命令行和 api。
平时用我们会通过命令行的方式,比如 eslint xxx --fix,但是别的工具集成这些工具的时候就会使用 api 了,它更灵活。
所以,调试这些工具的时候也就有两种方式,通过命令行调试和通过 api 调试。
这节我们以 eslint 为例子来试下两种调试方式,大家可以跟着调试一下。
我们创建一个 index.js 文件

配置下 .eslintrc
module.exports = {
extends: "standard",
};安装 eslint,然后执行 npx eslint ./index.js 会看到这样的报错:

后面三个错误都是格式错误,eslint 可以修复。
执行 npx eslint ./index.js --fix 就会自动修复错误。

我们想探究下 fix 的原理,就要调试下源码了。
用命令行的方式调试的话,就要添加一个这样的调试配置:

{
"name": "eslint 调试",
"program": "${workspaceFolder}/node_modules/.bin/eslint",
"args": ["./index.js", "--fix"],
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"cwd": "${workspaceFolder}",
"request": "launch",
"type": "node"
}在 .bin 下找到 eslint 的文件打个断点:

然后调试启动:

代码执行到这里断住了。
你发现它引入了 lib/cli 的模块,然后再去那个模块里看一下:

你会发现它创建了 ESLint 实例,然后调用了它的 lintFiles 方法。
点进去就会发现它调用了 executeOnFiles 方法:

点击 step into 进入函数内部:

然后点击 stop over 单步执行:

之后代码会走到 verifyText 的函数调用:

这就是实现 lint 的部分,因为它的返回值就是 lint 结果。
step into 进入这个函数,会执行到 verifyAndFix 的函数调用:

继续 step into,然后单步执行。
这时候你发现了一个循环:

每次都调用 verify 对传入的文本进行 lint:


返回的结果里包含了错误的信息和如何修复,就是把 range 范围的字符串替换成 text。
然后再调用 applyFixes 执行 fix 的修复。
我们分别看下如何 lint 的,以及如何 fix 的:
verify 部分调用了 _verifyWithoutProcessors:

单步执行你会发现会对文本做 parse,产生 AST:

然后传入 AST,调用各种 rule 来实现检查,返回的结果就是 problem 的数组:

具体调用插件的过程,大家可以调试下 runRules 这个方法,这里就不展开了。
然后拿到了 problems 和怎么 fix 的信息就可以执行自动修复了:
进入 applyFixes 方法,你会发现它会有个标记变量,默认为 false:
然后尝试修复,修复完设置为 true:

具体修复的实现就是个字符串替换:

那为什么会有 remainingMessages,也就是剩下的错误,然后还要循环多次来修复呢?
有两个原因,一个是有的 problem 本身不支持 fix,没有 fix,那自然要把问题留下来:


还有一个原因是两个 fix 冲突了:
比如上一个 fix 修复完之后,最新的位置是 10,而下一个 fix 要求从 9 开始替换文本,那自然就不行了。所以这条 fix 就被留了下来:

这也是为什么我们会看到这样一个循环:

如果有 fix 冲突的话,也就是上次修复完的文本还有问题,那会重新 lint,然后再 fix。
当然,自动 fix 的次数也是有上限的,默认修复 10 次还没修复完就终止。
这就是 eslint 的实现原理:

lint 的实现是基于 AST,调用 rule 来做的检查。
fix 的实现就是字符串的替换,多个 fix 有冲突的话会循环多次修复。
我们通过命令行的方式实现了 ESLint 源码的调试,但其实这样有很多没必要的部分,比如我们知道前面的命令行参数的解析的流程。
如果我们知道它最终调用的是 lintText 的 api,那完全可以从 api 入口开始调试:
通过命令行的方式调试 ESLint 源码的时候,我们知道了 ESLint 会创建 ESLint 实例,然后调用 lintText 方法来对代码 lint。
那我们可以自己调用这些 api 来 lint。
.eslintrc 文件还是这样:
module.exports = {
extends: "standard",
};通过 api 的方式对这段代码进行 lint:
const {ESLint} = require("eslint");
const engine = new ESLint({
fix: false,
});
(async function main() {
const results = await engine.lintText(`
function add (a, b)
{
return a + b
}
`);
console.log(results[0].output);
const formatter = await engine.loadFormatter("stylish");
const resultText = formatter.format(results);
console.log(resultText);
})();返回的结果用 formatter 打印下。
首先直接 node 执行,可以看到打印出的错误和我们用命令行的方式是一样的:

然后创建个 debug 配置来跑:

{
"name": "调试 eslint api",
"program": "${workspaceFolder}/api-test.js",
"request": "launch",
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal",
"type": "pwa-node"
}你可以直接从实现 lint 的部分开始调试,跳过了前面命令行参数解析的部分,这样更有针对性:

之后的调试流程倒是和命令行的方式一样,就不展开了。
命令行工具都有命令行和 api 两种入口,我们以 ESLint 源码的调试为例试了下两种调试方式。
api 的方式更精准一些,可以跳过命令行参数解析的部分,直接调试感兴趣的 api。
通过调试,我们知道了 ESLint 是通过 AST 实现的检查,具体检查是 rule 里做的,fix 的实现就是字符串替换,但因为多个 rule 的 fix 可能冲突,所以会循环来做,但最多循环 10 次
ESLint 源码的调试还是相对简单,因为没有经过编译,如果做了编译的话,那就需要 sourcemap 了。