这几天看到Eslint可能要开始重写了(ΩДΩ) https://github.com/eslint/eslint/eslint/discussions/16557
- Completely new codebase
- ESM with type checking
- run in any runtime: Node, Deno, brower…
- lint more language
- New public APIs
- Rust-based replacements
- AST mutations for autofixing
- …
又是Rust和AST~
然后突然想起来之前写的eslint插件的文,还没写完,今天用TDD的方式重写一下吧☺
插件的功能是给每个方法添加try-catch(代码地址)这里先不考虑像下面这种先是变量声明等语句,然后是try-catch包裹主要逻辑的情况,我们实现的简单一些,只要函数体最外层不是try-catch,就给它提示出来。
function foo () {
let var1 = '';
try {
...
} catch (err) {
}
}
建立文件结构
|-- README.md
|-- demo
| |-- package.json
| |-- pnpm-lock.yaml
| `-- src
| `-- index.js
|-- index.js
|-- package.json
|-- pnpm-lock.yaml
`-- test
`-- add.spec.js
- mkdir eslint-plugin-try-statement(要求插件命名都要以eslint-plugin-开头)
- pnpm init 生成package.json
- pnpm i eslint -D 安装eslint
- touch index.js 这里是我们插件的逻辑代码
- 新建test/add.spec.js文件,这里是我们的测试用例
- mkdir demo; pnpm init; pnpm i eslint -D
- pnpx eslint –init 生成.eslintrc.js
编写测试用例
准备工作就做完了,下面我们写测试用例(你可以先写一个正确的和一个错误的用例,先保证没问题,后面再添加其他用例)
const { RuleTester } = require('eslint');
const { rules } = require('../index');
const ruleTester = new RuleTester();
ruleTester.run('try-statement', rules.add, {
// 正确的用例
valid: [
{
name: 'empty body',
code: 'function fetchUsers() {}'
},
{
name: 'empty body && starts with try statement',
code: 'function fetchUsers() { try {} catch(err) {} }'
},
{
name: 'starts with try statement',
code: 'function fetchUsers() { try { var timeout = 1000; } catch(err) {} }'
}
],
// 错误的用例
invalid: [
{
name: 'has no try statement',
code: 'function fetchUsers1() { var timeout = 1000; var retry = 3; }',
output: 'function fetchUsers1() { try {\nvar timeout = 1000; var retry = 3;\n} catch(err) {\n} }',
errors:[{
message: 'function fetchUsers must be embraced by try statement'
}]
},
{
name: 'do not fix',
code: 'function fetchUsers2() { var timeout = 1000; var retry = 3; }',
options: [false],
output: 'function fetchUsers2() { var timeout = 1000; var retry = 3; }',
errors:[{
message: 'function fetchUsers must be embraced by try statement'
}]
}
]
})
vaild里的用例是看函数体是否为空,不为空的话是不是全部用try-catch包裹。
invalid里的第一个用例是测试插件自动添加try-catch的结果,第二个是测试当插件配置了options参数为false,即不fix的情况。
这里的ruleTester.run
是eslint提供的api,跟使用jest断言是比较相似的
test('name', () => {
...
expect(xxx).toEqual(xxx);
});
ruleTester.run
的第一个参数是插件的名称,第二个参数是规则的名称,第三个参数是一个包含valid和invalid的对象,它们又分别是一个对象数组
{
valid: [
],
invalid: [
{
name: 'name',
code: '', // 要执行测试的代码,相当于expect的内容
output: '', // 执行后的期望输出,相当于toEqual的内容
options: [false], // 同插件使用是配置的options
errors:[{
message: 'function fetchUsers must be embraced by try statement'
}]
}
]
}
初始化插件
先简单写一下插件的代码
module.exports = {
rules: {
"add": {
meta: {
docs: {
description: "所有函数都用try-catch包裹",
},
fixable: 'code'
},
create: function (context) {
// 公共变量和函数应该在此定义,要求必须返回对象
console.log('eslint-plugin-try-statement add created !!!');
return {}
}
}
}
};
node test/add.jest.js
看下结果
为了避免每次都要手动执行这个命令,可以安装下mocha, 然后修改下package.json的脚本, 这样就可以监听add.spec.js和它依赖的index.js改动,自动执行测试用例了
这是只保留第一个正确的测试用例和第一个错误的测试用例的结果,因为我们没有写插件的实现,看到了log打印,所以是符合预期的
下面我们就来补充index.js的create方法,实现插件的逻辑主要借助AST分析
可以看到区别了吧,所以我们只需要看body是不是空数组,不是的话是不是只有TryStatement
就好了,是不是超级简单~
插件逻辑实现
create: function (context) {
// visitor AST
return {
// 返回事件钩子, 这个插件我们值关心函数声明FunctionDeclaration就可以了
// context和node都是提供给我们的参数,可以打印下看看它们的结构
FunctionDeclaration(node) {
const functionName = node.id.name;
const blockStatementBody = node.body.body; // 通过上图的AST分析,主要操作的是node.body.body这个节点
const hasBody = blockStatementBody.length !== 0;
if (hasBody) {
const startsWithTry = blockStatementBody[0].type === 'TryStatement';
const hasCatch = !!blockStatementBody[0].handler;
// 如果函数体不为空,但是body数组不是TryStatement语句,这个就是我们需要提示的情况了
// 调用context.report方法进行警告提示,message就是警告信息
if (!startsWithTry) {
context.report({
node,
message: `function ${functionName} must be embraced by try statement`,
})
} else if(!hasCatch) { // 有try但没有catch也提示一下
context.report({
node,
message: 'function ${functionName} must be embraced by try-catch statement'
})
}
}
}
}
}
再看下用例执行结果,符合预期
自动fix
接下来写一个自动fix的逻辑,fix也是context.report
对象参数的一个属性,返回这样的结构
fix() {
return {
range: [],
text: ''
}
}
不过这样写起来有点儿麻烦,我们用它的参数fixer
提供的方法
- insertTextAfter
- insertTextAfterRange
- insertTextBefore
- insertTextBeforeRange
- replaceText
- replaceTextRange
- remove
- removeRange
有时候可能不想要自动fix,这时候可以通过option是配置来实现
单测通过后,就可以在demo中使用下我们的插件了
- cd demo
- pnpm install ../ -D
这时候我们看到了依赖里多了我们自定义的插件,并且是指向上一级目录的
然后在.eslint.js
中配置插件
plugins: [
'try-statement'
],
rules: {
'try-statement/add': ['warn', false] // 错误级别warn,false代表不自动fix
}
fix函数实现
fix(fixer) {
// console.log(fixer);
const isNeedFix = context.options[0]; // options即传入的配置,这里是[false]
if (isNeedFix === false) {
console.log('** options **', functionName, context.options)
return fixer.insertTextBeforeRange(blockStatementBody[0].range, '');
}
// 开头和结尾分别添加try、catch
return [
fixer.insertTextBeforeRange(blockStatementBody[0].range, 'try {\n'),
fixer.insertTextAfterRange(blockStatementBody[blockStatementBody.length - 1].range, '\n} catch(err) {\n}')
]
}
单测全部通过了,我们再去实际的使用就是demo中去看看效果。
因为上一步已经添加了自定义插件,现在只需要重启下eslint就可以了
现在就可以看到错误提示了
如果你开启了保存时格式化,conmand+s,就会看到按照fix方法里写的格式自动fix了
你还可以修改成不自动fix,验证一下(修改了.eslint.js,记得重启eslint~)
这个插件实现的功能比较简单,感兴趣的话可以写一个自己的eslint插件,来扩展rule,还可以发布到npm供其他人使用。写完之后,你会发现对eslint用法的理解又加深了~
参考
暂无评论内容