深入对比 eslint 插件 和 babel 插件的异同点

308次阅读  |  发布于3年以前

babel 和 eslint 都是基于 AST 的,一个是做代码的转换,一个是做错误检查和修复。babel 插件和 eslint 插件都能够分析和转换代码,那这俩到底有啥不同呢?

本文我们来探究下 babel 插件和 eslint 插件差别在哪里。

babel 插件

babel 的编译流程分为 parse、transform、generate 3 步,可以指定插件,在遍历 AST 的时候会合并调用 visitor。

比如我们写一个在 console.xx 的参数插入文件名 + 行列号的插件:

对函数调用节点(CallExpression)的 callee 属性进行检查,如果是 console.xx 的 api,则在 arguments 中插入一个 StringLiteral 的字符串字面量节点,值为文件名 + 行列号。

const targetCalleeName = ['log', 'info', 'error', 'debug'].map(item => `console.${item}`);

const parametersInsertPlugin = ({ types }, options) => {
    return {
        visitor: {
            CallExpression(path, state) {
                const calleeName = path.get('callee').toString()
                if (targetCalleeName.includes(calleeName)) {
                   const { line, column } = path.node.loc.start;
                   path.node.arguments.unshift(types.stringLiteral(`${options.file.filename}: (${line}, ${column})`))
               }
            }
        }
    }
}
module.exports = parametersInsertPlugin;

然后使用 babel core 的 api 进行代码编译,并调用插件:

const { transformFileSync } = require('@babel/core');
const insertParametersPlugin = require('./plugin/parameters-insert-plugin');
const path = require('path');

const inputFilePath = path.join(__dirname, './sourceCode.js');

const { code } = transformFileSync(inputFilePath, {
    plugins: [insertParametersPlugin],
    parserOpts: {
        sourceType: 'unambiguous',
        plugins: ['jsx']
    }
});

console.log(code);

当源码为下面的代码时:

console.log(1);

function func() {
    console.info(2);
}

export default class Clazz {
    say() {
        console.debug(3);
    }
    render() {
        return <div>{console.error(4)}</div>
    }
}

目标代码为:

可以看到,在 console.xx 的 api 调用处插入了文件名和行列号的参数。

这就是一个 babel 插件做代码转换的例子。

我们从中能总结出 babel 插件的特点:

eslint 插件

eslint 插件也会对代码进行 parse,查找要检查的 AST,之后进行检查和报错,但不一定会修复代码,只有指定了 fix 才会进行修复。

我们写一个检查对象格式的 eslint 插件。

需求时把下面的代码格式进行检查和修复:

const  obj = {
    a: 1,b: 2,

    c: 3
}

变成这种:

const  obj = {
    a: 1,
    b: 2,
    c: 3
}

eslint 是可以查找某个 AST 关联的 token 的,也就是我们可以拿到对象的每一个属性开始和结束的 token 还有行列号,

我们要校验上一个属性结束的 token 的行号要等于下一个属性开始的 token 的行号。

所以就是这样写:

指定对 ObjectExpression 也就是 {} 表达式的每一个属性的开始和结束 token 的行号做检查,如果不是下一个属性是上一个属性的 +1 行,那就报错。

并且,还可以指定如何修复,我们这里的错误的修复方式就是把两个 token 之间的部分替换为换行符(os.EOL) + tab。

const os = require('os');

module.exports = {
     meta: {
         fixable: true
     },
     create(context) {
         const sourceCode = context.getSourceCode();
         return {
            ObjectExpression(node) {
                for (let i = 1; i < node.properties.length; i ++) {
                     const firstToken = sourceCode.getTokenAfter(node.properties[i - 1]);
                     const secondToken = sourceCode.getFirstToken(node.properties[i]);
                     if(firstToken.loc.start.line !== secondToken.loc.start.line - 1) {
                        context.report({
                            node,
                            message: '对象属性之间不能有空行',
                            loc: firstToken.loc,
                            *fix(fixer) {
                                yield fixer.replaceTextRange([firstToken.range[1],secondToken.range[0]], os.EOL + '\t');
                            }
                        });
                     }

                }
             }
         };
     }
 };

这样就完成了对象格式的检查和自动修复。

这个插件文件名命名为 object-property-format,然后我们使用 api 的方式调用下:

首先,引入 eslint 模块,创建 ESLint 对象:

const { ESLint } = require("eslint");

const engine = new ESLint({
    fix: false,
    overrideConfig: {
        parser: '@babel/eslint-parser',
        parserOptions: {
            sourceType: "unambiguous",
            requireConfigFile: false,
        },
        rules: {
            "object-property-format": "error"
        }
    },
    rulePaths: ['./'],
    useEslintrc: false
});

这里把配置文件关掉(useEslintrc: false),只用这里的的配置(overrideConfig)。

我们指定用 babel 的 parser(@babel/eslint-parser),并且不需要 babel 配置文件。之后引入刚才我们写的那个 rule,也就是 object-property-format,报错级别设置为 error。

还需要指定 rulePaths,也就是告诉 eslint 去哪里查找 rule。

之后,我们调用 lintText 的 api 进行 lint:

(async function main() {
  const results = await engine.lintText(`
  const  obj = {
        a: 1,b: 2,

        c: 3
    }
  `);

  console.log(results[0].output);

  const formatter = await engine.loadFormatter("stylish");
  const resultText = formatter.format(results);
  console.log(resultText);
})()

对于结果,我们使用内置的 formater 格式化了一下。

用 node 执行,结果如下:

可以看到,eslint 检查出了对象格式的两处错误。

为什么没有修复呢?因为没开启 fix 啊,eslint 需要开启 fix 才会修复代码。

把 Eslint 的 fix option 修改为 true,再试一下:

可以看到,没有报错了,而且代码也进行了修复。

这就是一个 eslint 插件做代码格式检查和修复的例子。

我们从中总结出 eslint 插件的 rule 的特点:

eslint 插件和 babel 插件的异同

我们把总结的 babel 插件和 eslint 插件的特点拿到一起对比下。(这里的 eslint 插件严格来说是指的 eslint 的 rule,eslint 插件可以包含多个 rule。)

babel 插件:

eslint 插件:

我们来对比下两者的异同:

eslint 的 AST 中记录了在源码中 range 信息,可以根据 range 信息查找 token,但其实 babel 也可以,babel parser 也可以指定 ranges、tokens

也就是说理论上基于 babel 完全可以实现 eslint 的功能,只不过两者 api 设计上的不同,导致了两者适合的场景不同。

但是,从本质上来说,两者编译流程上差别并不大。

总结

我们写了一个在 console.xx api 插入参数的 babel 插件,又写了一个检查和修复对象格式的 eslint 插件,分析了两者的特点,然后做了下对比。

两者插件形式上不同,api 也不同:

babel 是通过 path 的 api 对 AST 进行增删改,而 eslint 是通过 sourceCode 的 api 进行代码格式的检查,通过 fixer 的 api 进行修复。这就导致了 babel 插件更适合做代码转换,eslint 插件更适合做代码格式的校验和修复。但实际上 babel 也能做到 eslint 一样的事情,两者本质上的编译流程是差不多的。

这篇文章把 babel 插件和 eslint 插件放到一起进行了对比,讲述了两者本质的相同和 api 的不同,希望能够帮大家更好的掌握 babel 和 eslint 插件。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8