深入对比 Eslint 插件 和 Babel 插件的异同点

开发 前端
babel 是通过 path 的 api 对 AST 进行增删改,而 eslint 是通过 sourceCode 的 api 进行代码格式的检查,通过 fixer 的 api 进行修复。

[[421210]]

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

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

babel 插件

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

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

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

  1. const targetCalleeName = ['log''info''error''debug'].map(item => `console.${item}`); 
  2.  
  3. const parametersInsertPlugin = ({ types }, options) => { 
  4.     return { 
  5.         visitor: { 
  6.             CallExpression(path, state) { 
  7.                 const calleeName = path.get('callee').toString() 
  8.                 if (targetCalleeName.includes(calleeName)) { 
  9.                    const { line, column } = path.node.loc.start; 
  10.                    path.node.arguments.unshift(types.stringLiteral(`${options.file.filename}: (${line}, ${column})`)) 
  11.                } 
  12.             } 
  13.         } 
  14.     } 
  15. module.exports = parametersInsertPlugin; 

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

  1. const { transformFileSync } = require('@babel/core'); 
  2. const insertParametersPlugin = require('./plugin/parameters-insert-plugin'); 
  3. const path = require('path'); 
  4.  
  5. const inputFilePath = path.join(__dirname, './sourceCode.js'); 
  6.  
  7. const { code } = transformFileSync(inputFilePath, { 
  8.     plugins: [insertParametersPlugin], 
  9.     parserOpts: { 
  10.         sourceType: 'unambiguous'
  11.         plugins: ['jsx'
  12.     } 
  13. }); 
  14.  
  15. console.log(code); 

当源码为下面的代码时:

  1. console.log(1); 
  2.  
  3. function func() { 
  4.     console.info(2); 
  5.  
  6. export default class Clazz { 
  7.     say() { 
  8.         console.debug(3); 
  9.     } 
  10.     render() { 
  11.         return <div>{console.error(4)}</div> 
  12.     } 

 目标代码为:

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

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

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

  • 插件的形式是函数返回一个对象,对象的 visitor 属性声明对什么节点做什么处理
  • visitor 函数可以通过 path 的 api 来对 ast 增删改
  • 修改后的 ast 会打印成目标代码

eslint 插件

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

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

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

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

变成这种:

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

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

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

所以就是这样写:

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

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

  1. const os = require('os'); 
  2.  
  3. module.exports = { 
  4.      meta: { 
  5.          fixable: true 
  6.      }, 
  7.      create(context) { 
  8.          const sourceCode = context.getSourceCode(); 
  9.          return { 
  10.             ObjectExpression(node) { 
  11.                 for (let i = 1; i < node.properties.length; i ++) { 
  12.                      const firstToken = sourceCode.getTokenAfter(node.properties[i - 1]); 
  13.                      const secondToken = sourceCode.getFirstToken(node.properties[i]); 
  14.                      if(firstToken.loc.start.line !== secondToken.loc.start.line - 1) { 
  15.                         context.report({ 
  16.                             node, 
  17.                             message: '对象属性之间不能有空行'
  18.                             loc: firstToken.loc, 
  19.                             *fix(fixer) { 
  20.                                 yield fixer.replaceTextRange([firstToken.range[1],secondToken.range[0]], os.EOL + '\t'); 
  21.                             } 
  22.                         }); 
  23.                      } 
  24.                       
  25.                 } 
  26.              } 
  27.          }; 
  28.      } 
  29.  }; 

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

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

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

  1. const { ESLint } = require("eslint"); 
  2.  
  3. const engine = new ESLint({ 
  4.     fix: false
  5.     overrideConfig: { 
  6.         parser: '@babel/eslint-parser'
  7.         parserOptions: { 
  8.             sourceType: "unambiguous"
  9.             requireConfigFile: false
  10.         }, 
  11.         rules: { 
  12.             "object-property-format""error" 
  13.         } 
  14.     }, 
  15.     rulePaths: ['./'], 
  16.     useEslintrc: false 
  17. }); 

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

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

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

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

  1. (async function main() { 
  2.   const results = await engine.lintText(` 
  3.   const  obj = { 
  4.         a: 1,b: 2, 
  5.  
  6.         c: 3 
  7.     } 
  8.   `); 
  9.  
  10.   console.log(results[0].output); 
  11.  
  12.   const formatter = await engine.loadFormatter("stylish"); 
  13.   const resultText = formatter.format(results); 
  14.   console.log(resultText); 
  15. })() 

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

用 node 执行,结果如下:

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

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

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

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

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

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

  • rule 的形式是对象,create 属性是一个函数,返回一个对象,指定对什么 AST 做什么检查和修复
  • AST 处理函数可以通过 context 的 api 来拿到源码不同位置的 token 来进行格式的检查
  • fix 函数可以拿到 fixer 的 api,来对某个位置的代码进行字符的增删改
  • 默认不会修复代码,需要指定 fix 才会进行修复

eslint 插件和 babel 插件的异同

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

babel 插件:

  • 插件的形式是函数返回一个对象,对象的 visitor 属性声明对什么节点做什么处理
  • visitor 函数可以通过 path 的 api 来对 ast 增删改
  • 修改后的 ast 会打印成目标代码

eslint 插件:

  • rule 的形式是对象,create 属性是一个函数,返回一个对象,指定对什么 AST 做什么检查和修复
  • AST 处理函数可以通过 context 的 api 来拿到源码不同位置的 token 来进行格式的检查
  • fix 函数可以拿到 fixer 的 api,来对某个位置的代码进行字符的增删改
  • 默认不会修复代码,需要指定 fix 才会进行修复

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

  • 从形式上来说,eslint 的 rule 是对象-函数-对象的形式,而 babel 插件是函数-对象的形式,多的部分是 eslint rule 的元信息,也就是 meta 属性。这是两者设计上的不同。
  • babel 插件和 eslint rule 都可以遍历节点,指定对什么节点做处理,但是 babel 插件可以通过 path 的 api 来增删改 AST,而 eslint 则是通过 context.getSourceCode() 拿到 sourceCode,然后通过 sourceCode 的 api 进行格式的检查,最后修复还要通过 fixer 的 api。
  • babel 插件的改动默认就是生效的,最多传入 options 进行控制,而 eslint 的 fix 功能只有开启才生效。

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

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

  • babel 是通过 path api 进行 AST 增删改,适合做代码分析和转换。
  • eslint 是通过 sourceCode 和 fixer api 进行代码格式的检查和 fix,适合做错误检查和修复。

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

总结

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

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

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

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

 

责任编辑:姜华 来源: 神光的编程秘籍
相关推荐

2021-09-02 16:15:29

开发技能代码

2021-11-19 23:54:19

插件Eslint

2009-11-06 10:11:34

WCF和Web Ser

2009-12-18 15:23:03

Vista和XP路由设

2015-05-22 09:49:25

2021-10-13 22:19:16

插件Babel import

2012-12-07 10:56:32

2021-09-11 19:46:14

配置

2010-06-13 09:18:28

UML依赖

2009-12-29 15:56:57

2022-01-18 18:46:55

Eslint抽象语法树Babel

2022-07-18 14:18:26

Babel代码面试

2022-09-30 15:46:26

Babel编译器插件

2012-11-22 13:02:24

jQuery插件Web

2009-11-10 11:25:35

VB.NET与Basi

2021-06-22 06:52:46

Vite 插件机制Rollup

2021-09-05 11:37:31

Eslint插件Vue

2009-12-11 10:29:03

PHP插件机制

2010-08-11 16:37:47

DB2数据库

2021-08-18 06:43:04

低代码无代码开发
点赞
收藏

51CTO技术栈公众号