Go之底層利器-AST遍歷

瑪莎拉哥哥發表於2019-05-01

原文出處:dpjeep.com/gozhi-di-ce…

背景

最近需要基於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,如有興趣可以去看看

相關文章