ESlint 运行原理分析
前言
前面我们我们已经学会了如何开发一个插件,现在我们来了解一下 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); } }
作者:Gavin
链接:https://juejin.cn/post/7028040889335283749