ESLint rules/extends/plugins
rules
官网所例举的核心规则前面有些有 ☑️ ,有些有 🔧,有些同时有 ☑️ 🔧,有些什么都没有。
☑️:表示 extends: 'eslint:recommended'
中包含的规则。(2023/7/2 更新:官网链接中 ✅ 且高亮的才是 recommended 中包含的规则,置灰的不是)
🔧:表示能通过 --fix
修正的规则。(2023/7/2 更新:官网链接中 🔧 且高亮的才能被 --fix 修复,置灰的不能被 --fix 修复)
"no-console": 0,
"no-debugger": 1,
"no-trailing-spaces": 2,
"quotes": [2, "single"], // 表示字符应使用单引号,如果违反规则,将报 error 级别的错误。
off
或 0:关闭规则warn
或 1:开启规则,warn
级别的错误 (不会导致程序退出)error
或 2:开启规则,error
级别的错误(当被触发的时候,程序会退出)
extends
默认所有规则都不生效,可通过 extends 指定应用哪些规则。例如 extends: 'eslint:recommended'
表示应用 eslint rules 中打勾的那些规则。
它可以配置为:
- String:一个配置文件的路径 或 可共享配置的名称(
eslint:recommended
或eslint:all
)。 - Array[String]:多个配置组合,后面的配置继承并覆盖前面的配置。
可共享配置(shareable configuration):它是一个配置对象的 npm 包,使用时确保已经安装到 ESLint 能够引用的目录中,在 extends 中使用时可以省略 eslint-config-
。例如:你需要校验 React 风格的代码,那么需要 extends: 'eslint-config-react'
,也可以写成 extends: 'react'
。
// .eslintrc.js
module.exports = {
extends: [ // Array[String]
'eslint:recommended', // 可共享配置的名称
'./path-to-config', // 配置文件的路径
'eslint-config-react', // 全称
'react', // 缩写
],
extends: 'eslint:recommended' // String
};
plugins
虽然官方提供了上百种规则可供选择,但是这还不够,因为官方的规则只能检查标准的 JavaScript
语法,如果你写的是 JSX
或者 TypeScript
,ESLint 的规则就开始束手无策了。
这个时候就需要安装 ESLint 插件,来定制一些特定规则进行检查。ESLint 插件与 extends
一样有固定的命名格式,以 eslint-plugin-
开头,使用的时候也可以省略这个头。
举个例子,我们要在项目中使用 TypeScript
,需要将解析器改为 @typescript-eslint/parser
,同时需要安装@typescript-eslint/eslint-plugin
插件来拓展规则,添加的 plugins
中的规则默认是不开启的,我们需要在 rules
中开启要使用的规则。也就是说 plugins
是要和 rules
结合使用的。如下所示:
// npm i --save-dev @typescript-eslint/eslint-plugin // 注册插件
{
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"], // 引入插件
"rules": {
"@typescript-eslint/rule-name": "error" // 使用插件规则
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/ban-ts-comment': 'error',
...
}
}
在 rules
中写一大堆的配置来启用 @typescript-eslint/eslint-plugin
插件规则,十分麻烦,这时候 extends
派上了用场。
{
extends: 'plugin:@typescript-eslint/recommended'
}
新建一个 ESLint 插件
插件目标:禁止项目中 setTimeout
的第二个参数是数字。例如:setTimeout(() => {}, 2)
是违背规则,const num = 2;setTimeout(() => {}, num)
是 ok 的。
项目初始化
-
全局安装 eslint plugin 脚手架工具,ESLint 官方为了方便开发者开发插件,提供了使用 Yeoman 模板 (
generator-eslint
) 。npm install -g yo generator-eslint
-
初始化项目目录
mkdir eslint-plugin-irenePugin cd eslint-plugin-irenePugin yo eslint:plugin
下面进入命令行交互流程,结束后会生成自定义插件的项目目录
? What is your name? irene ? What is the plugin ID? irenelint // 插件ID ? Type a short description of this plugin: for testing creating a eslint plugin // 插件描述 ? 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
-
创建规则
yo eslint:rule
下面进入命令行交互流程,结束后会生成一个规则文件的模板
? What is your name? irene ? Where will this rule be published? ESLint Plugin // 规则将在哪里发布 ❯ ESLint Core // 官方核心规则 ESLint Plugin // ESLint 插件 ? What is the rule ID? settimeout-no-number // 规则ID ? Type a short description of this rule: the second param of setTimeout is forbidden to use number // 规则描述 ? Type a short example of the code that will fail: setTimeout(() => {}, 2) // 失败例子的代码 create docs/rules/settimeout-no-number.md create lib/rules/settimeout-no-number.js create tests/lib/rules/settimeout-no-number.js
-
生成的项目目录如下
├── README.md ├── docs // 使用文档 │ └── rules // 所有规则的文档 │ └── settimeout-no-number.md // 具体规则文档 ├── lib // eslint 规则开发 │ ├── index.js 引入 + 导出 rules 文件夹的规则 │ └── rules // 此目录下可以构建多个规则 │ └── settimeout-no-number.js // 规则细节 ├── package.json └── tests // 单元测试 └── lib └── rules └── settimeout-no-number.js // 测试该规则的文件
-
安装项目依赖
npm install
规则模版
打开 lib/rules/settimeout-no-number.js,可以看到通过上述命令行操作后生成的模版。
module.exports = {
meta: {
docs: {
description: "the second param of setTimeout is forbidden to use number",
category: "Fill me in",
recommended: false
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
]
},
create: function(context) {
return {
// give me methods
};
}
};
create 方法返回的是一个 key 为选择器,value 为回调函数(参数是 AST node)的对象,例如:{ 'CallExpression': (node) => {} }
,ESLint 会收集所有生效规则监听的选择器以及对应的回调函数,在遍历 AST 时,每当匹配到选择器,就会触发该选择器对应的回调。
AST:Abstract Syntax Tree
ESLint 是通过将代码解析成 AST 并遍历它实现代码校验和格式化的,具体将在下面讨论。现在我们先看下 setTimeout(() => {}, 2)
解析成的 AST 是什么样子。在线AST
编写规则
通过观察生成的 AST,过滤出我们要选中的代码,对代码的值进行判断。
// lib/rules/settimeout-no-number.js
module.exports = {
meta: {
docs: {
description: "the second param of setTimeout is forbidden to use number",
category: "Fill me in",
recommended: false
},
fixable: null, // or "code" or "whitespace"
schema: [
// fill in your schema
]
},
create: function(context) {
return {
// give me methods
'CallExpression': (node) => {
if (node.callee.name !== 'setTimeout') return // 不是 setTimeout 直接过滤
const timeNode = node.arguments && node.arguments[1] // 获取第二个参数
if (!timeNode) return
if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {
context.report({
node,
message: 'setTimeout 第二个参数禁止是数字'
})
}
}
};
}
};
测试用例
提供一些违背和通过规则的测试代码
// tests/lib/rules/settimeout-no-number.js
var rule = require("../../../lib/rules/settimeout-no-number"), RuleTester = require("eslint").RuleTester;
var ruleTester = new RuleTester();
ruleTester.run("settimeout-no-number", rule, {
valid: [
{
code: "let num = 1000; setTimeout(() => { console.log(2) }, num)",
},
],
invalid: [
{
code: "setTimeout(() => {}, 2)",
errors: [
{
message: "setTimeout 第二个参数禁止是数字", // 与 rule 抛出的错误保持一致
type: "CallExpression", // rule 监听的对应钩子
},
],
},
],
});
自动修复
- fixable: 'code' 开始修复功能;
- context.report() 提供一个 fix 函数;
// lib/rules/settimeout-no-number.js
module.exports = {
meta: {
docs: {
description: "the second param of setTimeout is forbidden to use number",
category: "Fill me in",
recommended: false
},
fixable: 'code',
},
create: function(context) {
return {
// give me methods
'CallExpression': (node) => {
if (node.callee.name !== 'setTimeout') return // 不是 setTimeout 直接过滤
const timeNode = node.arguments && node.arguments[1] // 获取第二个参数
if (!timeNode) return
if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') {
context.report({
node,
message: 'setTimeout 第二个参数禁止是数字',
fix(fixer) {
const numberValue = timeNode.vlaue;
const statementString = `const num = ${numberValue}\n`;
return [
fixer.replaceTextRange(node.arguments[1].range, 'num'),
fixer.insertTextBeforeRange(node.range, statementString)
]
}
})
}
}
};
}
};
调试
点击 debug,然后选中项目。
点击设置,会打开一个 launch.json
,program 字段填上要 debug 的文件。
在 lib/rules/settimeout-no-number.js 打 debugger,点击启动程序。
发布插件
-
登陆 npm:npm login
-
发布 npm 包:npm publish
使用
-
安装插件
npm install --save-dev eslint-plugin-irenelint
-
引入插件并开启规则
-
通过 plugins
// .eslintrc.js module.exports = { plugins: [ 'irenelint' ], rules: { 'irenelint/settimeout-no-number': 'error' } }
-
通过 extends
因为 plugins 中的规则默认是不启用的,需要一条条的在 rules 中开启,当规则比较多的时候,写起来太麻烦,这时就可以使用 extends。
首先,我们需要修改下 lib/index.js
// lib/index.js var requireIndex = require("requireindex"); const output = { rules: requireIndex(__dirname + "/rules"), // 所有规则 configs: { recommended: { plugins: ['irenelint'], // 引入插件 rules: { 'irenelint/settimeout-no-number': 'error' // 开启规则 } } } } module.exports = output;
然后使用 extends
// .eslintrc.js module.exports = { extends: [ 'plugin:irenelint/recommended' ] }
-
测试
修复前:第一条提示就是自动修复的提示
修复后:如果配置了保存时自动修复,就会在保存的时候自动改正。
ESLint 原理
eslint/lib/linter/lint.js
假设待校验的文件内容是
console.log('irene');
依据文件内容生成如下 AST ,将每一个节点传入 nodeQueue 队列中,每个会被传入两次;在线AST
nodeQueue = [
{
isEntering: true,
node: {
type: 'Program',
body: [Array],
sourceType: 'module',
range: [Array],
loc: [Object],
tokens: [Array],
comments: [],
parent: null
}
},
{
isEntering: true,
node: {
type: 'ExpressionStatement',
expression: [Object],
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: true,
node: {
type: 'CallExpression',
callee: [Object],
arguments: [Array],
optional: false,
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: true,
node: {
type: 'MemberExpression',
object: [Object],
property: [Object],
computed: false,
optional: false,
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: true,
node: {
type: 'Identifier',
name: 'console',
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: false,
node: {
type: 'Identifier',
name: 'console',
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: true,
node: {
type: 'Identifier',
name: 'log',
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: false,
node: {
type: 'Identifier',
name: 'log',
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: false,
node: {
type: 'MemberExpression',
object: [Object],
property: [Object],
computed: false,
optional: false,
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: true,
node: {
type: 'Literal',
raw: "'irene'",
value: 'irene',
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: false,
node: {
type: 'Literal',
raw: "'irene'",
value: 'irene',
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: false,
node: {
type: 'CallExpression',
callee: [Object],
arguments: [Array],
optional: false,
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: false,
node: {
type: 'ExpressionStatement',
expression: [Object],
range: [Array],
loc: [Object],
parent: [Object]
}
},
{
isEntering: false,
node: {
type: 'Program',
body: [Array],
sourceType: 'module',
range: [Array],
loc: [Object],
tokens: [Array],
comments: [],
parent: null
}
}
]
遍历所有整合好的规则,如果该条规则不为 0 或 'off'(即规则是开启的),获取该条规则的 rule 对象,执行 create 函数返回监听对象,它表明了这条规则监听了哪些 AST 节点,当遍历这些节点的时候就会执行对应的回调函数;
// 假设整合好的规则如下
configuredRules = {
'@typescript-eslint/no-explicit-any': [ 0 ], // ruleId: [severity]
'@typescript-eslint/explicit-module-boundary-types': [ 0 ],
'prettier/prettier': [ 'error' ],
'@typescript-eslint/no-unused-vars': [ 'warn' ],
...
}
ruleObj = {
meta:
create: (context) => {
return {
'CallExpression:exit': func1,
'Identifier': func2
}
}
}
遍历该条规则的监听对象,为每个 AST 节点注册监听函数
listeners: {
'CallExpression:exit': [func1],
Identifier: [func2]
}
遍历规则结束之后,我们得到了一个 listeners 对象,key 是 AST 节点,value 是回调函数数组;
遍历第一步获取到的 nodeQueue,触发 listeners 中对应的回调函数,比如遍历到 CallExpression 的时候,去执行 listeners.CallExpression 数组里的函数,函数会检测当前节点是否违背规则,如果违背,则报告 warn/error,存于 lintingProblems 中;
返回 lintingProblems