Go 中 ast 抽象語法樹簡述
1、什麼叫語法樹?
我們知道人類語言上,無論什麼語種,都會有「主語」「動詞」「賓語」「標點符號」來描述一個現實世界所發生的事件。而在計算機編程語言上,無論什麼語種,都會有「類型」「運算符」「流程語句」「函數」「對象」等概念來表達計算機中存在內存中的 0 和 1,以及背後運算與邏輯。目前世界上大概有 700 種編程語言,而其中比較流行的有 50 種左右。不同的語言,都會配之不同的語法分析器,而語法分析器是把源代碼作爲字符串讀入、解析,並建立語法樹的程序。語法的設計和語法分析器的實現是決定語言外在表現的重要因素。
在計算機科學中,抽象語法樹(abstract syntax tree 或者縮寫爲 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這裏特指編程語言的源代碼。樹上的每個節點都表示源代碼中的一種結構。之所以說語法是「抽象」的,是因爲這裏的語法並不會表示出真實語法中出現的每個細節。
Wiki:什麼是語法樹?
2、Golang 的 AST.
2.1. golang 的 ast 定義
抽象語法樹由節點 (Node) 構成,從源代碼中 (go/ast/ast.go) 我們可以看出,Golang 的 AST 主要由三種節點構成:分別是表達式和類型節點 (Expressions and type nodes)、語句節點(statementnodes) 和聲明節點(declaration nodes)。所有的節點都包含標識其在源代碼中開頭和結尾位置的信息。
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
代碼中看到,所有的 AST 節點都實現了 ast.Node 接口,它只是返回 AST 中的一個位置。
另外,還有 3 個主要接口實現了 ast.Node。
ast.Expr - 代表表達式和類型的節點
ast.Stmt - 代表語句節點
ast.Decl - 代表聲明節點
2.2. golang 中 ast 實例
代碼清單:
package main
import "fmt"
func main() {
var msg = "Hello World!"
fmt.Println(msg)
}
對應的 ast 語法樹如下(篇幅過長,有刪減):
*ast.File {
. Doc: nil
. Package: foo:1:1
. Name: *ast.Ident {
. . NamePos: foo:1:9
. . Name: "main"
. }
. Decls: []ast.Decl (len = 2) {
. . 0: *ast.GenDecl {
. . . TokPos: foo:3:1
. . . Tok: import
. . . Specs: []ast.Spec (len = 1) {
. . . . 0: *ast.ImportSpec {
. . . . . Path: *ast.BasicLit {
. . . . . . ValuePos: foo:3:8
. . . . . . Kind: STRING
. . . . . . Value: "\"fmt\""
. . . . . }
. . . . }
. . . }
. . }
. . 1: *ast.FuncDecl {
. . . Name: *ast.Ident {
. . . . NamePos: foo:5:6
. . . . Name: "main"
. . . . Obj: *ast.Object {
. . . . . Kind: func
. . . . . Name: "main"
. . . . }
. . . }
. . . Type: *ast.FuncType {
. . . . Func: foo:5:1
. . . . Params: *ast.FieldList {
. . . . . Opening: foo:5:10
. . . . . Closing: foo:5:11
. . . . }
. . . }
. . . Body: *ast.BlockStmt {
. . . . Lbrace: foo:5:13
. . . . List: []ast.Stmt (len = 1) {
. . . . . 0: *ast.ExprStmt {
. . . . . . X: *ast.CallExpr {
. . . . . . . Fun: *ast.SelectorExpr {
. . . . . . . . X: *ast.Ident {
. . . . . . . . . NamePos: foo:6:2
. . . . . . . . . Name: "fmt"
. . . . . . . . }
. . . . . . . . Sel: *ast.Ident {
. . . . . . . . . NamePos: foo:6:6
. . . . . . . . . Name: "Println"
. . . . . . . . }
. . . . . . . }
. . . . . . . Args: []ast.Expr (len = 1) {
. . . . . . . . 0: *ast.BasicLit {
. . . . . . . . . ValuePos: foo:6:14
. . . . . . . . . Kind: STRING
. . . . . . . . . Value: "\"Hello World!\""
. . . . . . . . }
. . . . . . . }
. . . . . . }
. . . . . }
. . . . }
. . . }
. . }
. }
. Scope: *ast.Scope {
. . Objects: map[string]*ast.Object (len = 1) {
. . . "main": *(obj @ 35)
. . }
. }
}
Ps: 這裏給大家提供一個 golang 的 ast 在線轉換鏈接,有興趣的小夥伴可以試試將自己的 go 代碼輸入看看。https://yuroyoro.github.io/goast-viewer/index.html
2.3. Golang 的 ast 分析
既然是一棵樹,自然可以遍歷。Golang 官網已經爲我們提供去遍歷這棵樹的方法。
// Inspect traverses an AST in depth-first order: It starts by calling// f(node); node must not be nil. If f returns true, Inspect invokes f// recursively for each of the non-nil children of node, followed by a// call of f(nil).//
func Inspect(node Node, f func(Node) bool) {
Walk(inspector(f), node)
}
代碼是 golang 官方提供的代碼 go/ast,從註釋中我們知道 Inspect 函數以深度遍歷的方式遍歷 AST,通過調用 f(node) 開始,節點不能爲零。如果 f 返回 true,Inspect 會爲節點的每個非零子節點遞歸調用 f,然後調用 f(nil)。
這裏遍歷的內容 f 即是我們前面轉換得到的語法樹。
*ast.File
第一個要訪問的節點是 * ast.File,它是所有 AST 節點的根。它只實現了 ast.Node 接口。
ast.File 有引用包名、導入聲明和函數聲明作爲子節點。
*ast.Ident - 包名
我們可以看到包名是 main,在第一行聲明。
*ast.GenDecl - 導入聲明
從這我們可以看到,ast.GenDecl 代表除函數以外的所有聲明,即 import、const、var 和 type。
Tok 代表一個詞性標記 -- 它指定了聲明的內容(import 或 const 或 type 或 var)。
這個 AST 節點告訴我們,import 聲明在 foo.go 的第 3 行。
讓我們從上到下深入地看一下 ast.GenDecl 的下一個節點 * ast.ImportSpec。
*ast.FuncDecl - 函數聲明
一個 ast.FuncDecl 節點代表一個函數聲明,但它只實現了 ast.Node 接口。上述圖片中我們看到一個 ast.FuncDecl 下面還有其餘的類型,例如 * ast.Ident/*ast.Object 等等。
如此多的類型我們不可能全部記住,因爲官方文件已經幫我們全部記住了,我們只需按需查詢即可:go/ast 的全部節點類型:https://pkg.go.dev/go/ast#
3. 我們可以利用語法樹做些什麼?
看到這裏你可能會問,知道語法樹又有什麼用呢?跟我日常編寫代碼貌似半毛錢關係都沒有。其實語法樹還是很有用的,比如代碼語法的檢查、代碼風格的檢查、代碼的格式化、代碼的高亮、代碼錯誤提示、代碼自動補全等等
如使用語言的 Lint 工具對代碼錯誤或風格的檢查,發現一些潛在的錯誤
IDE 的錯誤提示、格式化、高亮、自動補全等
代碼混淆壓縮
UglifyJS2 等
優化變更代碼,改變代碼結構使達到想要的結構
代碼打包工具 webpack、rollup 等等
CommonJS、AMD、CMD、UMD 等代碼規範之間的轉化
CoffeeScript、TypeScript、JSX 等轉化爲原生 Javascript
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/YWKj94hPjfHrGofPWuEVGg