了解ESLint

原由是想写一个 繁简体 的业务检查,进而一步一步的了解了 ESLint 这个工具~ ## ESLint的历史 JavaScript 发展历史中,出现了很多 lint 工具,比较有代表性的是以下三款lint工具。

1. JSLint

最早的lint工具,由 Douglas Crockford开发(也是《JavaScript:语言精粹》的作者)。缺点是该工具的语法规则都是预设的,用户无法改变。也就是说,想要使用这个工具,你必须确保自己能接受它的所有规则。

2. JSHint

JSHint 是由 Anton Kovalyov 基于 JSLint 开发的开源项目,显著的特点就是允许用户自定义自己的语法规则,加持上开源社区的驱动,发展十分迅速。由于是基于 JSLint 开发,原有的一些问题也继承下来了,比如:不易扩展,不易直接根据报错定位到具体的规则配置等。

3. ESLint

ESLint是由 Nicholas C. Zakas (《JavaScript 高级程序设计》作者) 在2013年开始开发的,它的初衷就是为了能让开发者能自定义自己的 lint rules,它提供了一套完善的插件系统,可以自由的扩展,动态的加载配置,同时能方便的根据报错定位到具体的规则配置。ESLint的诞生并不是 Zakas 在重复造一个轮子,而是他在业务开发中需要新增一条规则,但是 JSHint 无法提供支持,于是他就结合 JSHint 和 JSCS (AST的方式进行规则检测) 写出来了 ESLint。

早期的 ESLint 并没有大火,因为需要将源代码转成 AST,运行速度上输给了 JSHint,并且 JSHint 当时已经有了完整的生态(编辑器的支持)。真正让 ESLint 大火是因为 ES6 的出现。

ES6 发布后,因为新增了很多语法,JSHint 短期内无法提供支持,而 ESLint 只需要有合适的解析器就能够进行 lint 检查。这时 Babel 为 ESLint 提供了支持,开发了 babel-eslint,让 ESLint 成为最快支持 ES6 语法的 lint 工具。

使用ESLint

ESLint 的用法包含两部分:通过配置文件配置 lint 规则;通过命令行执行 lint 。

配置eslint

通过 eslint --init 做出各种选择是最常见的 eslint 配置方式:

1
2
3
4
5
6
7
$ eslint --init
√ How would you like to use ESLint? · problems
√ What type of modules does your project use? · commonjs
√ Which framework does your project use? · none
√ Does your project use TypeScript? · No / Yes
√ Where does your code run? · browser
√ What format do you want your config file to be in? · JavaScript
上述选择之后,根目录会新生成一个 .eslintrc.js 文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* .eslintrc.js */
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es2021": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
}
};

然后通过 命令行 的交互就能去检查对应的 js 文件了。

1
eslint demo.js

创建 ESLint 自定义规则

1. npm 安装

根据 ESLint 的官方指南,我们需要 yeoman 和 generator-eslint 来构建插件的脚手架代码。

1
npm install -g yo generator-eslint

2. 创建文件夹

1
2
mkdir eslint-plugin-demo
cd eslint-plugin-demo

3. 初始化项目结构

1
yo eslint:plugin

会进入对应的命令行交互流程,流程结束生成 ESLint 插件项目的目录结构

1
2
3
4
5
6
7
8
? What is your name? hddhyq
? What is the plugin ID? demolint # 插件的ID是什么 eslint-plugin-<ID>
? Type a short description of this plugin: demo for create ESLint rule # 描述信息
? Does this plugin contain custom ESLint rules? Yes # 是否包含自定义ESLint规则
? Does this plugin contain one or more processors? No # 这个插件包含一个或多个处理器吗
create package.json
create lib\index.js
create README.md

4. 创建规则

1
yo eslint:rule

创建规则命令行交互:

1
2
3
4
5
6
7
8
9
10
? What is your name? hddhyq
? Where will this rule be published? (Use arrow keys) # 这个规则将在哪里发布
ESLint Core # 官方的核心规则 (目前有200多条规则)
❯ ESLint Plugin # 选择 ESLint 插件
? What is the rule ID? no-wareware
? Type a short description of this rule: 禁止使用~~转换数字
? Type a short example of the code that will fail: var a = ~~100 # 测试代码
create docs\rules\no-wareware.md # 使用文档
create lib\rules\no-wareware.js # 校验文件
create tests\lib\rules\no-wareware.js # 测试文件

ESLint 如何验证规则

Lint 是基于静态代码进行分析,对于 ESLint 来说,核心功能就是对 rule 及其配置,利用 Lint 分析源码。这里就需要介绍到 AST(抽象语法树) ,相信大家对它一定都用一定的了解,像Babel,Webpack,UglifyJS 等都使用到了 AST 。

关于 AST

ESLint 使用 Espree 来解析我们的 JS 语句,来生成抽象语法树。我们可以使用AST explorer ,方便查看一段代码被解析成 AST 后的样子。

AST展示

从上图可以看出,我们鼠标在右侧选中某个值,左侧对应的区域也会高亮展示。就像 CSS Selectors 一样,右侧被选中的项,我们称作 AST selectors 。

通过 AST selectors 我们可以快速的找到 静态代码中的内容 。 ### 单条 rule 如何工作? 这里看一下,我们自定义的规则 "eslint-plugin-demo/no-wareware": "error" 。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
meta: {
docs: {
description: "禁止使用~~转换数字",
},
fixable: null, // or "code" or "whitespace"
schema: []
},

create: function(context) {

return {
UnaryExpression(node) {
if (node.operator === '~' && node.argument.operator === '~') {
context.report({
node,
message: '請使用 + 代替 ~~ 運算符'
})
}
}
}
}
};

可以看到,一条 rule 就是一个 node 模块,主要由 metacreate 两部分组成,

  • meta 代表了这条规则的元数据,比如类别,文档,可接受的参数 schema 等,官方文档对其有详细描述。
  • create 主要表达了这条 rule 具体会怎么分析处理代码。

上面的代码,在运行到 UnaryExpression 会判断它的操作符是否为"",并判断是否下一个操作符是否也为""。如果匹配到,就会抛出错误。

拓展,关于 selector:exit

  • ESLint 的 traverser 用到了递归遍历,是一个由外向内再向外的过程,:exit 相当于添加了一重额外的回调,让我们对静态代码有了更多的控制。
  • ESLint 好像也能帮我们找到永远不会执行的语句,如:no-unreachable。ESLint 会通过 code path analysis 完成这一步骤。

rule 如何组合生效?

我们由多种途径传递 rule。

  1. 配置文件中 rule。
  2. 文件内部注释评论的 rule,这部分 rule,被称作 direcive rule。

ESLint 在获取到了所有需要对单个文件应用的规则之后,接下来就是也该多重遍历的过程。源码位于runRules 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 节选源码
function runRules(sourceCode, configuredRules, ruleMapper, parserOptions, parserName, settings, filename) {
const emitter = createEmitter();
const nodeQueue = [];
let currentNode = sourceCode.ast;

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
});


const lintingProblems = [];

Object.keys(configuredRules).forEach(ruleId => {
const severity = ConfigOps.getRuleSeverity(configuredRules[ruleId]);

if (severity === 0) {
return;
}

const rule = ruleMapper(ruleId);
const messageIds = rule.meta && rule.meta.messages;
let reportTranslator = null;
const ruleContext = Object.freeze(
Object.assign(
Object.create(sharedTraversalContext),
{
id: ruleId,
options: getRuleOptions(configuredRules[ruleId]),
report(...args) {

if (reportTranslator === null) {...}
const problem = reportTranslator(...args);
if (problem.fix && rule.meta && !rule.meta.fixable) {
throw new Error("Fixable rules should export a `meta.fixable` property.");
}
lintingProblems.push(problem);
}
}
)
);

const ruleListeners = createRuleListeners(rule, ruleContext);

// add all the selectors from the rule as listeners
Object.keys(ruleListeners).forEach(selector => {
emitter.on();
});
});

const eventGenerator = new CodePathAnalyzer(new NodeEventGenerator(emitter));

nodeQueue.forEach(traversalInfo => {
currentNode = traversalInfo.node;
if (traversalInfo.isEntering) {
eventGenerator.enterNode(currentNode);
} else {
eventGenerator.leaveNode(currentNode);
}
});

return lintingProblems;
}
  1. 遍历依据源码生成的 AST ,将每一个 node 传入 nodeQueue 队列中,每个会被传入两次。
  2. 遍历所有将被应用的规则,为规则中所有的选择器添加监听事件,在触发时执行,将问题 push 到 lintingProblems 中。
  3. 遍历第一步获取到的 nodeQueue,触发其中包含的事件。
  4. 返回 lintingProblems。

这里用到了 node 的事件驱动机制,遍历的同时触发监听事件。

Plugin

plugin 大致可以两重概念:

  1. 在 ESLint 配置项的字段,如 plugins: ["demo"],调用我们上面的自定义 plugin 。
  2. 社区封装的 ESLint plugin,像 eslint-plugin-vue 这样 npm 模块。

plugin 可以看作第三方规则的集合,ESLint 本身只会去支持标准的 ECMAScript 语法,如果我们需要在 Vue 中使用ESLint ,我们需要自己去定义一些规则,这样就有了 eslint-plugin-vue

plugin的配置规则和ESLint配置文件很相似,关于如何去配置 plugin,我们可以通过 working-with-plugins 去查看。

这里我们特别需要关注的是 plugin 的两种用法。

  • extends 中使用,plugin有自己的命名空间,可通过 "extends": ["plugin: myPlugin/myConfig"] 引入plugin 的某类规则,具体规则是由plugin中配置的。
  • plugin 中使用,如添加配置 plugin: ["vue"],需要在 rule 中声明需要配置的 eslint-plugin-vue 提供的规则。

自定义Parser

plugin 的应用已经大大的增加了 ESLint 的使用规则,但是如果我们需要对一些 JS 的方言添加 Lint 在怎么办呢?

只需要满足 ESLint 的规定,便能使 ESLint 支持我们的自定义 parser

在社区中,我们常见的 parser 有

自定义的 parser 使用方法如下

1
2
3
{
"parser": "./path/to/awesome-custom-parser.js"
}

业务中的使用场景

除了上面的类似检查 ~~ 的单纯检查编码的场景,还可以将业务中总结的业务规范通过 自定义 ESLint 的方式提示开发者,这对于团队开发,代码维护,确保业务的安全上线都有很大的帮助。

更多的应用场景:

  • 代码中不能使用 OSS 地址的静态资源路径,应该使用 CDN 地址的资源路径。
  • 代码中,工具类 utility 通过统一的路径引入。
  • 保证 API 的使用规范。
  • ...

参考

拓展

评论

加载中,最新评论有1分钟延迟...