这篇文章目的是介绍如何建立一个ESLint插件和建立一个ESLint
rule
,用以帮助咱们更深刻的理解ESLint的运行原理,而且在有必要时能够根据需求建立出一个完美知足本身需求的Lint规则。前端
禁止项目中setTimeout
的第二个参数是数字。vue
PS: 若是是数字的话,很容易就成为魔鬼数字,没有人知道为何是这个数字, 这个数字有什么含义。node
ESLint官方为了方便开发者开发插件,提供了使用Yeoman模板(generator-eslint
)。react
对于Yeoman咱们只需知道它是一个脚手架工具,用于生成包含指定框架结构的工程化目录结构。git
npm install -g yo generator-eslint
mkdir eslint-plugin-demo cd eslint-plugin-demo
yo eslint:plugin
下面进入命令行交互流程,流程结束后生成ESLint插件项目框架和文件。github
? What is your name? OBKoro1 ? What is the plugin ID? korolint // 这个插件的ID是什么 ? Type a short description of this plugin: XX公司的定制ESLint rule // 输入这个插件的描述 ? Does this plugin contain custom ESLint rules? Yes // 这个插件包含自定义ESLint规则吗? ? Does this plugin contain one or more processors? No // 这个插件包含一个或多个处理器吗 // 处理器用于处理js之外的文件 好比.vue文件 create package.json create lib/index.js create README.md
如今能够看到在文件夹内生成了一些文件夹和文件,但咱们还须要建立规则具体细节的文件。web
上一个命令行生成的是ESLint插件的项目模板,这个命令行是生成ESLint插件具体规则的文件。
yo eslint:rule // 生成 eslint rule的模板文件
建立规则命令行交互:npm
? What is your name? OBKoro1 ? Where will this rule be published? (Use arrow keys) // 这个规则将在哪里发布? ❯ ESLint Core // 官方核心规则 (目前有200多个规则) ESLint Plugin // 选择ESLint插件 ? What is the rule ID? settimeout-no-number // 规则的ID ? Type a short description of this rule: setTimeout 第二个参数禁止是数字 // 输入该规则的描述 ? Type a short example of the code that will fail: 占位 // 输入一个失败例子的代码 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
以上是开发ESLint插件具体规则的准备工做,下面先来看看AST和ESLint原理的相关知识,为咱们开发ESLint rule
打一下基础。json
AST是: Abstract Syntax Tree
的简称,中文叫作:抽象语法树。数据结构
将代码抽象成树状数据结构,方便后续分析检测代码。
astexplorer.net是一个工具网站:它能查看代码被解析成AST的样子。
以下图:在右侧选中一个值时,左侧对应区域也变成高亮区域,这样能够在AST中很方便的选中对应的代码。
下图中被圈起来的部分,称为AST selectors(选择器)。
AST 选择器的做用:使用代码经过选择器来选中特定的代码片断,而后再对代码进行静态分析。
AST 选择器不少,ESLint官方专门有一个仓库列出了全部类型的选择器: estree
下文中开发ESLint rule
就须要用到选择器,等下用到了就懂了,如今知道一下就行了。
在开发规则以前,咱们须要ESLint是怎么运行的,了解插件为何须要这么写。
ESLint使用JavaScript解析器Espree把JS代码解析成AST。
PS:解析器:是将代码解析成AST的工具,ES六、react、vue都开发了对应的解析器因此ESLint能检测它们的,ESLint也是所以一统前端Lint工具的。
在拿到AST以后,ESLint会以"从上至下"再"从下至上"的顺序遍历每一个选择器两次。
rule
回调在深度遍历的过程当中,生效的每条规则都会对其中的某一个或多个选择器进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。
打开rule
生成的模板文件lib/rules/settimeout-no-number.js
, 清理一下文件,删掉没必要要的选项:
module.exports = { meta: { docs: { description: "setTimeout 第二个参数禁止是数字", }, fixable: null, // 修复函数 }, // rule 核心 create: function(context) { // 公共变量和函数应该在此定义 return { // 返回事件钩子 }; } };
删掉的配置项,有些是ESLint官方核心规则才是用到的配置项,有些是暂时没必要了解的配置,须要用到的时候,能够自行查阅ESLint 文档
上文ESLint原理第三部中提到的:在深度遍历的过程当中,生效的每条规则都会对其中的某一个或多个选择器进行监听,每当匹配到选择器,监听该选择器的rule,都会触发对应的回调。
create
返回一个对象,对象的属性设为选择器,ESLint会收集这些选择器,在AST遍历过程当中会执行全部监听该选择器的回调。
// rule 核心 create: function(context) { // 公共变量和函数应该在此定义 return { // 返回事件钩子 Identifier: (node) => { // node是选中的内容,是咱们监听的部分, 它的值参考AST } }; }
建立一个ESLint rule
须要观察代码解析成AST,选中你要检测的代码,而后进行一些判断。
如下代码都是经过astexplorer.net在线解析的。
setTimeout(()=>{ console.log('settimeout') }, 1000)
lib/rules/settimeout-no-number.js
:
module.exports = { meta: { docs: { description: "setTimeout 第二个参数禁止是数字", }, fixable: null, // 修复函数 }, // rule 核心 create: function (context) { // 公共变量和函数应该在此定义 return { // 返回事件钩子 'CallExpression': (node) => { if (node.callee.name !== 'setTimeout') return // 不是定时器即过滤 const timeNode = node.arguments && node.arguments[1] // 获取第二个参数 if (!timeNode) return // 没有第二个参数 // 检测报错第二个参数是数字 报错 if (timeNode.type === 'Literal' && typeof timeNode.value === 'number') { context.report({ node, message: 'setTimeout第二个参数禁止是数字' }) } } }; } };
context.report():这个方法是用来通知ESLint这段代码是警告或错误的,用法如上。在这里查看context
和context.report()
的文档。
规则写完了,原理就是依据AST
解析的结果,作针对性的检测,过滤出咱们要选中的代码,而后对代码的值进行逻辑判断。
可能如今会有点懵逼,可是没关系,咱们来写一下测试用例,而后用debugger
来看一下代码是怎么运行的。
测试文件tests/lib/rules/settimeout-no-number.js
:
/** * @fileoverview setTimeout 第二个参数禁止是数字 * @author OBKoro1 */ "use strict"; var rule = require("../../../lib/rules/settimeout-no-number"), // 引入rule RuleTester = require("eslint").RuleTester; var ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 7, // 默认支持语法为es5 }, }); // 运行测试用例 ruleTester.run("settimeout-no-number", rule, { // 正确的测试用例 valid: [ { code: 'let someNumber = 1000; setTimeout(()=>{ console.log(11) },someNumber)' }, { code: 'setTimeout(()=>{ console.log(11) },someNumber)' } ], // 错误的测试用例 invalid: [ { code: 'setTimeout(()=>{ console.log(11) },1000)', errors: [{ message: "setTimeout第二个参数禁止是数字", // 与rule抛出的错误保持一致 type: "CallExpression" // rule监听的对应钩子 }] } ] });
下面来学习一下怎么在VSCode中调试node文件,用于观察rule
是怎么运行的。
实际上打console
的形式,也是能够的,可是在调试的时候打console实在是有点慢,对于node这种节点来讲,信息也不全,因此我仍是比较推荐经过debugger
的方式来调试rule
。
launch.json
rule
文件中打debugger
或者在代码行数那里点一下小红点。debugger
{ // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "node", "request": "launch", "name": "启动程序", // 调试界面的名称 // 运行项目下的这个文件: "program": "${workspaceFolder}/tests/lib/rules/settimeout-no-number.js", "args": [] // node 文件的参数 }, // 下面是用于调试package.json的命令 以前能够用,貌似vscode出了点bug致使如今用不了了 { "name": "Launch via NPM", "type": "node", "request": "launch", "runtimeExecutable": "npm", "runtimeArgs": [ "run-script", "dev" //这里的dev就对应package.json中的scripts中的dev ], "port": 9229 //这个端口是调试的端口,不是项目启动的端口 }, ] }
lib/rules/settimeout-no-number.js
中打一些debugger
tests/lib/rules/settimeout-no-number.js
rule
。eslint插件都是以npm
包的形式来引用的,因此须要把插件发布一下:
npm login
npm
包: npm publish
便可,ESLint已经把package.json
弄好了。安装npm
包:npm i eslint-plugin-korolint -D
引入插件一条条写入规则
// .eslintrc.js module.exports = { plugins: [ 'korolint' ], rules: { "korolint/settimeout-no-number": "error" } }
extends
继承插件配置:当规则比较多的时候,用户一条条去写,未免也太麻烦了,因此ESLint能够继承插件的配置:
修改一下lib/rules/index.js
文件:
'use strict'; var requireIndex = require('requireindex'); const output = { rules: requireIndex(__dirname + '/rules'), // 导出全部规则 configs: { // 导出自定义规则 在项目中直接引用 koroRule: { plugins: ['korolint'], // 引入插件 rules: { // 开启规则 'korolint/settimeout-no-number': 'error' } } } }; module.exports = output;
使用方法:
使用extends
来继承插件的配置,extends
不止这种继承方式,即便你传入一个npm包,一个文件的相对路径地址,eslint也能继承其中的配置。
// .eslintrc.js module.exports = { extends: [ 'plugin:korolint/koroRule' ] // 继承插件导出的配置 }
PS : 这种使用方式, npm的包名不能为eslint-plugin-xx-xx
,只能为eslint-plugin-xx
不然会有报错,被这个问题搞得头疼o(╥﹏╥)o
以上内容足够开发一个插件,这里是一些扩展知识点。
上文中说过: 在拿到AST以后,ESLint会以"从上至下"再"从下至上"的顺序遍历每一个选择器两次。
咱们所监听的选择器默认会在"从上至下"的过程当中触发,若是须要在"从下至上"的过程当中执行则须要添加:exit
,在上文中CallExpression
就变为CallExpression:exit
。
注意:一段代码解析后可能包含屡次同一个选择器,选择器的钩子也会屡次触发。
修复效果:
// 修复前 setTimeout(() => { }, 1000) // 修复后 变量名故意写错 为了让用户去修改它 const countNumber1 = 1000 setTimeout(() => { }, countNumber2)
// rule文件 module.exports = { meta: { docs: { description: 'setTimeout 第二个参数禁止是数字' }, fixable: 'code' // 打开修复功能 } }
context.report()
上提供一个fix
函数:把上文的context.report
修改一下,增长一个fix
方法便可,更详细的介绍能够看一下文档。
context.report({ node, message: 'setTimeout第二个参数禁止是数字', fix(fixer) { const numberValue = timeNode.value; const statementString = `const countNumber = ${numberValue}\n` return [ // 修改数字为变量 fixer.replaceTextRange(node.arguments[1].range, 'countNumber'), // 在setTimeout以前增长一行声明变量的代码 用户自行修改变量名 fixer.insertTextBeforeRange(node.range, statementString), ]; } });
呼~ 这篇博客断断续续,写了好几周,终于完成了!
你们有看到这篇博客的话,建议跟着博客的一块儿动手写一下,动手实操一下比你mark一百篇文章都来的有用,花不了很长时间的,但愿各位看完本文,都可以更深刻的了解到ESLint的运行原理。
前端进阶积累、公众号、GitHub、wx:OBkoro一、邮箱:obkoro1@foxmail.com
ESLint插件是向基友yeyan1996学习的,在遇到问题的时候,也是他指点个人,特此感谢。
参考资料: