自定义eslint 校验规则

1,290 阅读5分钟

问题

// 测试时使用,后面需要删除
let url = 'ceshi/dasdadaadsa'

// TODO 未完成逻辑,后续优化
for(a of b) {
    ...
}

我们提交代码的时候,有时候只是想做临时提交,或者是配合测试修改了某些字段,甚至我们已经加上备注提醒自己后续需要修改,但是依旧有些情况会忘记

那么,我们在提交代码的时候,可不可以通过eslint来自定义规则去校验代码呢

方案

方案1:查找eslint有没有现成的API

'no-warning-comments': [{terms: ['todo', '张三'], location: 'anywhere'}] //不能有警告备注

当备注中含有某些字符串时,警告提示

属性含义
terms["todo", "fixme", "xxx"]负责校验的字段(不区分大小写)
locationstart/anywhere校验的位置

当前示例

'no-warning-comments': [{terms: ['todo', 'ceshi'], location: 'start'}] //不能有警告备注

不正确的代码示例

/*eslint no-warning-comments: "error"*/

function callback(err, results) {
  if (err) {
    console.error(err);
    return;
  }
  // TODO
  // 张三
}

因为出现了todo张三字段,所以报错

/*eslint no-warning-comments: "error"*/

function callback(err, results) {
  if (err) {
    console.error(err);
    return;
  }
  // xyb TODO
}

no-warning-comments 校验时,会把多个单词拆开校验,所以,todo依旧会命中

正确的代码示例

/*eslint no-warning-comments: "error"*/

function callback(err, results) {
  if (err) {
    console.error(err);
    return;
  }
  // mnbdjklmbasdbjkTODO
}

这里location的值是 start 即从字符串开始开始校验,而todo字段在结尾才出现,所以是正确的通过 如果想改成错误,那么就需要将localtion修改为anywhere

pass:只能做备注的校验,与实际需求,有一定差距

方案2:自定义eslint插件

eslint 自定义规则可以通过npm 引入或者自定义 在项目中新建目录rule,新建ybxu.js内部添加自己要的校验逻辑

再在package.json中修改lint

增加--rulesdir ./rules src官方定义的新增自定义方法的写法,指向自己的文件夹

再在.eslintrc.js文件中,的rules里增加一条属性ybxu: 1,注意与自己新增的js同名

pass:执行时候需要执行 yarn lint 与现在执行的一套脚本差距过大(check.js),且上面拿到的node节点为AST树,上图代码中使用的node节点中根本没有直接对应的完整代码,反构建成代码太麻烦了。

方案3:知难而退,选择放弃

只要我放弃的足够快 那失败就永远追不上我

方案4:基于现有的check.js

现在的check.js逻辑

package.json文件中

"husky": {
    "hooks": {
        "pre-commit": "lint-staged",
        "pre-push": "npm run push-lint",
        "commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
    }
},
"lint-staged": {
    "*.{js,css,less,sass,scss,json,md,vue}": [
        "prettier --write",
        "node check.js",
        "git add"
    ]
}

可以看到我们的git commit真正执行的是node check.js

check.js文件中

// check.js
const exec = require('child_process').exec;
const CLIEngine = require('eslint').CLIEngine;
const cli = new CLIEngine({});
function getErrorLevel(number) {
    switch (number) {
        case 2:
            return 'error';
        case 1:
            return 'warn';
        default:
    }
    return 'undefined';
}
let pass = 0;

// 只针对src目录下的js和vue代码进行校验
exec('git diff --cached --name-only src| grep -E ".js$|.vue$"', (error, stdout) => {
    if (stdout.length) {
        const array = stdout.split('\n');
        array.pop();
        let index = array.indexOf('.eslintrc.js');

        const results = cli.executeOnFiles(array).results;
        let errorCount = 0;
        let warningCount = 0;
        results.forEach(result => {
            errorCount += result.errorCount;
            warningCount += result.warningCount;
            if (result.messages.length > 0) {
                console.log('\n');
                console.log(result.filePath);
                result.messages.forEach(obj => {
                    const level = getErrorLevel(obj.severity);
                    console.log(
                        `   ${obj.line}:${obj.column}  ${level}  ${obj.message}  ${obj.ruleId}`
                    );
                    pass = 1;
                });
            }
        });
        if (warningCount > 0 || errorCount > 0) {
            console.log(
                `\n   ${errorCount +
                    warningCount} problems (${errorCount} ${'errors'} ${warningCount} warnings)`
            );
        }
        process.exit(pass);
    }
    if (error !== null) {
        console.log(`exec error: ${error}`);
    }
});

check.js中的

const CLIEngine = require('eslint').CLIEngine;
const cli = new CLIEngine({});

通过Node API的方式直接使用

最终在下方将我们修改文件的目录数据,抛入,即可获得校验结果results

// array 是修改文件的地址数组
const results = cli.executeOnFiles(array).results;

那么我们能不能在new CLIEngine({})这个过程中塞入我们自定义的方法呢?

官方文档没看懂,放弃了

转念一想,我们在node环境中,也知道了修改代码的路径,那么我们自己可以把代码取出来,自己去做校验

直接贴代码

// check.js
const exec = require('child_process').exec;
const rf = require('fs');
const path = require('path');
const CLIEngine = require('eslint').CLIEngine;
const cli = new CLIEngine({});
function getErrorLevel(number) {
    switch (number) {
        case 2:
            return 'error';
        case 1:
            return 'warn';
        default:
    }
    return 'undefined';
}
let pass = 0;
// 封装的获取文件内容方法
function read(dir) {
    return new Promise((resolve, reject) => {
        rf.readFile(dir, 'utf-8', (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

// 只针对src目录下的js和vue代码进行校验
exec('git diff --cached --name-only src| grep -E ".js$|.vue$"', async (error, stdout) => {
    let errorCount = 0;
    let warningCount = 0;

    // 需要提示警告的单词
    const warnWordArray = ['todo'];

    if (stdout.length) {
        const array = stdout.split('\n');
        array.pop();
        const publicPath = path.resolve(__dirname); // 获取public路径
        let eslintData = [];
        for (let srcItem of array) {
            // 采用path.join来拼接路径,做win和ios兼容问题
            let eslintPath = path.join(publicPath, srcItem);
            // 这里采用异步处理,防止文件没拿到被底部的 process.exit(pass); 直接退出了
            await read(eslintPath).then(data => {
                // 将内容和路径拼接好放进 eslintData 中
                eslintData.push({
                    code: String(data).split('\n'),  // 切成数组,每个值就是代码中的每一行,下标+1为行数
                    path: eslintPath
                });
            });
        }

        for (let codeData of eslintData) {
            console.log('\n');
            // 保证一个模块只显示一次path
            let isShowPath = false;
            // 保证一个模块只显示一次warn
            let isWarn = false;
            let beforeItem = ''
            codeData.code.forEach((item, line) => {
                // 如果在对应行找到了这个特殊字段
                for (let warnItem of warnWordArray) {
                    // column 为匹配到有问题的字符串的位置
                    let column = item.toLowerCase().indexOf(warnItem.toLowerCase());
                    // 是否需要跳过
                    let isPass = beforeItem.includes('// pass warnWord');
                    // 如果命中字段,并且不需要跳过,就打印错误提示
                    if (column > -1 && !isPass) {
                        pass = 1;
                        isWarn = true;
                        if (!isShowPath) {
                            console.log(codeData.path);
                            isShowPath = true;
                        }
                        // 打印错误提示,将代码行数,代码位置,和具体哪个字段的报错打印出来
                        console.log(`   ${line + 1}:${column + 1}  warn ${warnItem} 字段未处理`);
                        warningCount += 1;
                    }
                }
                beforeItem = item
            });
            if(isShowPath && isWarn) {
                console.log(`👿     如果此模块代码需要上传,在对应校验行顶部添加  // pass warnWord`);
            }
        }

        // let index = array.indexOf('.eslintrc.js');

        const results = cli.executeOnFiles(array).results;
        results.forEach(result => {
            errorCount += result.errorCount;
            warningCount += result.warningCount;
            if (result.messages.length > 0) {
                console.log('\n');
                console.log(result.filePath);
                result.messages.forEach(obj => {
                    const level = getErrorLevel(obj.severity);
                    console.log(
                        `   ${obj.line}:${obj.column}  ${level}  ${obj.message}  ${obj.ruleId}`
                    );
                    pass = 1;
                });
            }
        });
        if (warningCount > 0 || errorCount > 0) {
            console.log(
                `\n   ${errorCount +
                    warningCount} problems (${errorCount} ${'errors'} ${warningCount} warnings)`
            );
        }
        process.exit(pass);
    }
    if (error !== null) {
        console.log(`exec error: ${error}`);
    }
});


接下来我们直接执行node check.js(上面说过走git commit 最终也会到执行 node check.js 中)

添加跳过检验的备注

完美

1610674040(1).jpg