爲什麼 Eslint 可以檢查和修復格式問題,而 Babel 不可以?

Eslint 可以檢查出代碼中的錯誤和一些格式問題,並能自動修復,它的實現原理就是基於 AST (抽象語法樹)。

通過 Parser 把源碼解析成 AST 對象樹,源碼字符串中的各種信息就被保存到了這個對象樹裏,然後遍歷 AST,對每一部分做檢查就能實現 Lint 的功能,而自動 fix 的功能則是基於字符串替換實現的,指定某一段 range,替換成另一段文本即可。

說起來,Babel 也是基於 AST 實現的代碼分析和轉換,但是卻不能檢查和修復格式的問題,這是爲什麼呢?爲什麼 Eslint 可以檢查格式而 Babel 不可以呢?

我們先寫一個 Eslint 的 rule 來感受下 Eslint 是怎麼檢查和修復格式問題的。

Eslint 檢查格式的 rule

大括號有兩種主流寫法,一種是同一行寫:

if (name === 'guang') {
}

另一種是新開一行寫:

if (name === 'guang')
{
}

我們寫一個 eslint 的 rule 來檢查大括號的格式並自動修復成同一行的格式。

思路分析

Eslint 的檢查是基於 AST,我們要檢查的 AST 是塊語句 BlockStatement。

關於什麼代碼是什麼 AST 可以用 astexplorer.net 可視化的查看,parser 選擇 espree (Eslint 默認的 parser):

具體來說檢查的是 BlockStatement 的最開始的 token { 的行號,是不是和它前一個 token 在同一行。(token 是指最小的不可再細分的單詞,比如關鍵字、變量名等標識符、各種分隔符等)

如果是同一行,則說明了是符合規範的。當然我們還可以進一步檢查一下大括號 { 和前一個 token 之間有沒有空格。

思路理清了,我們來寫下代碼:

代碼實現

Eslint 的 rule 的格式是這樣的:

 module.exports = {
     meta: {
         docs: {
             description: "enforce consistent brace style for blocks"
         },

         fixable: true
     },
 
     create(context) {
         return {
            BlockStatement(node) {
            }
        }
    }
 };

分爲 meta 和 create 兩部分:

我們在 create 裏聲明瞭對 BlockStatement 節點的檢查,它的參數就是對應的節點對象。

但是我們要檢查的是 token,這個用 context 裏的一個 api:

create(context) {
     const sourceCode = context.getSourceCode();
     return {
        BlockStatement(node) {
            const firstToken = sourceCode.getFirstToken(node);
            const beforFirstToken = sourceCode.getTokenBefore(node);
        }
     }
}

我們從 context 中拿到了 sourceCode 的 api,它就是用來取 token 的。

我們用 getFirstToken 拿到了當前 node 的開始 token,也就是 {,

然後又拿到了當前 node 的前面一個節點的 token,也就是 ):

token 中保存了行列號信息,那麼對比下行列號就知道是否有格式問題了:

如果 {所在的行和) 所在的行不是同一行,就報錯

if (firstToken.loc.start.line !== beforFirstToken.loc.start.line) {
    context.report({
        node,
        loc: firstToken.loc,
        message: '大括號格式不對'
    });
}

修復的方式自然就是把 {和) 之間的部分替換成一個空格,這個使用 fixer 提供的 api:replaceTextRange:

if (firstToken.loc.start.line !== beforFirstToken.loc.start.line) {
    context.report({
        node,
        loc: firstToken.loc,
        message: '大括號格式不對'
        fix: fixer ={
            return fixer.replaceTextRange([beforFirstToken.range[1], firstToken.range[0]]' ');
        }
    });
}

同理,也可以檢查出 {和) 之間沒有空格的格式錯誤,修復方式一樣:

if (firstToken.loc.start.column === beforFirstToken.loc.start.column + 1){
    context.report({
        node,
        loc: firstToken.loc,
        message: '大括號前缺少空格',
        fix: fixer ={
            return fixer.replaceTextRange([beforFirstToken.range[1], firstToken.range[0]]' ');
        }
    });
}

這樣就實現了大括號格式的檢查和自動修復。

我們來試下效果:

測試 rule

Eslint 除了提供命令行外,也提供了 api,我們調用它的 api 來測試 rule:

先創建 ESLint 對象,指定 rulePaths 也就是查找 rule 的目錄爲當前目錄:

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

const engine = new ESLint({
    fix: false,
    overrideConfig: {
        parserOptions: {
            ecmaVersion: 6,
        },
        rules: {
            'my-brace-style'['error']
        }
    },
    rulePaths: [__dirname],
    useEslintrc: false
});

useEslintrc 爲 false 是不查找配置文件, fix 爲 false 是不自動修復。

然後調用它的 lintText 代碼來測試,返回的結果使用 formatter 打印:

(async function main() {
    const results = await engine.lintText(`
        if (print) 
        {
            const num = a + b;
            console.log(num);
        }
        for(let i = 0;i<100;i++)
        {
            console.log(i);
        }
  `);
  
    console.log(results[0].output);
  
    const formatter = await engine.loadFormatter("stylish");
    const resultText = formatter.format(results);
    console.log(resultText);
  })();

我們來試一下效果:

三處格式錯誤都檢查出來了!

然後把 fix 設爲 true,來測試下自動修復:

格式自動修復了!

這樣我們就通過 Eslint 的 rule 實現了代碼格式的檢查和自動修復。

代碼上傳到了 github:https://github.com/QuarkGluonPlasma/eslint-plugin-exercize

那麼再回到最開始的問題,爲什麼 Eslint 可以檢查代碼格式,而 Babel 不可以呢?

爲什麼 Eslint 可以檢查格式 Babel 不可以

我們寫了一個檢查大括號格式的 rule,可以發現能夠做格式檢查關鍵是能找到關聯的 token。

Eslint 的 AST 記錄了所有的 token,token 中有行列號信息,而且 AST 中也保存了 range,也就是當前節點的開始結束位置。並且還提供了 SourceCode 的 api 可以根據 range 去查詢 token。這是它能實現格式檢查的原因。

而 Babel 其實也支持 range 和 token,但是卻沒有提供根據 range 查詢 token 的 api,這是它不能做格式檢查的原因。

其實 Babel 和 Eslint 原理差不多,但是 Eslint 是被設計來做代碼錯誤和格式檢查與修復的,而 Babel 是被設計用來做代碼分析和轉換的,目的不同,所以也就提供了不同的 api,能夠做不同的事情。

總結

Eslint 是用來檢查代碼中的錯誤和格式問題的,基於 AST,Babel 也是基於 AST 做的代碼分析和轉換,但是卻不能檢查格式。

爲了探究原因,我們寫了一個 EsLint 的檢查大括號格式的 rule,通過 SourceCode 的 api 拿到 {和) 的 token,對比下行列號來做檢查。並且通過 fixer 的字符串替換做了自動修復。

寫完之後,我們發現 EsLint 能做格式檢查的原因是因爲 AST 中記錄了 range,也保留了 token 信息,並且提供了根據 range 查詢 token 的 api,而 Babel 沒有。

EsLint 和 Babel 原理大同小異,但是有不同的設計目的,所以提供了不同的 api,有着不同的功能。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/sy_JdpuFWmrpXeKl5AvPRQ