深入對比 eslint 插件 和 babel 插件的異同點
babel 和 eslint 都是基于 AST 的,一個是做代碼的轉換,一個是做錯誤檢查和修復。babel 插件和 eslint 插件都能夠分析和轉換代碼,那這倆到底有啥不同呢?
本文我們來探究下 babel 插件和 eslint 插件差別在哪里。
babel 插件
babel 的編譯流程分為 parse、transform、generate 3 步,可以指定插件,在遍歷 AST 的時候會合并調用 visitor。
比如我們寫一個在 console.xx 的參數(shù)插入文件名 + 行列號的插件:
對函數(shù)調用節(jié)點(CallExpression)的 callee 屬性進行檢查,如果是 console.xx 的 api,則在 arguments 中插入一個 StringLiteral 的字符串字面量節(jié)點,值為文件名 + 行列號。
- 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 調用處插入了文件名和行列號的參數(shù)。
這就是一個 babel 插件做代碼轉換的例子。
我們從中能總結出 babel 插件的特點:
- 
    
插件的形式是函數(shù)返回一個對象,對象的 visitor 屬性聲明對什么節(jié)點做什么處理
 - 
    
visitor 函數(shù)可以通過 path 的 api 來對 ast 增刪改
 - 
    
修改后的 ast 會打印成目標代碼
 
eslint 插件
eslint 插件也會對代碼進行 parse,查找要檢查的 AST,之后進行檢查和報錯,但不一定會修復代碼,只有指定了 fix 才會進行修復。
我們寫一個檢查對象格式的 eslint 插件。
需求時把下面的代碼格式進行檢查和修復:
- const obj = {
 - a: 1,b: 2,
 - c: 3
 - }
 
變成這種:
- const obj = {
 - a: 1,
 - b: 2,
 - c: 3
 - }
 
eslint 是可以查找某個 AST 關聯(lián)的 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 模塊,創(chuàng)建 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 執(zhí)行,結果如下:

可以看到,eslint 檢查出了對象格式的兩處錯誤。
為什么沒有修復呢?因為沒開啟 fix 啊,eslint 需要開啟 fix 才會修復代碼。
把 Eslint 的 fix option 修改為 true,再試一下:

可以看到,沒有報錯了,而且代碼也進行了修復。
這就是一個 eslint 插件做代碼格式檢查和修復的例子。
我們從中總結出 eslint 插件的 rule 的特點:
- 
    
rule 的形式是對象,create 屬性是一個函數(shù),返回一個對象,指定對什么 AST 做什么檢查和修復
 - 
    
AST 處理函數(shù)可以通過 context 的 api 來拿到源碼不同位置的 token 來進行格式的檢查
 - 
    
fix 函數(shù)可以拿到 fixer 的 api,來對某個位置的代碼進行字符的增刪改
 - 
    
默認不會修復代碼,需要指定 fix 才會進行修復
 
eslint 插件和 babel 插件的異同
我們把總結的 babel 插件和 eslint 插件的特點拿到一起對比下。(這里的 eslint 插件嚴格來說是指的 eslint 的 rule,eslint 插件可以包含多個 rule。)
babel 插件:
- 
    
插件的形式是函數(shù)返回一個對象,對象的 visitor 屬性聲明對什么節(jié)點做什么處理
 - 
    
visitor 函數(shù)可以通過 path 的 api 來對 ast 增刪改
 - 
    
修改后的 ast 會打印成目標代碼
 
eslint 插件:
- 
    
rule 的形式是對象,create 屬性是一個函數(shù),返回一個對象,指定對什么 AST 做什么檢查和修復
 - 
    
AST 處理函數(shù)可以通過 context 的 api 來拿到源碼不同位置的 token 來進行格式的檢查
 - 
    
fix 函數(shù)可以拿到 fixer 的 api,來對某個位置的代碼進行字符的增刪改
 - 
    
默認不會修復代碼,需要指定 fix 才會進行修復
 
我們來對比下兩者的異同:
- 
    
從形式上來說,eslint 的 rule 是對象-函數(shù)-對象的形式,而 babel 插件是函數(shù)-對象的形式,多的部分是 eslint rule 的元信息,也就是 meta 屬性。這是兩者設計上的不同。
 - 
    
babel 插件和 eslint rule 都可以遍歷節(jié)點,指定對什么節(jié)點做處理,但是 babel 插件可以通過 path 的 api 來增刪改 AST,而 eslint 則是通過 context.getSourceCode() 拿到 sourceCode,然后通過 sourceCode 的 api 進行格式的檢查,最后修復還要通過 fixer 的 api。
 - 
    
babel 插件的改動默認就是生效的,最多傳入 options 進行控制,而 eslint 的 fix 功能只有開啟才生效。
 
eslint 的 AST 中記錄了在源碼中 range 信息,可以根據(jù) range 信息查找 token,但其實 babel 也可以,babel parser 也可以指定 ranges、tokens


也就是說理論上基于 babel 完全可以實現(xiàn) eslint 的功能,只不過兩者 api 設計上的不同,導致了兩者適合的場景不同。
- 
    
babel 是通過 path api 進行 AST 增刪改,適合做代碼分析和轉換。
 - 
    
eslint 是通過 sourceCode 和 fixer api 進行代碼格式的檢查和 fix,適合做錯誤檢查和修復。
 
但是,從本質上來說,兩者編譯流程上差別并不大。
總結
我們寫了一個在 console.xx api 插入?yún)?shù)的 babel 插件,又寫了一個檢查和修復對象格式的 eslint 插件,分析了兩者的特點,然后做了下對比。
兩者插件形式上不同,api 也不同:
babel 是通過 path 的 api 對 AST 進行增刪改,而 eslint 是通過 sourceCode 的 api 進行代碼格式的檢查,通過 fixer 的 api 進行修復。這就導致了 babel 插件更適合做代碼轉換,eslint 插件更適合做代碼格式的校驗和修復。但實際上 babel 也能做到 eslint 一樣的事情,兩者本質上的編譯流程是差不多的。
這篇文章把 babel 插件和 eslint 插件放到一起進行了對比,講述了兩者本質的相同和 api 的不同,希望能夠幫大家更好的掌握 babel 和 eslint 插件。















 
 
 


 
 
 
 