背景
最近需要基於AST來做一些自動化工具,遂也需要針對這個神兵利器進行一下了解研究。本篇文章也準備只是簡單的講解一下以下兩個部分:
- 通過AST解析一個Go程式
- 然後通過Go的標準庫來對這個AST進行分析
AST
什麼是AST,其實就是抽象語法樹Abstract Syntax Tree的簡稱。它以樹狀的形式表現程式語言的語法結構,樹上的每個節點都表示原始碼中的一種結構。之所以說語法是“抽象”的,是因為這裡的語法並不會表示出真實語法中出現的每個細節。
主菜
開胃提示語
以下內容有點長,要不先去買點瓜子,邊磕邊看?
編譯過程
要講解相關AST部分,先簡單說一下我們知道的編譯過程:
- 詞法分析
- 語法分析
- 語義分析和中間程式碼產生
- 編譯器優化
- 目的碼生成 而我們現在要利用的正是Google所為我們準備的一套非常友好的詞法分析和語法分析工具鏈,有了它我們就可以造車了。
程式碼示例
在Golang官方文件中已經提供例項,本處就不把文件原始碼貼出來了,只放出部分用例
// This example shows what an AST looks like when printed for debugging.
func ExamplePrint() {
// src is the input for which we want to print the AST.
src := `
package main
func main() {
println("Hello, World!")
}
`
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
// Print the AST.
ast.Print(fset, f)
// Output:
// 0 *ast.File {
// 1 . Package: 2:1
// 2 . Name: *ast.Ident {
// 3 . . NamePos: 2:9
// 4 . . Name: "main"
// 5 . }
// 6 . Decls: []ast.Decl (len = 1) {
// 7 . . 0: *ast.FuncDecl {
// 8 . . . Name: *ast.Ident {
// 9 . . . . NamePos: 3:6
// 10 . . . . Name: "main"
// 11 . . . . Obj: *ast.Object {
// 12 . . . . . Kind: func
// 13 . . . . . Name: "main"
// 14 . . . . . Decl: *(obj @ 7)
// 15 . . . . }
// 16 . . . }
// 17 . . . Type: *ast.FuncType {
// 18 . . . . Func: 3:1
// 19 . . . . Params: *ast.FieldList {
// 20 . . . . . Opening: 3:10
// 21 . . . . . Closing: 3:11
// 22 . . . . }
// 23 . . . }
// 24 . . . Body: *ast.BlockStmt {
// 25 . . . . Lbrace: 3:13
// 26 . . . . List: []ast.Stmt (len = 1) {
// 27 . . . . . 0: *ast.ExprStmt {
// 28 . . . . . . X: *ast.CallExpr {
// 29 . . . . . . . Fun: *ast.Ident {
// 30 . . . . . . . . NamePos: 4:2
// 31 . . . . . . . . Name: "println"
// 32 . . . . . . . }
// 33 . . . . . . . Lparen: 4:9
// 34 . . . . . . . Args: []ast.Expr (len = 1) {
// 35 . . . . . . . . 0: *ast.BasicLit {
// 36 . . . . . . . . . ValuePos: 4:10
// 37 . . . . . . . . . Kind: STRING
// 38 . . . . . . . . . Value: "\"Hello, World!\""
// 39 . . . . . . . . }
// 40 . . . . . . . }
// 41 . . . . . . . Ellipsis: -
// 42 . . . . . . . Rparen: 4:25
// 43 . . . . . . }
// 44 . . . . . }
// 45 . . . . }
// 46 . . . . Rbrace: 5:1
// 47 . . . }
// 48 . . }
// 49 . }
// 50 . Scope: *ast.Scope {
// 51 . . Objects: map[string]*ast.Object (len = 1) {
// 52 . . . "main": *(obj @ 11)
// 53 . . }
// 54 . }
// 55 . Unresolved: []*ast.Ident (len = 1) {
// 56 . . 0: *(obj @ 29)
// 57 . }
// 58 }
}
複製程式碼
一看到上面的列印是不是有點頭暈?哈哈,我也是。沒想到一個簡單的hello world就能列印出這麼多東西,裡面其實隱藏了很多有趣的元素,比如函式、變數、評論、imports等等,那我們要如何才能從中提取出我們想要的資料呢?為達這個目的,我們需要用到Golang所為我們提供的go/parser
包:
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
複製程式碼
第一行引用了go/token
包,用來建立一個新的用於解析的原始檔FileSet。
然後我們使用的parser.ParseFile
返回的是一個ast.File
型別結構體(原始文件),然後回頭檢視上面的日誌列印,每個欄位元素的含義你也許已經霍然開朗了,結構體定義如下:
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
複製程式碼
好了,目前我們就是要利用這個結構體做一下小的程式碼示例,我們就來解析下面的這個檔案ast_traversal.go
:
package ast_demo
import "fmt"
type Example1 struct {
// Foo Comments
Foo string `json:"foo"`
}
type Example2 struct {
// Aoo Comments
Aoo int `json:"aoo"`
}
// print Hello World
func PrintHello(){
fmt.Println("Hello World")
}
複製程式碼
我們已經可以利用上面說到的ast.File
結構體去解析這個檔案了,比如利用f.Imports
列出所引用的包:
for _, i := range f.Imports {
t.Logf("import: %s", i.Path.Value)
}
複製程式碼
同樣的,我們可以過濾出其中的評論、函式等,如:
for _, i := range f.Comments {
t.Logf("comment: %s", i.Text())
}
for _, i := range f.Decls {
fn, ok := i.(*ast.FuncDecl)
if !ok {
continue
}
t.Logf("function: %s", fn.Name.Name)
}
複製程式碼
上面,獲取comment的方式和import類似,直接就能使用,而對於函式,則採用了*ast.FucDecl
的方式,此時,移步至本文最上層,檢視AST樹的列印,你就發現了Decls: []ast.Decl
是以陣列形式存放,且其中存放了多種型別的node,此處通過強制型別轉換的方式,檢測某個型別是否存在,存在的話則按照該型別中的結構進行列印。上面的方式已能滿足我們的基本需求,針對某種型別可以進行具體解析。
但是,凡是還是有個但是,哈哈,通過上面的方式來一個一個解析是不是有點麻煩?沒事,谷歌老爹通過go/ast
包給我們又提供了一個方便快捷的方法:
// 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)
}
複製程式碼
這個方法的大概用法就是:通過深度優先的方式,把整個傳遞進去的AST進行了解析,它通過呼叫f(node) 開始;節點不能為零。如果 f 返回 true,Inspect 會為節點的每個非零子節點遞迴呼叫f,然後呼叫 f(nil)。相關用例如下:
ast.Inspect(f, func(n ast.Node) bool {
// Find Return Statements
ret, ok := n.(*ast.ReturnStmt)
if ok {
t.Logf("return statement found on line %d:\n\t", fset.Position(ret.Pos()).Line)
printer.Fprint(os.Stdout, fset, ret)
return true
}
// Find Functions
fn, ok := n.(*ast.FuncDecl)
if ok {
var exported string
if fn.Name.IsExported() {
exported = "exported "
}
t.Logf("%sfunction declaration found on line %d: %s", exported, fset.Position(fn.Pos()).Line, fn.Name.Name)
return true
}
return true
})
複製程式碼
後記
至此,你手中的瓜子可能已經嗑完了,AST用處頗多,上面我們所講到的也只是AST其中的一小部分,很多底層相關分析工具都是基於它來進行語法分析進行,工具在手,然後要製造什麼藝術品就得看各位手藝人了。後續會陸續更新部分基於Go AST的小工具出來,希望自己能早日實現吧,哈哈?。
以下為上文中所用到的測試用例及使用AST針對結構體進行欄位解析的原始碼,我已提交至Github,如有興趣可以去看看