Eslint 的實現原理,其實挺簡單

Eslint 是我們每天都在用的工具,我們會用它的 cli 或 api 來做代碼錯誤檢查和格式檢查,有時候也會寫一些 rule 來做自定義的檢查和修復。

雖然每天都用,但我們卻很少去了解它是怎麼實現的。而瞭解 Eslint 的實現原理能幫助我們更好的使用它,更好的寫一些插件。

所以,這篇文章我們就通過源碼來探究下 Eslint 的實現原理吧。

Linter

Linter 是 eslint 最核心的類了,它提供了這幾個 api:

verify // 檢查
verifyAndFix // 檢查並修復

getSourceCode // 獲取 AST
defineParser // 定義 Parser
defineRule // 定義 Rule
getRules // 獲取所有的 Rule

SourceCode 就是指的 AST(抽象語法樹),Parser 是把源碼字符串解析成 AST 的,而 Rule 則是我們配置的那些對 AST 進行檢查的規則。這幾個 api 比較容易理解。

Linter 主要的功能是在 verify 和 verifyAndFix 裏實現的,當命令行指定 --fix 或者配置文件指定 fix: true 就會調用 verifyAndFix 對代碼進行檢查並修復,否則會調用 verify 來進行檢查。

那 verify 和 fix 是怎麼實現的呢?這就是 eslint 最核心的部分了:

確定 parser

我們知道 Eslint 的 rule 是基於 AST 進行檢查的,那就要先把源碼 parse 成 AST。而 eslint 的 parser 也是可以切換的,需要先找到用啥 parser:

默認是 Eslint 自帶的 espree,也可以通過配置來切換成別的 parser,比如 @eslint/babel-parser、@typescript/eslint-parser 等。

下面是 resolve parser 的邏輯:

確定了 parser 之後,就是調用 parse 方法了。

parse 成 SourceCode

parser 的 parse 方法會把源碼解析爲 AST,在 eslint 裏是通過 SourceCode 來封裝 AST 的。後面看到 SourceCode 就是指 AST.

有了 AST,就可以調用 rules 對 AST 進行檢查了

調用 rule 對 SourceCode 進行檢查,獲得 lintingProblems

parse 之後,會調用 runRules 方法對 AST 進行檢查,返回結果就是 problems,也就是有什麼錯誤和怎麼修復的信息。

那 runRules 是怎麼運行的 rule 呢?

rule 的實現如下,就是註冊了對什麼 AST 做什麼檢查,這點和 babel 插件很類似。

runRules 會遍歷 AST,然後遇到不同的 AST 會 emit 不同的事件。rule 裏處理什麼 AST 就會監聽什麼事件,這樣通過事件監聽的方式,就可以在遍歷 AST 的過程中,執行不同的 rule 了。

註冊 listener:

遍歷 AST,emit 不同的事件,觸發 listener:

這樣,遍歷完一遍 AST,也就調用了所有的 rules,這就是 rule 的運行機制

還有,遍歷的過程中會傳入 context,rule 裏可以拿到,比如 scope、settings 等。

還有 ruleContext,調用 AST 的 listener 的時候可以拿到:

而 rule 裏面就是通過這個 report 的 api 進行報錯的,那這樣就可以把所有的錯誤收集起來,然後進行打印。

這個 problem 是什麼呢?

linting problem

lint problem 是檢查的結果,也就是從哪一行(line)哪一列(column)到哪一行(endLine)哪一列(endColumn),有什麼錯誤(message)。

還有就是怎麼修復(fix),修復其實就是 從那個下標到哪個下標(range),替換成什麼文本(text)。

爲什麼 fix 是 range 返回和 text 這樣的結構呢?因爲它的實現就是簡單的字符串替換。

通過字符串替換實現自動 fix

遍歷完 AST,調用了所有的 rules,收集到了 linting problems 之後,就可以進行 fix 了。

fix 部分的相關源碼是這樣的:

也就是 verify 進行檢查,然後根據 fix 信息自動 fix。

fix 其實就是個字符串替換:

有的同學可能注意到了,字符串替換爲什麼要加個 while 循環呢?

因爲多個 fix 之間的 range 也就是替換的範圍可能是有重疊的,如果有重疊就放到下一次來修復,這樣 while 循環最多修復 10 次,如果還有 fix 沒修復就不修了。

這就是 fix 的實現原理,通過字符串替換來實現的,如果有重疊就循環來 fix。

preprocess 和 postprocess

其實核心的 verify 和 fix 的流程就是上面那些,但是 Eslint 還支持之前和之後做一些處理。也就是 pre 和 post 的 process,這些也是在插件裏定義的。

module.exports = {
    processors: {
        ".txt"{
            preprocess: function(text, filename) {
                return [ // return an array of code blocks to lint
                    { text: code1, filename: "0.js" },
                    { text: code2, filename: "1.js" },
                ];
            },

            postprocess: function(messages, filename) {
              
                return [].concat(...messages);
            }
        }
    }
};

之前的處理是把非 js 文件解析出其中的一個個 js 文件來,這和 webpack 的 loader 很像,這使得 Eslint 可以處理非 JS 文件的 lint。

之後的處理呢?那肯定是處理 problems 啊,也就是 messages,可以過濾掉一些 messages,或者做一些修改之類的。

那 preprocess 和 postprocess 是怎麼實現的呢?

這個就比較簡單了,就是在 verify 之前和之後調用就行。

通過 comment directives 來過濾掉一些 problems

我們知道 eslint 還支持通過註釋來配置,比如 /* eslint-disable */ /*eslint-enable*/ 這種。

那它是怎麼實現的呢?

註釋的配置是通過掃描 AST 來收集所有的配置的,這種配置叫做 commentDirective,也就是哪行那列 Eslint 是否生效。

然後在 verify 結束的時候,對收集到的 linting problems 做一次過濾即可。

上面講的這些就是 Eslint 的實現原理:

Eslint 和 CLIEngine 類

Linter 是實現核心功能的,上面我們介紹過了,但是在命令行的場景下還需要處理一些命令行參數,也就需要再包裝一層 CLIEngine,用來做文件的讀寫,命令行參數的解析。

它有 executeOnFiles 和 executeOnText 等 api,是基於 Linter 類的上層封裝。

但是 CLIEngine 並沒有直接暴露出去,而是又包裝了一層 EsLint 類,它只是一層比較好用的門面,隱藏了一些無關信息。

我們看下 eslint 最終暴露出來的這幾個 api:

總結

我們通過源碼理清了 eslint 的實現原理:

ESLint 的核心類是 Linter,它分爲這樣幾步:

除了核心的 Linter 類外,還有用於處理配置和讀寫文件的 CLIEngine 類,以及最終暴露出去的 Eslint 類。

這就是 Eslint 的實現原理,其實還是挺簡單的:

基於 AST 做檢查,基於字符串做 fix,之前之後還有 pre 與 post 的 process,支持註釋來配置過濾掉一些 problems。

把這些理清楚之後,就算是源碼層面掌握了 Eslint 了。

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