GoのASTからソースコードを生成する
Goの標準ライブラリを用いてAST(抽象構文木)からソースコードを生成する方法のメモです。
ASTの取得
文字列からGoのASTを取得する例
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
src := `
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
`
// ソースファイル群を表すデータの作成
// ソースファイルデータにはファイル名やファイル内の構文の位置などの情報を持つ
// たとえばパッケージ単位でコードの解析を行う場合は同一ディレクトリのソースファイルをまとめて扱う必要があるのでソースファイル群という単位でソース情報を持っているものと思われる
fset := token.NewFileSet()
// ソースコードを構文木に変換
// 第二引数にファイル名を渡すとファイルを、第三引数にソースコードの文字列を渡すと文字列を変換する
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
ast.Print(fset, f)
go/parser
のParseFile
の戻り値がASTになっている。go/ast
のPrint
を利用して読みやすいように表示させている。
上記プログラムを実行するとこんな感じになる。
0 *ast.File {
1 . Package: ../data/sample.go:1:1
2 . Name: *ast.Ident {
3 . . NamePos: ../data/sample.go:1:9
4 . . Name: "main"
5 . }
6 . Decls: []ast.Decl (len = 2) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: ../data/sample.go:3:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: ../data/sample.go:3:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: ../data/sample.go:13:6
26 . . . . Name: "main"
27 . . . . Obj: *ast.Object {
28 . . . . . Kind: func
29 . . . . . Name: "main"
30 . . . . . Decl: *(obj @ 23)
31 . . . . }
32 . . . }
33 . . . Type: *ast.FuncType {
34 . . . . Func: ../data/sample.go:13:1
35 . . . . Params: *ast.FieldList {
36 . . . . . Opening: ../data/sample.go:13:10
37 . . . . . Closing: ../data/sample.go:13:11
38 . . . . }
39 . . . }
40 . . . Body: *ast.BlockStmt {
41 . . . . Lbrace: ../data/sample.go:13:13
42 . . . . List: []ast.Stmt (len = 1) {
43 . . . . . 0: *ast.ExprStmt {
44 . . . . . . X: *ast.CallExpr {
45 . . . . . . . Fun: *ast.SelectorExpr {
46 . . . . . . . . X: *ast.Ident {
47 . . . . . . . . . NamePos: ../data/sample.go:16:2
48 . . . . . . . . . Name: "fmt"
49 . . . . . . . . }
50 . . . . . . . . Sel: *ast.Ident {
51 . . . . . . . . . NamePos: ../data/sample.go:16:6
52 . . . . . . . . . Name: "Println"
53 . . . . . . . . }
54 . . . . . . . }
55 . . . . . . . Lparen: ../data/sample.go:16:13
56 . . . . . . . Args: []ast.Expr (len = 1) {
57 . . . . . . . . 0: *ast.BasicLit {
58 . . . . . . . . . ValuePos: ../data/sample.go:16:14
59 . . . . . . . . . Kind: STRING
60 . . . . . . . . . Value: "\"Hello, World!\""
61 . . . . . . . . }
62 . . . . . . . }
63 . . . . . . . Ellipsis: -
64 . . . . . . . Rparen: ../data/sample.go:16:29
65 . . . . . . }
66 . . . . . }
67 . . . . }
68 . . . . Rbrace: ../data/sample.go:17:1
69 . . . }
70 . . }
71 . }
72 . Scope: *ast.Scope {
73 . . Objects: map[string]*ast.Object (len = 1) {
74 . . . "main": *(obj @ 27)
75 . . }
76 . }
77 . Imports: []*ast.ImportSpec (len = 1) {
78 . . 0: *(obj @ 12)
79 . }
80 . Unresolved: []*ast.Ident (len = 1) {
81 . . 0: *(obj @ 46)
82 . }
83 }
ASTからコードを生成
先ほど取得したASTからソースコードを生成する例 とは言ってもめちゃくちゃ簡単。
import (
"bytes"
"go/format"
"go/token"
)
// NodeToCode ...
func NodeToCode(node ast.Node) string {
buf := new(bytes.Buffer)
_ = format.Node(buf, token.NewFileSet(), node)
return buf.String()
}
go/format
のNode
関数に書き込み先のバッファとFileSet構造体、対象のASTノードを渡してあげるだけ。
これだけでASTからソースコードが生成できる。
上記ではより利用しやすいように関数にしてみた。
上記NodeToCode
関数に、先ほど取得したASTノードを渡してみると以下のようなソースコードが得られる。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
最後に
以前からGoのコード生成は楽だという話を聞いてはいたが衝撃的なくらいに簡単だった。
ASTそのものの編集にはgo/ast
のInspect
関数と "golang.org/x/tools/go/ast/astutil"
の関数群を利用すればだいぶ楽そうだ。