前言
前面我们我们已经学会了如何开发一个插件,现在我们来了解一下 ESlint 是怎么运行的,以加深对插件的理解。
先下载一份 ESlint 源码(下面示例代码版本:v8.2.0):
git clone https://github.com/eslint/eslint.git
整体流程
接下来看一下 ESlint 主要的几个流程,然后再一步一步解析源码。
-
使用解析器把代码解析成 AST,并把 AST 传入了 runRule 方法。
-
深度遍历生成的 AST,将每一个 node 传入 nodeQueue 队列中,每个会被传入两次。(node: 节点)
-
遍历所有给定的规则,创建 rule 对象,执行 rule 对象的 create 方法,返回 ruleListeners 对象(这个对象里面包含了 rule 的选择器和回调函数),遍历 ruleListeners 对象(每个 rule 可以有多个选择器),为规则中所有的选择器添加监听事件。
-
遍历 nodeQueue 队列,触发匹配到当前 node 的选择器的监听事件,执行相应的回调函数。
误区说明
很多文章中说:在拿到 AST 之后,ESLint会以 "从上至下" 再 "从下至上" 的顺序遍历每个选择器两次。
我这里解释一下:
1、遍历的是节点,不是选择器。
2、 nodeQueue 队列类似下面的结构,这下知道为什么说每个会被传入两次了吧。
// 当选择器添加了 :exit 修饰符就会在 下一次 遍历到节点的时候触发回调函数
// 可以理解为离开这个节点的时候
[
{
isEntering: true,
node: Node {
type: 'Literal',
start: 17,
end: 22,
loc: [SourceLocation],
range: [Array],
value: '131',
raw: "'131'",
parent: [Node]
}
},
{
isEntering: false,
node: Node {
type: 'Literal',
start: 17,
end: 22,
loc: [SourceLocation],
range: [Array],
value: '131',
raw: "'131'",
parent: [Node]
}
}
...
]
源码解读
生成 AST
// lib/linter/linter.js:1173 行
// 解析代码生成 AST
const parseResult = parse(
text,
parser,
parserOptions,
options.filename
);
// 1184 行
slots.lastSourceCode = parseResult.sourceCode;
// 1202行
const sourceCode = slots.lastSourceCode;
// 执行给定的规则
try {
lintingProblems = runRules(
sourceCode,
configuredRules,
...
);
} catch (err) {
...
}
遍历 AST,生成 nodeQueue 队列
// lib/linter/linter.js:854 行
// 深度遍历生成的 AST,将每一个 node 传入 nodeQueue 队列中
// 上面说的传入了两次就是这里 push 进去了两次,并且使用 isEntering 来标识
Traverser.traverse(sourceCode.ast, {
enter(node, parent) {
node.parent = parent;
nodeQueue.push({ isEntering: true, node });
},
leave(node) {
nodeQueue.push({ isEntering: false, node });
},
visitorKeys: sourceCode.visitorKeys
});
// lib/shared/traverser.js:109 行
// 上面传入的 enter 和 leave 函数分别赋值给 _enter 和 _leave
traverse(node, options) {
this._visitorKeys = options.visitorKeys || vk.KEYS;
this._enter = options.enter || noop;
this._leave = options.leave || noop;
}
// 127 行
// 递归遍历 AST 节点
_traverse(node, parent) {
if (!isNode(node)) {
return;
}
this._current = node;
this._skipped = false;
// push 1 次
this._enter(node, parent);
if (!this._skipped && !this._broken) {
const keys = getVisitorKeys(this._visitorKeys, node);
if (keys.length >= 1) {
this._parents.push(node);
for (let i = 0; i < keys.length && !this._broken; ++i) {
const child = node[keys[i]];
// 遍历每一个key
if (Array.isArray(child)) {
for (let j = 0; j < child.length && !this._broken; ++j) {
// 递归遍历key
this._traverse(child[j], node);
}
} else {
// 递归遍历key
this._traverse(child, node);
}
}
this._parents.pop();
}
}
if (!this._broken) {
// push 2 次
this._leave(node, parent);
}
this._current = parent;
}
在 _traverse 方法中我们可以看到,其实就是在递归遍历 AST 的节点,那么每个节点到底是什么呢?traverser怎么知道遍历哪些字段呢?
看看下面的代码你就明白了:
// lib/shared/traverser.js:12 行
const vk = require("eslint-visitor-keys");
// lib/shared/traverser.js:43 行
function getVisitorKeys(visitorKeys, node) {
let keys = visitorKeys[node.type];
if (!keys) {
keys = vk.getKeys(node);
debug("Unknown node type \"%s\": Estimated visitor keys %j", node.type, keys);
}
return keys;
}
// xxx/node_modules/eslint-visitor-keys/lib/visitor-keys.json
{
...
"Program": [
"body"
]
...
}
当 AST 的类型为 Program 时,节点指的就是 body 里面的数组项(子数组项也一样)。
// 示例的 AST
{
"type": "Program",
"start": 0,
"end": 12,
"range": [
0,
12
],
"body": [
// 节点
{
"type": "VariableDeclaration", // 变量声明
"start": 0,
"end": 12,
"range": [
0,
12
],
"declarations": [
// 节点
...
],
"kind": "let" // 表示使用的是 let 关键字
}
],
"sourceType": "module"
}
遍历规则,给选择器添加监听事件
// lib/linter/linter.js:975 行
Object.keys(ruleListeners).forEach(selector => {
emitter.on(
selector,
timing.enabled
? timing.time(ruleId, ruleListeners[selector])
: ruleListeners[selector]
);
});
遍历 nodeQueue 队列,触发监听事件
// lib/linter/linter.js:992 行
nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
try {
// 进入节点的时候触发监听事件
if (traversalInfo.isEntering) {
eventGenerator.enterNode(currentNode);
} else {
// 离开节点的时候触发监听事件
// 需要给选择器添加 :exit 修饰符,如 Literal:exit
eventGenerator.leaveNode(currentNode);
}
} catch (err) {
err.currentNode = currentNode;
throw err;
}
});
// lib/linter/node-event-generator:295 行
// 匹配到当前节点的选择器的监听事件,执行相应的回调函数。
applySelector(node, selector) {
if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
this.emitter.emit(selector.rawSelector, node);
}
}