好奇:eslint的工作原理

需要了解的概念

1. 抽象语法树AST(Abstract Syntax Tree)

维基百科中的解释:
源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

如webpack、rollup、UglifyJS、Lint等很多的工具和库的核心都是通过Abstract Syntax Tree 抽象语法树这个概念来实现对代码的检查、分析等操作的。
AST explorer这个网站可以在线生成AST。

好奇:eslint的工作原理_第1张图片
image.png

2. JavaScript Parser

JavaScript Parser,把js源码转化为抽象语法树的解析器。

浏览器会把js源码通过解析器转为抽象语法树,再进一步转化为字节码或直接生成机器码。

一般来说每个js引擎都会有自己的抽象语法树格式,Chrome的v8引擎,firefox的SpiderMonkey引擎等等,MDN提供了详细SpiderMonkey AST format的详细说明,算是业界的标准。

发展到现在可能不同的JavaScript Parser的AST格式会不同,或基于SpiderMonkey AST format,或重新设计自己的AST format,或基于SpiderMonkey AST format优化改进。通过优化抽象语法树,来使程序运行的更快,也是一种提高效率的方法。
常用的JavaScript Parser有:

  • Esprima
  • UglifyJS2
  • Traceur
  • Acorn
  • Shift

eslint用的是espree

抽象语法树的用途

  • 代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码错误提示、代码自动补全等等
    • IDE的错误提示、格式化、高亮、自动补全等等
    • Eslint等代码风格工具
  • 代码混淆压缩
    • UglifyJS2等
  • 优化变更代码,改变代码结构使达到想要的结构
    • 代码打包工具webpack、rollup等等
    • CommonJS、AMD、CMD、UMD等代码规范之间的转化
    • CoffeeScript、TypeScript、JSX等转化为原生Javascript

Eslint如何工作的

eslint产生的背景

C 语言诞生之初,程序员编写的代码风格各异,在移植时会出现一些因为不严谨的代码段导致无法被编译器执行的问题。于是在 1979 年,一款叫 lint的程序被开发出来,能够通过扫描源代码检测潜在的错误。

最初javascript开发出来是在web中实现简单的交互(表单提交),后来随着互联网发展,需要展示更多的东西,业务日渐复杂化,前端项目越来越庞大。再加上 JavaScript 本身设计上存在许多缺陷,代码不严谨也可能就会触发神奇的错误。

所以语法检测工具就诞生了。在市场检验下,eslint脱颖而出。参考资料eslint起源。

eslint配制

ESlint 被设计为完全可配置的,这意味着你可以关闭每一个规则而只运行基本语法验证,或混合和匹配 ESLint 默认绑定的规则和你的自定义规则,以让 ESLint 更适合你的项目。可以查看详细eslint配置官方文档。

执行下面命令可以创建一个配置

eslint --init
/** .eslintrc.js */
module.exports = {
    "extends": "eslint:recommended",
    "rules": {
        // enable additional rules
        "indent": ["error", 4],
        "linebreak-style": ["error", "unix"],
        "quotes": ["error", "double"],
        "semi": ["error", "always"],

        // override default options for rules from base configurations
        "comma-dangle": ["error", "always"],
        "no-cond-assign": ["error", "always"],

        // disable rules from base configurations
        "no-console": "off",
    }
}
  • extends 属性值可以是:

    • 在配置中指定的一个字符串
    • 字符串数组:每个配置继承它前面的配置

ESLint 递归地进行扩展配置,所以一个基础的配置也可以有一个 extends 属性。

  • rules 属性可以做下面的任何事情以扩展(或覆盖)规则:

    • 启用额外的规则
    • 改变继承的规则级别而不改变它的选项:
      • 基础配置:"eqeqeq": ["error", "allow-null"]
      • 派生的配置:"eqeqeq": "warn"
      • 最后生成的配置:"eqeqeq": ["warn", "allow-null"]
    • 覆盖基础配置中的规则的选项
      • 基础配置:"quotes": ["error", "single", "avoid-escape"]
      • 派生的配置:"quotes": ["error", "single"]
      • 最后生成的配置:"quotes": ["error", "single"]

最出名的两个风格:airbnb、standard

rule是怎么工作的?

先了解一下rule的结构,官方文档rule。

eslint推荐规则rule列表里我们选一个常见而且简单的规则来学习:no-const-assign 禁止修改 const 声明的变量。在eslint源码库中找到对应的这条规则的源码。

const astUtils = require("../util/ast-utils");

module.exports = {
   meta: {
       type: "problem",

       docs: {
           description: "disallow reassigning `const` variables",
           category: "ECMAScript 6",
           recommended: true,
           url: "https://eslint.org/docs/rules/no-const-assign"
       },

       schema: [],

       messages: {
           const: "'{{name}}' is constant."
       }
   },

   create(context) {

       /**
        * Finds and reports references that are non initializer and writable.
        * @param {Variable} variable - A variable to check.
        * @returns {void}
        */
       function checkVariable(variable) {
           astUtils.getModifyingReferences(variable.references).forEach(reference => {
               context.report({ node: reference.identifier, messageId: "const", data: { name: reference.identifier.name } });
           });
       }

       return {
           VariableDeclaration(node) {
               if (node.kind === "const") {
                   context.getDeclaredVariables(node).forEach(checkVariable);
               }
           }
       };

   }
}

每条rule是一个暴露的nodejs模块,模块包含两个属性:metacreate

  • meta:包含规则的元数据。类别,文档,可接收的参数的 schema,messages等
  • create:create (function) returns an object with methods that ESLint calls to “visit” nodes while traversing the abstract syntax tree (AST as defined by ESTree) of JavaScript code:
    • if a key is a node type or a selector, ESLint calls that visitor function while going down the tree
    • if a key is a node type or a selector plus :exit, ESLint calls that visitor function while going up the tree
    • if a key is an event name, ESLint calls that handler function for code path analysis

我的理解就是create返回的这个对象包含三种不同类型的函数

  • AST中选择器名字命名的函数,例如VariableDeclaration
  • AST中选择器名字+:exit的函数, 列入FunctionExpression:exit
  • code path 抽象的 5 个事件onCodePathStartonCodePathEndonCodePathSegmentStartonCodePathSegmentEndonCodePathSegmentLoop

当触发这个函数发现不符合规则的时候会调用context.report()抛出问题。
比如,当我们代码中出现修改const声明的变量phone的时候,就会抛出phone is constant.的错误

举个有完整的create栗子:array-callback-return 的rule:

function checkLastSegment (node) {
    // report problem for function if last code path segment is reachable
}

module.exports = {
    meta: { ... },
    create: function(context) {
        // declare the state of the rule
        return {
            ReturnStatement: function(node) {
                // at a ReturnStatement node while going down
            },
            // at a function expression node while going up:
            "FunctionExpression:exit": checkLastSegment,
            "ArrowFunctionExpression:exit": checkLastSegment,
            onCodePathStart: function (codePath, node) {
                // at the start of analyzing a code path
            },
            onCodePathEnd: function(codePath, node) {
                // at the end of analyzing a code path
            }
        };
    }
};

code-path-analysis

  • code-path的官方文档。

code-path就是程序的执行路径。我们的代码会被解析成一些code path,单个code path又是多个CodePathSegment组成。

看一下官方文档中的例子,这段if代码段:

if (a && b) {
    foo();
}
bar();
好奇:eslint的工作原理_第2张图片
image.png

ESLint 将 code path 抽象为 5 个事件,在rule里可以给这些事件绑定函数。

那eslint是如何触发rule里的函数的呢?

查看官方文档

eslint 对象的主要方法是 verify(),接收两个参数:要验证的源码文本和一个配置对象(通过准备好的配置文件加命令行操作会生成配置)。该方法首先使用 espree(或配置的解析器) 解析获取的文本,检索 AST。AST 用来产生行/列和范围的位置,对报告问题的位置和检索与 AST 节点有关的源文本很有帮助。

一旦AST是可用的,estraverse 被用来从上到下遍历 AST。在每个节点,eslint对象触发与该节点类型同名的一个事件(即 “Identifier”,”WithStatement” 等)。在回退到子树上时,一个带有 AST 类型名称和 “:exit” 后缀的事件被触发,比如 “Identifier:exit” - 这允许规则在正向和逆向遍历开始起作用。每个事件在恰当的 AST 节点可用时触发。

现在我们已经深入的了解rule了。

你可能感兴趣的:(好奇:eslint的工作原理)