ESlint 运行原理分析

768 阅读3分钟

前言

前面我们我们已经学会了如何开发一个插件,现在我们来了解一下 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);
  }
}