Eslint是团队合作coding必不可少的利器,ESLint通过各种规则对代码添加约束。通过合适的规则约束,能让我们的代码更健壮,工程更可靠,增加可读性。
当我们遇到想要统一的规范时,一般会找现有的规则,大部分情况是可以解决问题。那么如果目前没有可用的规则,如何处理?依赖人为限制?code review?
我们先简单回顾一下eslint配置,然后了解一下eslint运行、校验规则的原理,最后实现一套自己的代码规则。
eslint 配置
eslint 支持如下文件类型,并且当同一目录下存在多个配置文件时,文件优先级:
1.eslintrc.js
2.eslintrc.cjs
3.eslintrc.yaml
4.eslintrc.yml
5.eslintrc.json
6.package.json eslintConfig 属性
配置文件,如.eslintrc.js
中,常用配置如下:
{
"env":{
"browser":true,
},
"plugins": [
"vue" // 对 eslint-plugin-vue 的缩写
],
"extends": [
"eslint:recommended",
"plugin:vue/recommended" // 对该插件下的某一类配置的引用
],
"rules": {
"vue/require-v-for-key": "error" // 对具体eslint-plugin-vue插件下的某个规则的使用
},
"globals":{
"my":"readonly", // 只读
"wx":"writable" // 可写
},
"parserOptions":{
"ecmaVersion":6,
"sourceType":"module",
"ecmaFeatures":{
"jsx":true
}
},
"parser": "@typescript-eslint/parser",
}
env
: 当前可以使用哪个环境的全局变量,如
-
browser
,浏览器环境使用,可以使用windows下的变量,比如不配置则document会报错(前提是要配置了extends:eslint:recommended
)es6
,es6环境,支持es6语法
plugins
:配置定义在插件中的规则,plugins为由插件名称组成的列表。可以省略插件名称中的eslint-plugin-
前缀,extends
:使用扩展,可以继承另一个配置文件的所有特征(包括规则、插件和语言选项)并修改所有选项。可以是一个eslint配置文件的路径,可以是下载的npm包或者插件的名称,可以是eslint推荐的一些风格例如eslint:recommended
或eslint:all
rules
: 配置具体的规则及规则严重性,no-console:1
-
"off"
/0
: 关闭规则"warn"
/1
: 打开规则作为警告"error"
/2
: 打开规则作为错误
global
: 全局变量,eslint 不允许出现未定义的变量,如果运行在小程序里,存在my、wx等全局变量,直接使用会报错,此时需要向ESlint规则中添加需要辨认的变量parserOptions
, 语言选项配置,默认支持 ECMAScript 5,
-
ecmaVersion
: 指定想要使用的 ECMAScript 版本sourceType
: js模块语法,默认“script”在ECMAScript模块中需要设置为“module”ecmaFeatures
:指定需要哪些附加选项,如需要允许使用jsx,配置"jsx":true
parser
: 指定eslint用何种方式将代码解析成AST,默认为Espree,比较常用的配置为@typescript-eslint/parser
,将TypeScript 转换为与 ESTree 格式。
💡 extends
和plugin
的区别—— extends= plugin+rules
如果只配plugin
,plugin
会加载但是规则不会生效,要配合rules
去配置具体的规则使用;
extends
提供了一个方法可以将插件中推荐or导出的规则集合同时生效,当然也可以配置单独rule去覆盖
eslint 运行原理
ESLint 将代码解析为抽象语法树AST,通过遍历AST,在遍历到不同的节点或者合适的时机的时候,触发相应的规则函数进行校验,如未通过校验则抛出错误,如需修复则尝试进行修复。
运行原理
1、处理命令行命令&初始化实例
ESLint 首先读取命令行的配置,进行处理,包含--init
则进行eslint初始化,无则进入校验逻辑,并且生成一个ESLint
实例,其中又会生成一个CLIEngine
实例和一个Linter
实例。在ESLint
实例生成后,根据参数做不同的处理,但是主要是遍历文件并进行校验。
这三个为ESLint的核心类,包含了如下功能,
2、读取eslint配置
校验文件时需要获取eslint
的配置,首先按照配置文件的优先级读取配置,再递归处理 extends
,最后返回自己的配置,所以最终得到的配置list中里的顺序则是:配置中的 extends > 配置。
读取流程
3、使用parser解析text获得AST
获取到配置后接着对每个文件进行校验,校验过程会循环进行校验->修复,当满足以下两者之一时则结束循环:
- 没有更多错误需要处理
- 满足最大校验次数
在校验中,如没有指定的parser,eslint 则默认使用 JavaScript 解析器 Espree
把JS代码解析成AST。
4、运行具体规则并实现校验
- 获取到AST之后,ESLint会以从上至下再从下至上的顺序遍历AST,将节点加到节点队列中。
- 再遍历所有配置的规则,将规则返回的选择器添加为监听器。此时会获取规则配置的严重性,
0 | 1 | 2
,如果是关闭状态就转到下一条规则。 - 再次遍历节点队列,判断节点是否触发监听器,触发则执行回调校验。
- 最后返回校验的结果,一个问题列表,包括错误的列、错误提示信息等。
5、修复
如果需要修复,则遍历需要修复的问题,并且调用规则的修复方法
6、过滤并输出结果
根据代码里的 eslint
注释配置来过滤问题列表并输出
选择器
选择器 (Identifier)是可以用来匹配抽象语法树(AST)中节点的字符串,类似css的选择语法。
选择器并不局限于对单一节点类型的匹配。例如,选择器 VariableDeclarator > Identifier ,将匹配所有以 VariableDeclarator 为直接父节点的 Identifier 节点。
支持的选择器有:
- AST节点类型
- 通配符:*(匹配所有节点)
- 第一个或最后一个子项::first-child 或 :last-child
- 第 n 个子项(不支持 ax+b)::nth-child(2)
- 最后 n 个子项(不支持 ax+b)::nth-last-child(1)
- AST 节点类::statement、:expression、:declaration、:function 或 :pattern
- 后裔:FunctionExpression ReturnStatement
- 选择其子项:UnaryExpression > Literal
- 随同兄弟:VariableDeclaration ~ VariableDeclaration
- 相邻兄弟:ArrayExpression > Literal + SpreadElement
- ...更多可参见:官网
例:
// 选中所有 有块的 IfStatement 节点。
"IfStatement > BlockStatement": function(blockStatementNode) {
//
},
// 选中所有 有 3个 以上参数的函数声明。
"FunctionDeclaration[params.length>3]": function(functionDeclarationNode) {
//
}
实现一个eslint插件
根据上述运行原理,我们了解规则主要是在第4步运行具体规则并实现校验中生效,下面实现一个eslint插件,以如下规则为例:
禁止项目中使用console.time()
。
1、脚手架安装
ESLint官方为了方便开发者开发插件,提供了一个脚手架工具,Yeoman模板(generator-eslint
),用于生成包含指定框架结构的工程化目录结构。以下指令进行安装,
npm install -g yo generator-eslint
2、命令行初始化ESLint插件
生成ESLint插件的项目模板
yo eslint:plugin
创建时的交互如下,processors是用于处理js以外的语言时添加,
$ yo eslint:plugin
? What is your name? yn
? What is the plugin ID? eslint-demo
? Type a short description of this plugin: demo
? Does this plugin contain custom ESLint rules? Yes
? Does this plugin contain one or more processors? No
create package.json
create eslint.config.mjs
create lib/index.js
create README.md
初始化生成文件及结构:
3、创建规则文件
生成ESLint插件具体规则的文件
yo eslint:rule
交互
$ yo eslint:rule
? What is your name? yn
? Where will this rule be published? ESLint Plugin
? What is the rule ID? yn-demo-rule // 规则ID
? Type a short description of this rule: settimeout第二个参数禁止为数字
? Type a short example of the code that will fail: empty
create docs/rules/yn-demo-rule.md
create lib/rules/yn-demo-rule.js
create tests/lib/rules/yn-demo-rule.js
文件及结构:
可以看到刚刚空的文件夹下多了以rule ID
命名的文件。lib/rules/*.js
为具体的规则,tests/lib/rules/*.js
为对应的测试文件。
4、编写具体规则
进入到刚刚生成的文件 lib/rules/no-console-time.js
:
/**
* @fileoverview 禁止console.time()
* @author yn
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: null, // `problem`, `suggestion`, or `layout`
docs: {
description: "禁止console.time()",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: 'code', // Or `code` or `whitespace`
schema: [], // Add a schema if the rule has options
messages: {
avoidMethod:"console method '{{name}}' is forbidden."
}, // Add messageId and message
},
create(context) {
return {
// visitor functions for different types of nodes
};
},
};
上述初始化为编写规则的模板,一个规则对应一个可导出的node模块,其主要由 meta
和 create
两部分组成,其中,
meta
代表了这条规则的元数据,如其类别,文档,可接收的参数的schema
等等,
-
type
表示规则的类型,是 "problem"、"suggestion" 或 "layout" 其中之一,docs
核心规则必须存在此字段,而自定义规则则可自行选择。
-
-
description
:在规则页面中提供规则的简短描述。recommended
:表示在配置文件 中是否使用"extends": "eslint:recommended"
属性启用该规则。url
:指定可以访问完整文档的链接(使代码编辑器能够在突出显示的规则违反上提供一个有用的链接)
-
-
fixable
"code" 或 "whitespace",在需要修复时是强制指定的。如果命令行存在--fix
选项,fixable
却没有指定则会抛错。schema
:用于验证规则的配置选项messages
用于指定在report
返回错误时,代替打印信息
meta:{
messages: {
avoidMethod: "console method '{{name}}' is forbidden.",
},
}
create(context){
return {
// ...
context.report({
context.report({
/// ...
node,
messageId: 'avoidMethod',
data: {
name: 'time',
},
fix(fixer){
//...
return //
}
});
})
}
}
详细的描述可见官方文档《Custom Rules》。
create
表达这条rule
具体会怎么分析代码。Create
返回的是一个对象,其中最常见的键的名字可以是上面我们提到的AST选择器,在该选择器中,我们可以获取对应选中的内容,随后我们可以针对选中的内容作一定的判断,看是否满足我们的规则,如果不满足,可用context.report()
抛出问题,ESLint 会利用我们的配置对抛出的内容做不同的展示。
-
create
方法的入参,这个context
对象包含了我们这条自定义规则的上下文相关的信息,如下,
-
-
id
:规则idfileName
:与源关联的文件路径parserOptions
:此配置运行的解析器,为.eslintrc.js
中parserOptions
配置options
:通过这个可以拿到规则传进来的参数getScope()
: 返回当前遍历节点的作用域sourceCode
: 返回SourceCode
对象,就是源码对象
-
sourceCode
是获取有关正在检查的源代码的主要对象,通过 sourceCode.ast
可以获得当前校验代码的ast
-
report()
: 当校验不通过的时候,通过这个方法输出错误信息
-
-
message
:问题提示信息,等效于meta 中的 messageIdnode
:与问题哟管的AST节点fix(fixer)
:修复函数,在指定需要fix时执行,同时需要指定meta.fixable
为true
,fixer
为传入的对象,存在一些方法,如在给定的节点(范围)或标记后/前插入文本、删除/移除置顶的节点等。data
:提供给message中的占位符
-
根据上述的eslint运行原理,我们知道是需要通过选择AST的节点并且进行校验的,于是我们观察一下 console.time()
方法转化成的AST:
通过 astexplorer.net 链接可以查看代码被解析成AST的样子
利用其中以下内容判断代码是否含有 console.time
于是编写 create
规则如下:
create(context) {
return {
'CallExpression MemberExpress':(node)=>{
if(node.property.name==='time'&&node.object.name==='console'){
// 不满足规则
context.report({
node,
messageId:'avoidMethod',
data:{
name:'time'
}
})
}
}
};
},
5、输出规则
在 lib/index
下输出规则,如
// import all rules in lib/rules
module.exports = {
// 元数据
meta:{
name:pkg.name,
version:pkg.version
},
// 规则
rules:{
'no-console-time':require('./rules/no-console-time')
},
processor:{
},
// 插件中的配置
configs:{
myConfig:{
plugins:['eslint-yn-demo'],
rules:{
'eslint-yn-demo/no-console-time':2,
semi:'error',
},
}
},
}
其中,
meta
元数据,为了更容易进行调试和有效的缓存插件,官方建议配置meta对象,meta一般和package中保持一致rules
对象必须被输出,包含一个规则ID到规则实现的映射,规则命名不需要遵循惯例。使用这个配置时,按照插件名/规则Id
引用,如{rules:eslint-yn-demo/no-console-time}
processor
:用于处理JS以外的文件,对应第3步在verify校验时,如果需要处理js以外的文件会执行config
为插件中的配置 ,可以集合多条规则,其中plugins
为插件名称,rules
则是配置的规则如果要使用 myConfig下的规则,则在 eslint 配置中配置如下,
// .exlintrc.js
{
extends:[
"plugin:eslint-yn-demo/myConfig"
],
}
6、测试规则
到tests/lib/rules/no-console-time.js
下编写单元测试,
'use strict';
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
let rule = require('../../../lib/rules/no-console-time');
let RuleTester = require('eslint').RuleTester;
// ------------------------------------------------------------------------------
// Tests
// ------------------------------------------------------------------------------
let ruleTester = new RuleTester();
ruleTester.run('no-console-time', rule, {
// 合法示例
valid: [
'_.time({a:1});',
"_.time('abc');",
"_.time(['a', 'b', 'c']);",
"lodash.time('abc');",
'lodash.time({a:1});',
'abc.time',
"lodash.time(['a', 'b', 'c']);",
],
// 不合法示例
invalid: [
{
code: 'console.time()',
errors: [{ messageId: 'avoidMethod',}],
},
{
code: "console.time.call({}, 'hello')",
errors: [
{
messageId: 'avoidMethod',
},
],
},
{
code: "console.time.apply({}, ['hello'])",
errors: [
{
messageId: 'avoidMethod',
},
],
},
{
code: 'console.time.call(new Int32Array([1, 2, 3, 4, 5]));',
errors: 1,
},
],
});
运行 npm run test
,输出如下,
至此,一个简单的eslint插件就开发完成了,接下来是使用的部分
7、安装到项目
在安装前还会有一个发布的操作,本次demo就不发布了,本地link测试看看,初始化一个简单的项目,引入eslint并新增配置.eslintrc.js
配置,
module.exports = {
plugins:[
'eslint-yn-demo'
],
rules:{
"eslint-yn-demo/no-console-time":'error'
}
}
新增一个测试文件和代码console.time()
,运行 npm run lint
至此,一个简单的插件就开发和测试完成了
总结
本次通过实现和运用一个简单的eslint插件,回顾了eslint的配置、梳理了一下eslint运行流程。
参考:ESLint自定义规则:zh-hans.eslint.org/docs/latest…