阅读 101

ESlint 运行原理分析

前言

前面我们我们已经学会了如何开发一个插件,现在我们来了解一下 ESlint 是怎么运行的,以加深对插件的理解。

先下载一份 ESlint 源码(下面示例代码版本:v8.2.0):

git clone https://github.com/eslint/eslint.git 复制代码

接下来看一下 ESlint 主要的几个流程,然后再一步一步解析源码。

  1. 使用解析器把代码解析成 AST,并把 AST 传入了 runRule 方法。

  2. 深度遍历生成的 AST,将每一个 node 传入 nodeQueue 队列中,每个会被传入两次。(node: 节点)

  3. 遍历所有给定的规则,创建 rule 对象,执行 rule 对象的 create 方法,返回 ruleListeners 对象(这个对象里面包含了 rule 的选择器和回调函数),遍历 ruleListeners 对象(每个 rule 可以有多个选择器),为规则中所有的选择器添加监听事件。

  4. 遍历 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


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐