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:
Linter 是核心的類,直接對文字進行 lint
ESLint 是處理配置、讀寫檔案等,然後呼叫 Linter 進行 lint(中間的那層 CLIEngine 並沒有暴露出來)
SourceCode 就是封裝 AST 用的
RuleTester 是用於 rule 測試的一些 api。
總結
我們透過原始碼理清了 eslint 的實現原理:
ESLint 的核心類是 Linter,它分為這樣幾步:
preprocess,把非 js 文字處理成 js
確定 parser(預設是 espree)
呼叫 parser,把原始碼 parse 成 SourceCode(ast)
呼叫 rules,對 SourceCode 進行檢查,返回 linting problems
掃描出註釋中的 directives,對 problems 進行過濾
postprocess,對 problems 做一次處理
基於字串替換實現自動 fix
除了核心的 Linter 類外,還有用於處理配置和讀寫檔案的 CLIEngine 類,以及最終暴露出去的 Eslint 類。
這就是 Eslint 的實現原理,其實還是挺簡單的:
基於 AST 做檢查,基於字串做 fix,之前之後還有 pre 與 post 的process,支援註釋來配置過濾掉一些 problems。
把這些理清楚之後,就算是原始碼層面掌握了 Eslint 了。