叨叨eslint如何自定义规则
一、前言
- ESLint 是一个用于识别和报告
ECMAScript/JavaScript
代码中发现的模式的工具,其目标是使代码更加一致并避免bug
- 随着项目,团队的不断发展,可能会遇到已有规则不能满足现在团队开发的情况。这时候,则可以选择创建一个 ESLint插件然后自定义规则进行规范
- 带着目的完成这件事,那么我们需要从四步入手
- 了解eslint的过程
- 了解AST树
- 了解如何开发一个eslint规则
- 了解如何测试该规则
二、eslint的过程
我们只需要了解比较关键的两步
parse
- ESLint的规则是基于AST树进行检查的,那么肯定有一步是从 源码-> AST树,这个过程就称为
parse
- 在这个过程是可以切换
parser
的, Eslint默认使用的是Espree, 也可以通过配置切换成其他的parser
, 比如说常用的@typescript/eslint-parser
使用规则
parse
之后,会调用runRules
这个方法对 AST 进行遍历检查(从上至下和从下至上各来一次)runRules
会遍历 AST。规则定义了要处理什么,AST 就会监听什么事件,然后处触发listener
三、了解AST树
总结了一下自己见过的用过的, 我觉得对AST的基础归类有一定的了解比直接看AST树astexplorer然后进行开发会更容易上手,也能够较全面的去分析各种状况
整体
- 包裹在最外面的总是program节点,我们可以通过astexplorer来康康其中出现的属性,点击如图位置的按钮即可调整显示的属性
- start, end: 记录了开始和结束的位置
- range: 以数组的方式记录了正文的位置,不包括前后注释
- body: 存放了该文件下的其他节点
- sourceType: 可以有两个选择,或是module或是script
- comments: 该文件下所有注释都存放于此,分为Line和Block
字面量Literal
- 平时我们的数字,字符串,布尔值,正则,null都是属于字面量
注意: undefined并不是
Literal
, 而是Idtentifier
标识符Identifier
- 这个也是经常出现的,我是喜欢把它当作一个变量来看待,可以是对象名,可以是key,可以是值,可以是函数名,也可以是函数参数
- 这里感觉难以罗举,所以列举了三种比较常见的区分
Identifier
和Literal
声明Declaration
- 这个就很容易辨别了,只要是声明,就是Declaration, 比如说声明一个变量,声明一个函数,声明一个类,或者是导入导出语句
- 也许你会发现怎么导入语句就只有一种ImportDeclaration, 而导出语句咔咔咔给我整了三种
- 别急,继续往下看
Specifier
- 这里主要介绍import的三种Specifier, 对导出的语句是将其导出声明归纳为三种,而对导入语句的声明语句则是统一为一种,其中导入的东西(见荧光部分)再细分为三种
嘿,了解完声明语句那么也许你会好奇,如果我先声明,再赋值的话,这个表达式又是什么节点呢? 继续往下看看吧
表达式Exprssion
比如我们的赋值表达式,函数表达式,一元表达式,二元表达式,对象表达式,逻辑表达式啥啥啥的,还是比较多的,除了以下归纳的还有挺多的比如说NewExpression, UpdateExpression...
看到这里你也许会发现,前面都是把代码拆分成小部件去识别,那么比如switch, for, while这些又是什么呢
语句Statement
我对它的形容就是,总是包裹在外面的大块头或者是能够独立执行的语句
typescrript
- 目前
typescript
的普及度也越来越高了,单纯对JS进行规范限制在很多时候都是不够的,故我们可以借助@typescript/eslint-parser
,从而对TS代码也能进行一个parse - 那么又会多出了很多节点和属性出来,不过大概的基本分类还是以上几种
四、开发规则
先看官网 主要是分成两大部分,第一部分是meta属性,第二部分是create方法
module.exports = {
meta: {...}
create(context) {...};
}
meta
meta主要用来记录/配置一些元数据
type
: 指示规则的类型,可以是problem
,suggestion
,layout
;- problem就是识别的代码会导致错误嘛,或者导致让人困然,是作为一个问题的存在;
- suggestion就是可以有更好的方法来完成,建议以更好的方式去完成
- layout表示比较注重外观,比如说分号,空格....
docs
:- description: 描述
- url 可以指定完整的文档的URL-就是报错时的信息中出现的文档路径
schema
: 指定规则配置的类型,如果没有指定的话配置内容是无法生效的fixable
: 如果没有该属性的话,fix功能不生效messages
: 作为对象的存在,key为messageId, value为报错信息
具体怎么写可以看以下例子
create
不完全是标准的estree,也不能直接使用estree-walk,因为eslint会给每个节点加上父属性
create: function(context) {
// 一些公共的事件和变量可以在这里定义
// 也有比如对一些白名单文件进行过滤,可以直接在这里return {} 这样就不会走下面的操作
return {
// 返回事件钩子
};
}
事件钩子的回调有三种:
- key为node或者selector的时候: 事件在进入node或者selector的时候触发(最常用到)
- key为“node:exit”或者“selector:exit”的时候: 事件在退出node或selector的时候出发
- key为事件名称的时候: ESLint 为代码路径分析调用函数(这个感觉还是比较少用到的,比如在计算圈复杂度的时候可以用到)
这里的selector其实就类似于CSS的selector, 个人感觉还是挺好用的,可以减少一些在回调函数里面的判断,也可以减少对相似类型node的多次调用,比如说:function就涵盖了具体可看selecotr
五、实践规则
个人实践,非最佳实践
- 一般开发一个规则可以三步走
- 写一个规则描述md文档, 可以包括一些good Case, bad Case ,description(忽略这一步)
- 写规则内容:就是上面讲述的内容
- 写规则单测:就写一些valid code和invalid code去判断规则内容是否符合预期
- 比如可以实践一个规则, 动态import必须捕获错误,我们可以考虑以下三种情况
- 前两种我们希望有catch去捕获错误,防止直接报错,后面我们希望有try catch将其包住
// 使用了Promise.all
async function test() {
return Promise.all([
// other
import(modulePath),
]).then(() => {
// doSomething
}).catch((err) => {
// doSomeThing
})
}
// 直接使用import
import(modulePath).then(res => {
// doSomething
}).catch((err) => {
// doSomething
})
// 使用await
try {
const { MailEditor } = await import(modulePath);
} catch {
// doSomething
}
- 那么就需要有两种报错信息, meta.messages就可以写为
messages: {
'no-try': '动态import{{modulePath}}时需要用try catch包住',
'no-catch': '动态import{{modulePath}}时需要catch捕获错误',
}
- 提一嘴,这里是可以使用变量替换的,用{{xxx}}表示变量,可以传入不同的值
- 规则内容
// try-catch-import.js
module.exports = {
meta: {
type: 'problem', // `problem`, `suggestion`, or `layout`
docs: {
description: "动态import需要用try catch包住", // 描述
url: ‘xxxx’, // 这里写文档对应的链接
},
fixable: null, // Or `code` or `whitespace` 如果可以快速修复的话就改为code,然后在下面写如何去修复
schema: [], // 如果需要配置传入参数的话则在这里配置
messages: {
'no-try': '动态import需要用try catch包住',
'no-catch': '动态import需要catch捕获错误',
}
},
create(context) {
// 向上寻找判断是否出现TryStatement
const findParentHasTry = (node) => {
while(node.type !== 'Program') {
if (node.type === 'TryStatement') {
return true;
}
node = node.parent;
}
return false;
}
// 会找到所有的 .then,.xxx , 找到catch就算ok
const findCallAndExpression = (node) => {
const currentUseProperty = [];
while(node.type !== 'CallExpression' || node.type !== 'MemberExpression') {
if (node.type === 'MemberExpression' && node.property.type === 'Identifier') {
currentUseProperty.push(node.property.name);
}
if (node.parent && node.parent.type === 'CallExpression' && node.parent.parent) {
node = node.parent.parent;
} else {
break;
}
}
return currentUseProperty.some((item) => item === 'catch')
}
// import作为argument的情况,也就是Promise.all的情况
const checkAsArgumentImport = (node) => {
while(node.type !== 'Program') {
if (node.type === 'CallExpression' && isPromise(node) && node.parent) {
return findCallAndExpression(node.parent);
}
node = node.parent
}
return false
}
const isPromise = (node) => {
return node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'Promise'
}
return {
// key为node节点,在进入节点的时候触发value的回调函数
ImportExpression: (node) => {
if (node.parent.type === 'AwaitExpression') {
// context.report用于报错,需要传入node属性,messageId/message
findParentHasTry(node.parent) || context.report({
node,
messageId: 'no-try', //指定messages中的key,就会报错对应的value
data: { // 传入报错信息对应的变量
modulePath: node.source.value ?? node.source.name
}
})
} else if (node.parent && node.parent.type === 'MemberExpression' && node.parent.property.type === 'Identifier') {
findCallAndExpression(node.parent) || context.report({
node,
messageId: 'no-catch'
})
} else {
// 上面两种情况都没有命中的话则找到最近的MemberExpression
checkAsArgumentImport(node) || context.report({
node,
messageId: 'no-catch',
data: {
modulePath: node.source.value ?? node.source.name
}
})
}
},
};
},
};
- 测试文件
- 无论是valid, 还是invalid都可以传入一些属性
- 比如code:用例代码, filename: 代码所在文件路径, options: 传入配置项
- invalid的每一项必须传入errors,会导致几个错误就需要传入几项
const rule = require('**/try-catch-import'); // 导入以上的规则内容
const RuleTester = require('eslint').RuleTester; // 导入RuleTester
// 一般我习惯使用@typescript-eslint/parser作为parser了,这个规则可以省略这一步
const eslintTester = new RuleTester({
parser: require.resolve('@typescript-eslint/parser'),
parserOptions: {
ecmaVersion: 6,
sourceType: 'module',
ecmaFeatures: {
jsx:true
}
},
});
eslintTester.run('try-catch-import', rule, {
// 这里写一些Good case
valid: [
{
// 使用await
code: `
try {
const { xxxx } = await import('xxxxx');
} catch {
//
}
`
},
// import(xx).then().catch()
{
code : `
import(modulePath).then(obj => {
//
return obj
}).catch(err => {
//
})
`
},
{
code : `
import(modulePath).then(obj => {
//
return obj
}).catch(err => {
//
}).finally(all => {})
`
},
// 使用Promise.all等
{
code: `
async function test() {
return Promise.all([
xxxxx,
import('xxxxx'),
]).then() => {
//
}).catch((err)=> {})
}
`
}],
// 这里写一些base case
invalid: [
{
code: `
const { xxx } = await import('xxxxxxx');
`,
errors: [ { messageId: 'no-try', type: "ImportExpression"} ]
},
{
code: `
import(modulePath).then(obj => {
// xxxxx
return xxxxx
})
`,
errors: [ { messageId: 'no-catch', type: "ImportExpression"} ]
},
{
code: `
async function test() {
return Promise.all([
xxx
import('xxxxx'),
]).then(xxx) => {
//
}`,
errors: [{ messageId: 'no-catch', type: "ImportExpression" }],
},
],
});
转载自:https://juejin.cn/post/7209110611858358331