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。

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

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

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

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

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

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

TSLint 和 ESLint 是怎么融合在一起的_前端_02

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 是这样的:

TSLint 和 ESLint 是怎么融合在一起的_复用_03

而 typescript 的 AST 是这样的:

TSLint 和 ESLint 是怎么融合在一起的_前端_04

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。

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

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

TSLint 和 ESLint 是怎么融合在一起的_复用_06

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

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

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

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

TSLint 和 ESLint 是怎么融合在一起的_前端_07

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 融合在了一起。还是挺巧妙的。