Go之底層利器-AST遍歷

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

背景

最近須要基於AST來作一些自動化工具,遂也須要針對這個神兵利器進行一下了解研究。本篇文章也準備只是簡單的講解一下如下兩個部分:git

  • 經過AST解析一個Go程序
  • 而後經過Go的標準庫來對這個AST進行分析

AST

什麼是AST,其實就是抽象語法樹Abstract Syntax Tree的簡稱。它以樹狀的形式表現編程語言的語法結構,樹上的每一個節點都表示源代碼中的一種結構。之因此說語法是「抽象」的,是由於這裏的語法並不會表示出真實語法中出現的每一個細節。github

主菜

開胃提示語

如下內容有點長,要不先去買點瓜子,邊磕邊看?golang

編譯過程

要講解相關AST部分,先簡單說一下咱們知道的編譯過程:編程

  • 詞法分析
  • 語法分析
  • 語義分析和中間代碼產生
  • 編譯器優化
  • 目標代碼生成 而咱們如今要利用的正是Google所爲咱們準備的一套很是友好的詞法分析和語法分析工具鏈,有了它咱們就能夠造車了。

代碼示例

在Golang官方文檔中已經提供實例,本處就不把文檔源碼貼出來了,只放出部分用例json

// 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.goide

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,若有興趣能夠去看看

相關文章
相關標籤/搜索