淺談 AST 及 ESlint Rule:AST 是殺毀?(上)

前言

AST 和 ESlint Rule 是我近期在公司技術分享的主題,AST 是我一直想去了解。但是默默就拖延沒去研究的領域;ESlint Rule 則是既然都研究 AST 了,那總該有個實際應用的例子,所以就一併放進主題中了。

AST & ESlint Rule 會分成上下兩章節,上半章節簡單介紹 AST,下半章節則是會透過兩個例子來了解 AST 在 ESlint 裡是如何被利用的,以及如何實際開發專屬自己的 ESlint Rule。

Why should I care?

AST 和我們日常開發常用到的工具息息相關,舉例來說,像是 babel:

透過 babel 的幫助,我們可以寫更新更潮的程式碼,再透過 babel 轉換得到較舊的語法,不必擔心寫新語法造成的相容問題。

又比如像是可以幫你做 cherry pick 的 babel plugin:

又者是 Eslint:

透過 Eslint,除了檢查語法上的 coding style 以外,更可以提前偵錯,比如 import 路徑出錯:

又或者是 Webpack、TypeScript 等等談及程式碼靜態分析或是調整程式碼的工具,都會涉及到 AST,因此了解 AST 的好處是你可以更了解這些工具是如何運作的,更而甚者是你可以寫一個自己的 plugin,滿足自己客製化的需求。

AST 是什麼?

談論到 AST 以前,讓我們先談談 JavaScript code 如何被執行的(以 V8 為例):

我們寫的 JavaScript code 在經過 Parser 解析之後產生了 AST(Abstract Syntax Tree),而產生後的 AST 就是我們要談論的,但離題一下,先繼續往下講。

AST 接著會進到 V8 引擎做執行,Interpreter 負責編譯程式碼變為 bytecode,當 bytecode 被頻繁的執行時,會被丟到 optimizing compiler 做編譯,編譯成執行起來更有效率的 machine code,變成經過優化的 code。

所以到底什麼是 AST?

當 JavaScript 的 source code 經過 parser 轉換的時候會經歷以下階段:

  1. Lexical Analysis(Tokenization)
  2. Syntax Analysis(Parsing)
  3. Code Generation

Lexical Analysis(Tokenization):

在 Lexical Analysis(又稱為 Tokenization)會把程式碼切成一小塊一小塊的 tokens,如下圖:

可以把這階段想成像是把「一個句子」拆成「一個一個字」,每個字都會有自己的 type,比如名詞、動詞又或者是標點符號,但此時我們還不知道句子本身的含義,以及每個字在句子中的關聯。

Syntax Analysis(Parsing):

在這個階段 parser 會把一整個 list 的 tokens 變成 AST,parser 將這些所有的 tokens 變成一個真正代表我們程式碼結構的 tree,像是比如我們原本 token 裡有 () 兩個括號,但我們不知道那是什麼意思,但現在我們知道他是一個 function call 了。

那再舉一個例子的話,像是 HTML 不是會被轉成 DOM Tree 嗎?這也是同樣的概念,我們可能寫了像是 h2div 等等的 tag,也是同樣會被轉成 token 後,再被連結成一棵 tree。

那可以特別注意的是現在沒有一種特定的 AST 規格,也就是說產生出來的抽象語法樹裡面的屬性會因為不同的 parser 而有不一樣的屬性,當然內容看起來會大同小異,但可能一些名稱會不一樣,或是多一些、少一些內容都有可能。

像我們剛剛的 isPanda('🐼') 使用 Eslint 的 parser Espree 產生出來的 AST 會長這樣:

{
  "type": "Program",
  "start": 0,
  "end": 13,
  "range": [
    0,
    13
  ],
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 13,
      "range": [
        0,
        13
      ],
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 13,
        "range": [
          0,
          13
        ],
        "callee": {
          "type": "Identifier",
          "start": 0,
          "end": 7,
          "range": [
            0,
            7
          ],
          "name": "isPanda"
        },
        "arguments": [
          {
            "type": "Literal",
            "start": 8,
            "end": 12,
            "range": [
              8,
              12
            ],
            "value": "🐼",
            "raw": "'🐼'"
          }
        ]
      }
    }
  ],
  "sourceType": "module"
}

Code Generation

接下來,我們就可以針對產生的 AST 內容進行操弄,調整出自己想要的 code:

針對 AST 進行調整的好處是什麼呢?因為我們有時候想調整一段程式碼,比如通過 code 直接調整,又或者是直接調整 token,有可能會發生非預期的結果,比如我們以為改動的範圍在自己理解的 scope,但因為不知道程式碼之間的結構、關聯性的緣故,會影響到其他 scope。

那透過 AST 來改動程式碼的好處是提供了我們足夠的訊息,讓我們知道 code 的結構如何,就可以了解到調整某段程式碼之後影響的範圍,所以用 AST 來調整程式碼是相對安全的。

AST 中場小結

簡而言之,我們剛剛講的所有東西可以簡化成下面這張圖:

左邊是我們人看的程式碼,也就是我們寫的 source code;右邊則是電腦看的程式碼,會經過 Lexical Analysis、Syntax Analysis 後產生出 AST,我們的各種工具再透過操作 AST 產生出想要的程式碼。

以我簡單的理解來看,AST 就是包含你程式碼資訊的 object,「抽象語法樹」聽起來很恐怖,但實際上就是個 object。

因為 AST 包含了更多資訊、結構更加齊全,我們就可以透過操弄這棵 tree 來得到自己想要的結果,又或是透過 tree 針對程式碼進行分析,進而提早偵錯。

AST Explorer

那目前有什麼工具是可以更簡單的知道程式碼被 parser 轉換過後的 AST 嗎?有的,那就是 AST Explorer

以下圖來說:

AST Explorer

  • 左上角的區塊代表原本程式碼
  • 右上角代表 AST 的長相,AST 在滑鼠上去的時候會 hightlight 到對應的程式碼。
  • 左下角是 Eslint plugin 的規則撰寫
  • 右下角是 output,可以看到自己的程式碼會如何被抓出來警告

AST Explorer 很讚的地方在於提供多種不同 parser,並且回饋很及時、介面直覺,剛看介面可能覺得有點硬派,或看不懂在幹嘛,但耐心摸索一下之後就會發現非常好用,很多介紹 AST 的文章都會提到這工具,我自己也覺得 AST Explorer 非常適合剛接觸 AST 的人,可以很快速的了解 AST 到底是個什麼東西。

這篇文主要就是介紹基礎的 AST、AST Explorer 這兩個工具,有了這些前置知識以後,撰寫 ESlint Rule 的時候就會更了解到底是在做些什麼事情了!所以預計下一篇文章,會基於這篇對 AST 的介紹,再更進一步的介紹 AST 的應用。

References