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