阅读 1183

TSLint 和 ESLint 是怎么融合在一起的

Eslint 可以静态检查 javascript 代码一些逻辑上的错误,还有一些代码格式的错误。原理是把代码 parse 成 AST,然后基于 AST 来检查一些问题。

Tslint 可以静态检查 typescript 代码的一些逻辑上的错误,一些代码格式的错误。原理也是基于 AST 的。

既然都是基于 AST,而且做的事情差不多,那为啥不合并到一起呢?

后来,还真合并了,tslint 合并到了 eslint 中,把 tslint 标记为了废弃。

但是两者毕竟是不同的 AST,而且 tslint 里还有一些类型检查相关的逻辑,这是 eslint 不支持的。那它们是怎么融合的呢?

本文我们就来探索一下。

不同的 AST

eslint 有自己的 espree 的 parser 和相应的 AST。

typescript 也有自己的 parser 和相应的 AST。

babel 也有自己的 parser 和相应的 AST。

这些 AST 之间的关系是什么?

最早的 parser 是 esprima,它参考了 Mozilla 浏览器的 SpiderMonkey 引擎的 AST 的标准,然后做了扩充。后来形成了 estree 标准。

后面的很多 parser 都是对这个 estree 标准的实现和扩充。esprima、espree、babel parser(babylon)、acorn 等都是。

当然,也有不是这个标准的,自己实现了一套的 typescript、terser 等的 parser。

他们之间的关系如图所示:

esprima 和 acorn 都是 estree 标准的实现,而 acorn 支持插件机制来扩充语法,所以 espree 和 babel parser 是直接基于 acorn 来实现的。

terser、typescript 等则是另外的一套。

所以,对于 JS 的 AST,我们可以简单的划分为两类: estree 系列、非 estree 系列。

可以借助 astexplorer.net 这个工具来可视化的查看不同 parser 产生的 AST。

espree 就是 eslint 自己实现的 parser,但是它毕竟主要是来做代码的逻辑和格式的静态检查的,在新语法的实现进度上比不上 babel parser。所以 eslint 支持了 parser 的切换,也就是可以在配置不同的 parser 来解析代码。

配置文件里面可以配置不同的 parser,并通过 parserOptions 来配置解析选项。

下面分别讲下 eslint、typescript、babel、vue 等的 parser 怎么在 eslint 中使用:

  • 默认 parser 是 espree。parse 出来的是 estree 系列的 AST,一系列 rule 都是基于这些 AST 实现的。
{
    "parserOptions": {
        "ecmaVersion": 6,
        "sourceType": "module",
        "ecmaFeatures": {
            "jsx": true
        }
    }
}
复制代码
  • 可以通过 @babel/eslint-parser 来切换到 babel 的 AST,它也是 estree 系列,但是支持的语法更多,在 babel7 之后,支持 typescript、jsx、flow 等语法的解析。
{
  parser: "@babel/eslint-parser",
  parserOptions: {
    sourceType: "module",
    plugins: []
  },
}
复制代码
  • 可以通过 @typescript-eslint/parser 来切换到 typescript 的 parser,它可以 parse 类型的信息。
{
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
      "project": "./tsconfig.json"
  }
}
复制代码
  • 可以通过 vue-eslint-parser 来解析 vue 的单文件组件,因为 vue 组件代码同样通过 eslint 来检查规范和逻辑错误,所以实现了对应的 parser。
{
    "parser": "vue-eslint-parser",
    "parserOptions": {
        "sourceType": "module",
        "ecmaVersion": 2018,
        "ecmaFeatures": {
            "globalReturn": false,
            "impliedStrict": false,
            "jsx": false
        }
    }
}
复制代码

而且单文件组件中的 js 部分还可以分别指定不同的 parser。

{
    "parser": "vue-eslint-parser",
    "parserOptions": {
        "parser": {
             // 指定默认 js 的 parser
            "js": "espree",
             // 指定 `<script lang="ts">` 时的 parser
            "ts": "@typescript-eslint/parser",
             // 指定模版中的一些脚本的 parser
            "<template>": "espree",
        }
    }
}
复制代码

是不是感觉有点晕,typescript、babel、vue 等的 parser 都有相应的用于 eslint 的版本。 其实细想一下也很正常,因为 lint 就是基于 AST 的,如果不能 parse,那么怎么 lint,所以需要支持 parser 的扩展,支持切换。

但是 parser 之后的 AST 可能不同,那么 lint 的 rule 的实现也不同。为了复用 rule,大家还是都往 estree 标准上靠比较好。

tslint 和 eslint 的融合也是这样的思路,下面我们来详细看一下。

tslint 融合进 eslint

tslint 是独立的工具,基于 typescript 的 parser 来解析代码,并且实现了基于该 AST 的一系列 rule。

如果要融合进 eslint,那么怎么融合呢?

主要考虑的是 AST 怎么融合,因为 rule 就是基于 AST 的。

比如 const a = 1; 这段代码,

estree 系列的 AST 是这样的:

而 typescript 的 AST 是这样的:

AST 都不同,那么基于 AST 的 rule 肯定也要有不同的实现。

怎么融合呢?

转换!把一种 AST 转成另一种 AST 不就行了。

没错,@typescript-eslint/parser 中确实也是这么做的,它把 ts 的 AST 转换成 estree 的 AST(当然对于类型的部分,estree 中没有,就保留了该 AST,但是加上了 TS 前缀)。这样,就能够用 eslint 的 rule 来检查 typescript 代码中的问题了。

我们来简单看一下 @typescript-eslint/parser 的源码:

我简化了一下,是这样的:

function parseAndGenerateServices(code) { 
    // 用 ts parser 来 parse
    let {ast, program} = createIsolatedProgram(code);
    // 转换成 estree 的 ast
    ast = convertAst(ast);
    return {
      ast,
      services: {
        program,
        esTreeNodeToTSNodeMap,
        tsNodeToESTreeNodeMap
      }
    }
}
复制代码

首先通过 ts 的 parser 把源码 parse 成 AST,然后转换成 estree 的,并且记录了 estree node和 ts node 的映射关系,通过两个 map 来保存。

具体转换的过程,其实就是遍历 ts 的 AST,然后创建新的 estree 的 AST。

其中对于 estree 中没有的类型相关的 AST,则直接复制,并在 AST 名字前加个 TS。

这样,就把 ts parser 产生的 AST 转成了 estree 的。

既然 AST 统一了,那么 eslint 的 rule 就可以用来 lint ts 代码了。

但是对于一些类型的部分,还是需要用 ts 的 api 来检查 ts 的 AST 怎么办呢?

还记得我们保存了两个 map 么?estree node 到 ts node 的 map,还有反过来的 map。 这样,需要用到 ts 的 AST 的时候,再映射回去就行了:

eslint 的自定义 parser 的返回结果中,除了有 ast,还支持返回 services,这是用于放一些其他信息的,比如这里用到的 map,还有 ts 的 program 的 api(比如 program.getTypeChecker 这种)。那么需要的时候就可以从 estree 的 ast 再映射回 ts 的 ast 了。

通过把 ts AST 映射成 estree AST 达到了复用 eslint 的 rule 的目的,并且保存了节点映射关系和一些操作 ts AST 的 api,可以基于这些单独做 ts 相关的 lint。完美的融合到了一起。

可以把这种融合用“求同存异”来总结:

  • 求同:把 AST 都转成 estree 系列的,从而复用一系列针对 estree AST 的 rule。
  • 存异:转换过程中保留映射关系,还有一些 api,这样需要单独对 ts 类型等做检查的时候,还可以映射回去。

总结

js 有不同的 parser,分为 estree 系列和非 estree 系列:

  • estree 系列有 esprima、acorn 以及扩展自 acorn 的 espree、babel parser 等。

  • 非 estree 系列有 typescript、terser 等。

eslint 中支持了 parser 的切换,可以在 babel parser、vue template parser、typescript 和 espree 中切换,当然也可以扩展其他的 parser。

tslint 是基于 typescript 做 parse 的一个独立的工具。它和 eslint 都是基于 AST 检查代码中的逻辑和格式错误的工具,后来做了融合。

为了复用基于 estree 的一些 rule, @typescript-eslint/parser 把 ts node 转成了 estree node,但是依然保留了映射关系和一些操作 ts ast 的 api。

这样基于 estree AST 的 rule 可以正常运行,基于 ts AST 的 rule 也可以映射回原来的 ts node 然后运行。

通过这种方式,完美的把 eslint 和 tslint 融合在了一起。还是挺巧妙的。

文章分类
前端