新建一个 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 原理
假设待校验的文件内容是
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
参考
blog.csdn.net/obkoro1/art…
其他
- ESLint 之解析包名
- ESLint 之与 Prettier 配合使用
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!