はじめに
Goでアプリケーションを開発していると、
go generate
でコードを生成することがよくあると思います。大抵の場合は、コードを解析するのに抽象構文木(Abstract Syntax Tree)が用いられ、そのためにGoでは
ast
パッケージが提供されています。
最近、コードを自動生成する処理を実装する必要に迫られてきたため、
ast
パッケージとそれを使ったコードの解析について調べてみました。
抽象構文木とは
抽象構文木は
Abstract Syntax Tree
の訳で、プログラムの文法構造を木構造で表したものを指します。
詳細は省きますので、詳しく知りたい方はこちら等をご覧ください
ASTでコードを解析する
サンプルコードを解析する
ASTで解析するためのサンプルコードとして、以下のファイルを用意しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | package src import "fmt" // Book represents a book. type Book struct { Title string Author string Pages int } // String returns a string representation of a Book. func (b *Book) String() string { fmt.Println("String()") return b.Title + " by " + b.Author } |
まず初めに、このファイルを解析してパッケージ名、インポートしたパッケージ、定義された構造体の名前と変数名をそれぞれ出力してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package main import ( "fmt" "go/ast" "go/parser" "go/token" ) func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "src/book.go", nil, parser.ParseComments) if err != nil { panic(err) } fmt.Printf("package: %+v\n", f.Name) // package: src for _, object := range f.Scope.Objects { fmt.Printf("object: %s\n", object.Name) // object: Book for _, filed := range object.Decl.(*ast.TypeSpec).Type.(*ast.StructType).Fields.List { fmt.Printf("field: %+v\n", filed.Names[0].Name) // field: Title, field: Author, field: Pages } } for _, importSpec := range f.Imports { fmt.Printf("import: %+v\n", importSpec.Path.Value) // import: "fmt" } for _, comment := range f.Comments { fmt.Printf("Comment: %+v", comment.Text()) // Comment: // Book represents a book. } } |
f, err := parser.ParseFile(fset, "src/book.go", nil, parser.ParseComments)
で対象のファイルを
ast,File
に変換しています。
このコードを実行した際の出力は以下の通りです。
1 2 3 4 5 6 7 8 | package: src object: Book field: Title field: Author field: Pages import: "fmt" Comment: Book represents a book. Comment: String returns a string representation of a Book. |
上記の通り、パッケージ名は
src
、インポートしているパッケージは
fmt
、そして、
Book
構造体が定義されていて、
Title
、
Author
、
Pages
フィールドがあることがわかります。
このファイルを解析すると第一階層には3つのNodeがあります、それぞれのNodeが表しているものは以下の通りです。
- import文
- Book構造体
- Book構造体のStringメソッド
import文の部分はシンプルなので飛ばして、構造体の部分から見ていきます。
構造体の木構造を確認する
構造体は、
ast.GenDecl
の中の
ast.StructType
で表されています。以下はこの構造体と関連する構造体の一部を抜粋したものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | type StructType struct { Struct token.Pos // position of "struct" keyword Fields *FieldList // list of field declarations Incomplete bool // true if (source) fields are missing in the Fields list } type FieldList struct { Opening token.Pos // position of opening parenthesis/brace/bracket, if any List []*Field // field list; or nil Closing token.Pos // position of closing parenthesis/brace/bracket, if any } type Field struct { Doc *CommentGroup // associated documentation; or nil Names []*Ident // field/method/(type) parameter names; or nil Type Expr // field/method/parameter type; or nil Tag *BasicLit // field tag; or nil Comment *CommentGroup // line comments; or nil } type Expr interface { Node exprNode() } 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 } |
BookをFieldListで表すと以下のようになります(最低限のみで簡略化しています)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | ast.GenDecl{ Tok: token.TYPE, Specs: []ast.Spec{ &ast.TypeSpec{ Name: ast.NewIdent("Book"), Type: &ast.StructType{ Fields: &ast.FieldList{ List: []*ast.Field{ { Names: []*ast.Ident{ { Name: "Title", }, }, Type: &ast.Ident{ Name: "string", }, }, { Names: []*ast.Ident{ { Name: "Author", }, }, Type: &ast.Ident{ Name: "string", }, }, { Names: []*ast.Ident{ { Name: "Pages", }, }, Type: &ast.Ident{ Name: "int", }, }, }, }, }, }, }, } |
このように、Namesの中のNameにフィールド名が、Typeの中のNameに型の名前が入っています。
ちなみに、以下のように構造体の定義を
type()
でまとめると、
ast.GenDecl.Specs
の要素が複数になります。
1 2 3 4 5 6 7 8 9 10 11 | type ( Book struct { Title string Author string Pages int } User struct { Name string } ) |
メソッドの木構造を確認する
Bookには
(b *Book) String() string
メソッドが生えていますが、
ast.StructType
には含まれていませんでした。最初に説明した通り、第一階層のBookと別のNodeに含まれており、
ast.FuncDecl
の中の
ast.FuncType
で表されています(ただし、
receiver
の情報は
ast.FuncDecl
に含まれています)
以下はこの構造体と関連する構造体の一部を抜粋したものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | type FuncDecl struct { Doc *CommentGroup // associated documentation; or nil Recv *FieldList // receiver (methods); or nil (functions) Name *Ident // function/method name Type *FuncType // function signature: type and value parameters, results, and position of "func" keyword Body *BlockStmt // function body; or nil for external (non-Go) function } type FuncType struct { Func token.Pos // position of "func" keyword (token.NoPos if there is no "func") TypeParams *FieldList // type parameters; or nil Params *FieldList // (incoming) parameters; non-nil Results *FieldList // (outgoing) results; or nil } |
StringメソッドをFuncTypeで表すと以下のようになります(最低限のみで簡略化しています)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 | ast.FuncDecl{ Recv: &ast.FieldList{ List: []*ast.Field{ { Names: []*ast.Ident{ { Name: "b", }, }, Type: &ast.StarExpr{ Star: token.Pos(180), X: &ast.Ident{ Name: "Book", }, }, }, }, }, Type: &ast.FuncType{ Results: &ast.FieldList{ List: []*ast.Field{ { Type: &ast.Ident{ Name: "string", }, }, }, }, }, Body: &ast.BlockStmt{ List: []ast.Stmt{ &ast.ExprStmt{ X: &ast.CallExpr{ Fun: &ast.SelectorExpr{ X: &ast.Ident{ Name: "fmt", }, Sel: &ast.Ident{ Name: "Println", }, }, Args: []ast.Expr{ &ast.BasicLit{ Kind: token.STRING, Value: "\"String()\"", }, }, }, }, &ast.ReturnStmt{ Results: []ast.Expr{ &ast.BinaryExpr{ X: &ast.BinaryExpr{ X: &ast.BinaryExpr{ X: &ast.SelectorExpr{ X: &ast.Ident{ Name: "b", }, Sel: &ast.Ident{ Name: "Title", }, }, Op: token.ADD, Y: &ast.BasicLit{ Kind: token.STRING, Value: "\" by \"", }, }, Op: token.ADD, Y: &ast.SelectorExpr{ X: &ast.Ident{ Name: "b", }, Sel: &ast.Ident{ Name: "Author", }, }, }, }, }, }, }, }, } |
このようにかなり長くなってしまいましたが、
Recv
の中の
Field[0].Names.Name
にreceiver変数名が、
Field[0].Type.X.Name
にreceiverの型の名前が入っています。
メソッド名は
Name.Name
にあり、返り値の型は
Type.Results.List[0].Type.Name
にあります。
return文は
Body.List[1].Results
に表されており、その中の各要素がそれぞれ
b.Title
、
+ " by " +
、
b.Author
を表しています。
任意の対象を捜索する
ASTから特定の対象を見つけるには、
astutil.Apply
メソッドを使ってすべてのNodeを再帰的に探索します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | package main import ( "fmt" "go/ast" "go/parser" "go/token" "golang.org/x/tools/go/ast/astutil" ) func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "src/book.go", nil, parser.AllErrors) if err != nil { panic(err) } astutil.Apply(f, nil, func(c *astutil.Cursor) bool { node := c.Node() switch x := node.(type) { case *ast.FuncDecl: fmt.Printf("func: %+v\n", x.Name.Name) // func: String return false } return true }) } |
また、
astutil.Cursor
の
Replace
メソッドを使うことで、対象のNodeを置き換える事もできます。
1 2 3 4 5 6 7 8 9 10 11 12 | astutil.Apply(f, nil, func(c *astutil.Cursor) bool { node := c.Node() switch x := node.(type) { case *ast.FuncDecl: x2 := *x x2.Name.Name = "String2" c.Replace(&x2) fmt.Printf("func: %+v\n", x.Name.Name) // func: String2 return false } return true }) |
ASTをファイルに反映する
最後に、ASTをもとにファイルに出力する方法を紹介します。
もともとの
book.go
の
String
メソッドを一部書き換えて、
book2.go
に出力してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package main import ( "fmt" "go/ast" "go/format" "go/parser" "go/token" "os" "golang.org/x/tools/go/ast/astutil" ) func main() { fset := token.NewFileSet() f, err := parser.ParseFile(fset, "src/book.go", nil, parser.AllErrors) if err != nil { panic(err) } astutil.Apply(f, nil, func(c *astutil.Cursor) bool { node := c.Node() switch x := node.(type) { case *ast.FuncDecl: x2 := *x x2.Name.Name = "String2" c.Replace(&x2) fmt.Printf("func: %+v\n", x.Name.Name) // func: String2 return false } return true }) fp, err := os.Create("src/book2.go") if err != nil { panic(err) } defer fp.Close() if err := format.Node(fp, fset, f); err != nil { panic(err) } } |
出力されたファイルは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | package src import "fmt" type Book struct { Title string Author string Pages int } func (b *Book) String2() string { fmt.Println("String()") return b.Title + " by " + b.Author } |
string
メソッドの代わりに
string2
メソッドになっています。
さいごに
go generate
によるファイル生成を行うために、
ast
パッケージを使ったファイルの解析と変更について調べてみました。これを応用することで、定型的なコードを自動生成できるようになれば良いなと思います。