动态

详情 返回 返回

AST 初探深淺,代碼還能這樣玩?! - 动态 详情

大家好,這裏是 菜農曰,歡迎來到我的頻道。我們今天的主題是 AST (抽象語法樹)

AST 聽起來好像是個很新的東西,那麼具體有什麼用,好不好用就在這篇文章中找到答案吧~

我們簡單將這個詞拆分抽象、語法、樹,如果我們能夠順利將這個詞拆分,那麼我們也就掌握了其核心所在

  • 抽象:抽象的反義詞是具象,也就説明抽象的事物關注點不在於細節,而在於整體
  • 語法:語法一組詞法的表達式,具備某種指定的規則,具有某種特定的意義,比如 1+1
  • :樹是一種一對多的結構,通過根節點往下遞生,可以存在多個子樹,當然這不是我們這篇討論的主題,但卻是重點

我們接下來通過幾個例子更加清楚瞭解一下什麼是樹

一、什麼是樹?

1)算數表達式

5 * 4 / 2 + 3 * 6 這是一個簡單的算法運算,但是如果我們要通過樹形的方式表達它的話,結果可能是以下這樣:

我們通過分析這張樹形圖,我們可以發現有哪幾個結構 ?

  • 一部分是數字5,4,2,3,6
  • 一部分是操作符*, /, +, *

我們從中抽取出了 + 符號,並將其作為該樹的根節點,這個時候又可以分為左右兩個子樹,我們從中提取出一棵子樹來看

觀察發現子樹又變成了一棵樹,那麼可以得出一個結論:任何一棵子樹都可以獨立成為一棵完整的樹,多個子樹可以組合成一棵完整的樹。至此,我們就完成了一棵樹的定義,接下來我們再看一個其他例子

2)XML 文件

XML文件也是我們日常中比較常用到的文件結構

<person>
    <name>
        張三
    </name>
    <label>
        法外狂徒
    </label>
</person>

我們將文件結構轉成屬性結構後,就可以很直觀的看出數據層級內容

二、樹的轉換

樹的有點是很直觀,可以直接看出數據層級內容,但是我們平時操作的時候只能是操作客觀上的樹形結構,而不是以上主觀的樹形結構。因此當我們得到上述樹形結構後,我們就需要對該樹進行扁平化操作,那問題來了,如何扁平化呢?

我們一樣拿上述算數運算為例

紅色的框框代表一棵樹,而綠色和黃色框框則表示該樹的兩棵子樹,當然 5 * 4 當然也可以框起來作為綠色框的子樹。

這個時候,聰明的小夥伴們看到這些樹有沒有什麼發現,比如每棵樹表示什麼?

我們可以發現每棵樹似乎都表示着一個算數運算

1)規則定義

轉換需要建立在一定的規則基礎上

我們需要先定義下規則,如果遇到一個運算,我們就以 BinaryExpression 來表示,而 運算 中的結構自然就包含着 字符運算符 ,比如 5 * 4 這是一個運算,我們將整體標識為一個 BinaryExpression

而這個運算中存在三個元素,分別是: 5, 4, *。那麼其中 54 我們就可以稱之為 字符* 可以稱之為 運算符。由此我們可以再定一個規則,字符 的類型我們可以用 Identifier 來標識,運算符 的類型我們就以 Operator 來表示。

到這步我們就已經簡單地定義好了一個 規則,接下來我們要做的事情就是利用我們的規則將上述樹形結構扁平化

2)小試牛刀

我們先拿上述例子來做操作,首先這是一個表達式,我們利用 BinaryExpression 進行標識

BinaryExpression
        type: BinaryExpression

從運算中我們 以運算符 可以拆分為左右兩部分,也就是 54,我們繼續進行標識

left: Identifier
        type: Identifier
        value: 5
right: Identifier
        type: Identifier
        valuer: 4

定義好兩部分後我們該如何將兩部分鏈接起來呢? 那就得用到我們的運算符了 *,我們先利用規則定義好運算符的表示

operator: *

然後將兩部分鏈接起來

BinaryExpression
        type: BinaryExpression
        left: Identifier
                type: Identifier
                value: 5
        operator: *
        right: Identifier
                type: Identifier
                valuer: 4

3)成品展示

很好,到這裏我們就完成了第一塊里程碑了!

4)趁熱打鐵

上面我們才完成了一小部分的規則轉換定義,接下來我們繼續將樹形結構進行轉換:

到這裏我們已經從樹形結構圖轉到了我們定義的層級結構了,但我們可以發現,以上的層級結構圖依然是不夠完整的

目前為止我們才定義了上述表達式中左邊的部分,還缺少右邊的定義,這個時候就需要大家來幫個忙, 幫我補充一下右邊的部分,結構體已經在下述文本中貼出,大家可以複製到自己的文本編輯器中進行填空補充,將__ 內容替換補充即可

right: __
        type: __
        left: __
            type: __
            value: __
        operator: __
        right: __
            type: __
            value: __

接下來就到了公佈答案的環節了!

right: BinaryExpression
        type: BinaryExpression
        left: Identifier
            type: Identifier
            value: 3
        operator: *
        right: Identifire
            type: Identifier
            value: 6

大家可以進行比對下答案是否正確,然後我們將兩部分內容進行組裝

到這裏,我們就已經得到了一個完整的層級結構了,那麼這部分內容跟我們今天將的 AST 有什麼關係呢?

我們先來看下真正的 AST(抽象語法樹)長啥樣

我們轉換一個簡單的函數:

function add(n, m){
  return n + m
}

左邊是我們平時編寫的代碼,而右側便是通過代碼轉換得到的 AST 樹

我們通過觀察這棵 AST 樹有什麼發現?沒錯!這棵 AST 樹的結構基本和我們剛剛共同完成的層級結構圖一致,這意味着我們剛剛自己手擼了一棵 AST 樹出來

三、揭露 AST 面紗

1)AST 定義

1. 它是什麼?

AST(抽象語法樹)並沒有我們所想的那麼神秘,它是源代碼語法結構的一種抽象表示,它以樹狀的形式表現編程語言的語法結構,樹上的每個節點都表示源代碼中的一種結構。

2. 它有什麼特徵?

首先它是抽象的,它無關語法結構,不會記錄源語言真實語法中的每個細節,比如分隔符,空白符,註釋等,它都會進行移除。

3. 它有什麼用?

通過以上的實踐,我們也認識到了轉換AST 是一項繁瑣的過程,但為什麼要去轉換呢?現在各種語言語法種類繁多,雖然最終落到計算機的眼中都是 0 和 1,但是編譯器需要識別語言,這個時候就需要使用一種通用的數據結構來描述,而 AST 就是那個東西,因為 AST 是真實存在且存在一定邏輯規則的。

4. 它是如何進行轉換的?

它轉換的過程中也是運用到了我們剛剛所説的幾種方式:

  • 詞法分析器
  • 語法分析器
  • 解釋器

比如我們寫個簡單的代碼:

const name = '張三'
  • 詞法分析

第一步就是 詞法分析 ,它的任務就是一個一個字母地讀取代碼,當它遇到 空格操作符特殊符號 的時候,就表示自己第一活已經掃描結束了,我們上述的代碼這經過 詞法分析 後就會被解析為 [const, name, =, '張三'] 這幾個值

  • 語法分析

經過上層的分析,我們已經拿到了各個 token, 也就是 token流 ,也就是接下來我們就可以對 token流 進行語法分析,比如我們第一個遇到的 token 是 const ,語法分析器通過分析,判斷它是一個 聲明參數 ,就會標記為 VariableDeclaration,以此類推,後面的幾個 token 都會進行分析,直到生成了一棵 AST 抽象語法樹

當生成樹的時候,解析器 會刪除一些沒必要的標識tokens(比如不完整的括號),因此AST不是100%與源碼匹配的,但是已經能讓我們知道如何處理了

2)AST 應用

AST 查看輔助工具:點我

解析並轉換 AST 的這個步驟比較繁瑣,當然我們不必重複造輪子,已經有人替我們造好了輪子,比如解析服Java文件,我們可以應用 Javaparser 進行 AST 轉換,解析 Js / Ts 文件,可以應用 Babelparser 進行 AST 轉換。當然,儘管輪子已經為我們準備好了,我們還需要如何運用,那就是得了解規則,下面附上一些常用的節點類型含義對照表,也就是 AST 轉換的規則:

類型名稱 中文譯名 描述
Program 程序主體 整段代碼的主體
VariableDeclaration 變量聲明 聲明變量,比如 let const var
FunctionDeclaration 函數聲明 聲明函數,比如 function
ExpressionStatement 表達式語句 通常為調用一個函數,比如 console.log(1)
BlockStatement 塊語句 包裹在 {} 內的語句,比如 if (true) { console.log(1) }
BreakStatement 中斷語句 通常指 break
ContinueStatement 持續語句 通常指 continue
ReturnStatement 返回語句 通常指 return
SwitchStatement Switch 語句 通常指 switch
IfStatement If 控制流語句 通常指 if (true) {} else {}
Identifier 標識符 標識,比如聲明變量語句中 const a = 1 中的 a
ArrayExpression 數組表達式 通常指一個數組,比如 [1, 2, 3]
StringLiteral 字符型字面量 通常指字符串類型的字面量,比如 const a = '1' 中的 '1'
NumericLiteral 數字型字面量 通常指數字類型的字面量,比如 const a = 1 中的 1
ImportDeclaration 引入聲明 聲明引入,比如 import

為了快速瞭解,我們這篇以 JavaScript 文件為例,那麼解析與操作 JavaScript 文件,已經有了比較好用的輪子 -- jscodeshift,我們下面就利用 jscodeshift 來操作 AST

1、查找

這裏是一段十分簡易的代碼:

import React from 'react';
import { Button } from 'antd';

我們對比上面的 節點類型含義對照表 ,可以看出這是兩個 ImportDeclaration 語句

然後我們將這段代碼放到 AST 可視化工具中查看轉換成 AST 後的樣子:

這個時候我們有個小小的需求,那就是我想要獲取下面代碼塊中的導包源,也就是 from 後面的內容

import React from "react";
import { Button } from "antd";
import { moment } from "moment";

我們來看這段話的含義,代碼中我們通過引入 jscodeshift 來幫助我們解析和操作 AST 文件,然後在 API 中聲明瞭我們要查找元素的類型

這個時候我們可以打開控制枱運行 node find.js 來運行該腳本內容,可以看到控制枱成功的輸出了我們想要的結果!

react
antd
moment

接下來我們玩法進階,我們在下面代碼塊中除了看到有 import 語法,還定義了 name 屬性,那我們這個時候需求又來了, 我想獲取該 name 的值!這個時候要怎麼辦呢?

第一步我們需要查看 AST 結構,我們可以將文件體複製到我們的 AST 查看輔助工具上進行 AST 結構概覽:

可以看到我們想要的內容在 ArrayExpression 中的 elements中,那麼接下來我們在代碼中該如何操作呢?大家可以先進行嘗試~

答案如下:

我們先要找到 ArrayExpression 類型的元素,然後訪問該元素下的 elements 屬性,就會得到我們想要的值了!

張三
李四
王五
2、修改

我們上面已經實現了通過 AST 結構來查找我們想要的元素,下面我們就可以開始進行操作節點元素了!

首先先看如何修改,這時來了個需求,我們的 Button 組件名稱變了,換成了 Button01 ,那我們就得做出相應的修改

接下來我們繼續看以下文件,通過查看可以發現有些不同,這個時候多了 find API,而且這個API可以增加參數 { source: { value: "antd" } }

這個 API 的目的是隻查找 source = antdImportDeclaration 元素,然後進行替換,Button 命名的所在位置在 imported.name,因此我們相應修改該值即可

我們通過運行 node modify.js 便可以看到我們修改後的文件內容,想要使之生效,我們還需要將修改後的內容寫會該文件中,我們可以在文件最下方補上下面一段代碼:

fs.writeFileSync('./code/demo.js', root.toSource(), 'utf-8')

然後運行代碼,這個時候我們就可以發現 demo.js文件內容已經發生了修改。

import React from "react";
import { Button01 } from "antd";
import { moment } from "moment";

var name = ["張三", "李四", "王五"];
3、新增

有了查,改,接下來就輪到了了,增的話會比上面複雜些,因為我們需要將我們要新增的內容構建成 AST 結構,然後再往已有的 AST 結構中插入

老樣子,我們老朋友需求又來了,之前頁面中只用到了 antdButton 組件,那我們頁面這個時候還需要用到 antdSelect 組件

我們第一步就是要將我們要插入的內容構建成 AST 元素,我們先分析已有的 Button AST 結構長啥樣,然後依葫蘆畫瓢構建即可。

我們分析得到該結構的組成部分由 ImportSpecifierIdentifier 組成,ImportSpecifier 中包着 Identifier

那麼我們就可以得出我們要插入的內容結構為:

接下來就交給 jscodeshift 幫我們生成

$.importSpecifier($.identifier("Select"))

得到 AST 結構後我們還需要查看我們要插入的位置,回到之前的 AST 結構中

我們發現導入的資源組件內容都放在了 specifiers 屬性中,那我們就可以動手操作了,我們在項目中找到 create.js 文件

通過運行代碼,可以發現結果已經變成了我們修改後的內容。

import React from "react";
import { Button, Select } from "antd";
import { moment } from "moment";

var name = ["張三", "李四", "王五"];
4、刪除

講完查,改,增,最後就剩下我們拿手的

需求它又來了,頁面這個時候不需要 antd 組件了,也就是將 import { Button } from "antd"; 這句話移除

那就老規則,先找到 antd 這個元素所在的 AST,然後將它置為空即可

這個時候通過運行,就可以發現打印出來的內容已經沒有了關於antd 的引入信息了

import React from "react";
import { moment } from "moment";

var name = ["張三", "李四", "王五"];

到這裏我們就講完了關於 AST 的增刪改查操作


好了,以上便是本篇的所有內容,AST 是個很有用的工具,如果覺得對你有幫助的小夥伴不妨點個關注做個伴,便是對小菜最大的支持。不要空談,不要貪懶,和小菜一起做個吹着牛X做架構的程序猿吧~ 咱們下文再見!

今天的你多努力一點,明天的你就能少説一句求人的話!

我是小菜,一個和你一起變強的男人。 💋

微信公眾號已開啓,菜農曰,沒關注的同學們記得關注哦!

Add a new 评论

Some HTML is okay.