TDD方式创建一个Eslint插件,实现检查try catch的rule规则

image.png
这几天看到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
  1. mkdir eslint-plugin-try-statement(要求插件命名都要以eslint-plugin-开头)
  2. pnpm init 生成package.json
  3. pnpm i eslint -D 安装eslint
  4. touch index.js 这里是我们插件的逻辑代码
  5. 新建test/add.spec.js文件,这里是我们的测试用例
  6. mkdir demo; pnpm init; pnpm i eslint -D
  7. 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改动,自动执行测试用例了

image.png

这是只保留第一个正确的测试用例和第一个错误的测试用例的结果,因为我们没有写插件的实现,看到了log打印,所以是符合预期的

image.png

下面我们就来补充index.js的create方法,实现插件的逻辑主要借助AST分析

image.png

image.png
可以看到区别了吧,所以我们只需要看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'
                    })
                }
            }
        }
    }
}

再看下用例执行结果,符合预期

image.png

自动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

这时候我们看到了依赖里多了我们自定义的插件,并且是指向上一级目录的

image.png

image.png

然后在.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}')
    ]
}

image.png
单测全部通过了,我们再去实际的使用就是demo中去看看效果。
因为上一步已经添加了自定义插件,现在只需要重启下eslint就可以了

image.png
现在就可以看到错误提示了

image.png
如果你开启了保存时格式化,conmand+s,就会看到按照fix方法里写的格式自动fix了

image.png

你还可以修改成不自动fix,验证一下(修改了.eslint.js,记得重启eslint~)

这个插件实现的功能比较简单,感兴趣的话可以写一个自己的eslint插件,来扩展rule,还可以发布到npm供其他人使用。写完之后,你会发现对eslint用法的理解又加深了~

参考

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容