深入探索Eslint【下篇】

738 阅读8分钟

历史(History)

怎么使用 (How to use)

  • .eslintrc配置说明

如何写一个规则(Write a rule)

  • AST
  • code path analysis
  • Eslint工作流程 简单聊下mpx的eslint

如何写一个规则

让我们看看有哪些规则

eslint vue react

有很多规则,眼花缭乱,文档中一些前面打✅的是推荐的,没打的就是根据需求定制,有🔧符号的是可以修复的那种,可以通过--fix或者在编辑器中提示修复选项中修复 让我们看一个例子"no-debugger": "error"

module.exports = {
    meta: {
        type: "problem",
        docs: {
            description: "disallow the use of `debugger`",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-debugger"
        },
        fixable: null,
        schema: [],
        messages: {
            unexpected: "Unexpected 'debugger' statement."
        }
    },
    create(context) {
        return {
            DebuggerStatement(node) {
                context.report({
                    node,
                    messageId: "unexpected"
                });
            }
        };
    }
};

具体字段的解释在官网中解释的很详细

简单来说meta主要是规则的基本配置,描述这个规则,create中是具体实现,这个例子是当代码中出现debugger时会提示报错。

重点讲一下create函数 。 拿上面例子看,create中需要知道的几个元素:

  • 参数context
  • 当遇到debugger时触发DebuggerStatement
  • 返回AST节点node

context

顾名思义,context就是一个对象包含与规则上下文相关的信息,我总结了一些常用的api,比如

  • context.report: 报告错误的代码,比较容易理解
  • context.options:配置中的一些选项,比如
{
    "quotes": ["error", "double"]
}
// context.options[0] === "double"  true
  • context.getScope: 获取当前规则的作用域,它提供一些当前作用域的一些内容,比如:

    • type: 当前作用域类型
    • isStrict: 是不是strict模块
    • upper: 父级作用域
    • childScopes: 子级作用域
    • variables: 声明的一些变量
    • through:由无法解析的变量组成的数组
    • references: 此范围所有引用的数组
    • ...
  • context.getSourceCode: 是获取源代码的api,返回一个SourceCode对象,可以用这个对象返回节点的源码,或者返回节点的第一个或者最后一个token等等对节点文本的查询判断操作。

DebuggerStatement

是AST的一种节点类型,常用的类型

e0870a372ff146a2b6298b25dedef1e2~tplv-k3u1fbpfcp-zoom-1.image.png node 是AST的一个节点,具体可以看一下

d0f48c22710843079edc87c7d29a3c8f~tplv-k3u1fbpfcp-zoom-1.image.png 具体的AST可以尝试在这里写一段代码,看下AST长什么样子

code path analysis

简单来说就是解析代码的一个执行路径,加入一些钩子函数,帮助我们在这个阶段做一些事情。

if (a && b) {
    foo();
}
bar();

我们分析一下上述代码的可能执行路径:

  • 如果 a 为真 - 检测 b 是否为 真
    • 如果 b 为真 — 执行 foo() — 执行 bar()
    • 如果 b 非真 — 执行 bar()
  • 如果 a 非真,执行 bar() 具体的图示:

image.png 这是一个包含AST具体类型的codepath,整体可以看作一个CodePath,然后由四个CodePathSegment组成,Eslint抽象了五个钩子

  • onCodePathStart
  • onCodePathEnd
  • onCodePathSegmentStart
  • onCodePathSegmentEnd
  • onCodePathSegmentLoop 根据函数名称容易想到:整体的开始,结束。各自模块的开始,结束,还有一个就是循环语句 上诉代码的执行顺序应该是
onCodePathStart
onCodePathSegmentStart
onCodePathSegmentEnd
onCodePathSegmentStart
onCodePathSegmentEnd
onCodePathSegmentStart
onCodePathSegmentEnd
onCodePathSegmentStart
onCodePathSegmentEnd
onCodePathEnd

Eslint的工作流程

aaa.png 这张是整体的一个流程,eslint也是这种插件的架构,parser也可以替换,规则可以自己生成配置,通过多个plugin的作用,生成代码的问题列表作用于ide,架构极具灵活性。下面是更深入的探究。

bbb.png

ccc.png 上图是babel对ast的应用,下图是eslint对ast的处理 babel是把code解析成ast然后调整ast产出期望的code eslint则是解析成ast后,然后把eslint拍平并且产生了对称的结构,具体是对这个ast进行深度优先遍历,在遍历节点的开始存下node,结束再存一次,生成了一个这样的数组。

然后再遍历这个ast的数组,如果你的规则中写了相关ast节点的处理,则告知你这个方法执行。比如:code中有debugger,ast中就存在DebuggerStatement,你的rule里写了DebuggerStatement,在遍历数组的时候,它遇到了DebuggerStatement,则告知你执行你的DebuggerStatement方法 这里我就产生一些疑问?

  • 为什么要拍平ast?
  • 为什么要复制一些节点,按照对称的结构存储?

为什么要拍平ast?

eslint明明可以在深度优先遍历ast的时候做告知规则的处理,为什么要构建一个这样的数组,再遍历数组去做呢?这样做不是性能有所缺失嘛。

的确如此,我觉得代码的作者放弃了一些性能,保证了代码的可维护性,就是让遍历ast成为一个单一职责的函数,让具体的规则处理在外部执行。并且遍历ast这个函数在单元测试也得到了复用

为什么要复制一些节点,按照对称的结构存储?

这个则是让规则开发者对静态代码充分的控制,也可以理解为节点的结束回调,可以看到每个ast数组里有两个ast节点,拿DebuggerStatement举例:

  • 第一个节点 DebuggerStatement() { // todo }
  • 第二个节点 DebuggerStatement:exit() { // todo } 理解了上述我再扩展和简述下流程,如下图:

ddd.png

我们从下往上看,eslint的底层是acorn这个库,最早的解析器是esprima,后来用espree。

  • acorn:生成ast的工具库
  • espree:其实就是包装了acorn提供了和esprima一样的接口函数。
  • esquery: 类似jquery,对ast节点和规则的selector进行查找匹配,如果匹配成功则执行相应的规则函数
  • lintingProblems: eslint处理后得到的问题列表
  • eslint-visitor-keys: ast遍历的时候用于找到可遍历的子节点,就比如遍历tree的时候访问的是leftNode或者rightNode,这个库提供ast的节点的可遍历子节点map

{
 "FunctionDeclaration": [
    "id",
    "params",
    "body"
    ],
    // ...
}

从左侧看,.eslintrc中提供的是parser,默认是解析js的espree,当然可以改成解析vue的vue-eslint-parser plugin是相关语言框架的插件,里面可以配置自己的parser,rules等等,并且还得配置processors识别相关的文件比如.vue.mpx等等 rules是具体配置的规则,具体的规则需要在相应的parser下才能生效,如果你配置了个espree,那vue的规则是不会解析js的ast的。 中间内容也是最重要的部分,

  1. 首先eslint整合了规则,把extend的,插件里的规则合并在一起。
  2. 然后根据优先级关系选择解析器,传入code解析出ast,提供给runrules方法使用
  3. 前面做的是准备工作,runrules是核心规则对code的处理.
  4. 首先遍历ast,生成一个回文的ast数组,也即是ast每个节点存在两个
  5. 然后给规则里对ast的处理函数注册监听,达到代码的ast有相应的规则则发送告知处理
  6. 然后遍历ast数组的节点,找到相应的规则,执行规则拿到处理结果
  7. 最后把结果也就是有问题的代码返回给调用方

mpx的Eslint

当然eslint不只是针对js做处理,我觉得只要你的code是有规律的,能生成类似ast的结构都可以做成一个解析器,然后来制定一些规则规范代码。

mpx是滴滴出品的一款小程序框架,不同于业内大部分小程序框架将web MVVM框架迁移到小程序中运行的做法,以小程序原生的语法和技术能力为基础,借鉴参考了主流的web技术设计对其进行了扩展与增强,并在此技术上实现了以微信增强语法为base的同构跨平台输出,详情戳

mpx的eslint大部分都是仿照vue的eslint做的,相比vue,mpx有四个模块,template,js,styles,json,比vue多了json模块,解决这个json模块还遇到了很多问题,比如多了一个script标签,vue的开始设计时就完全没考虑可能存在两个script标签,以至于我们会展示两条相同的错误。

eslint主要作用于三个地方吧,

  • mpx的vscode插件
  • .eslintrc,直接配置的形式,走的是eslint的vscode插件
  • webpack配置中要做个loader(暂时没深入研究)

mpx的vscode插件

如上文我所说的,分为四个模块,每个模块分别解析,不是整体的解析

  • template: 这个用的是eslint-plugin-mpx专门的eslint插件,还有专属的解析器mpx-eslint-parser,插件的主要内容是,一些关于template中小程序标签的规则编写,以及识别.mpx文件。解析器是解析template中的标签,生成一个标签的ast,用于插件编写规则
  • js:这个用的是typescript中提供的语言服务解析出来,不仅可以对js的代码解析,还可以对ts的代码解析
  • styles: 这个是vscode官方提供了一些比如less,css的lint库
  • json:这个是我们找了一个eslint-plugin-jsonc库来解决json的eslint问题

eslint的vscode插件

当然可以在.eslintrc中配置eslint-plugin-mpx,相比于mpx的vscode中的优势在于可以配置很多规则,可以随意下掉规则,mpx的vscode中的规则是固定的必要的规则,比较死板,当然可以通过配置关闭mpx的vscode的eslint。

深入探索Eslint【上篇】

历史 zhuanlan.zhihu.com/p/32297243

原理 zhuanlan.zhihu.com/p/53680918

官网 eslint.cn/