はじめに
- Go言語のできる人が「ルーティング定義からコントローラを自動生成している」とか言っているのを聞いていてすごい人はすごいなー、とか思っていたけど自分でツールを作ってみてコード生成とか意外とできるということがわかった。
- なのでコード生成をやったことがない人向けにコード生成意外と大変じゃないよっていうのが伝わるといいなと思って手順をまとめてみる。
コード生成の大まかな手順
- ソースコードを読み込んで構文木データに変換する
- 構文木データから目的の構文データを検索する
- 構文データを使ってソースコードを生成する
ソースコードを読み込んで構文木データに変換する
-
go/parser
などを使ってソースコードの文字列を構文木データに変換する。
構文木データとは
- 構文木データとはソースコードをツリー状のデータ構造に置き換えたもので例えば以下のようなもの。
元になるソースコード
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
ソースコードを変換してできた構文木データ
- コメント部分は追記したもの
// ルートノードはファイル
0 *ast.File {
// パッケージ宣言や...
1 . Package: 2:1
// ファイル名などの情報があり...
2 . Name: *ast.Ident {
3 . . NamePos: 2:9
4 . . Name: "main"
5 . }
// import 宣言や関数宣言などが続く
6 . Decls: []ast.Decl (len = 2) {
// import 宣言
7 . . 0: *ast.GenDecl {
8 . . . TokPos: 3:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: 3:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
// main 関数宣言
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: 4: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: 4:1
35 . . . . Params: *ast.FieldList {
36 . . . . . Opening: 4:10
37 . . . . . Closing: 4:11
38 . . . . }
39 . . . }
// main 関数のブロック部分
40 . . . Body: *ast.BlockStmt {
41 . . . . Lbrace: 4: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: 5:2
48 . . . . . . . . . Name: "fmt"
49 . . . . . . . . }
50 . . . . . . . . Sel: *ast.Ident {
51 . . . . . . . . . NamePos: 5:6
52 . . . . . . . . . Name: "Println"
53 . . . . . . . . }
54 . . . . . . . }
55 . . . . . . . Lparen: 5:13
56 . . . . . . . Args: []ast.Expr (len = 1) {
57 . . . . . . . . 0: *ast.BasicLit {
58 . . . . . . . . . ValuePos: 5:14
59 . . . . . . . . . Kind: STRING
60 . . . . . . . . . Value: "\"Hello, World!\""
61 . . . . . . . . }
62 . . . . . . . }
63 . . . . . . . Ellipsis: -
64 . . . . . . . Rparen: 5:29
65 . . . . . . }
66 . . . . . }
67 . . . . }
68 . . . . Rbrace: 6: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 }
ソースコードを構文木に変換するコード
- コメントにあるが、ソースコードまたはソースコードの記載されたファイル名を使って、ソースファイル群データと構文木データの作成を行う
- 以後ファイル群データと構文木データはセットで扱う
- Playground で実行できるのでソースコードを変えて試してみるとなんとなくわかってくるかも
package main
import (
"go/ast"
"go/parser"
"go/token"
)
func main() {
// ソースコード
src := `
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
`
// ソースファイル群を表すデータの作成
// ソースファイルデータにはファイル名やファイル内の構文の位置などの情報を持つ
// たとえばパッケージ単位でコードの解析を行う場合は同一ディレクトリのソースファイルをまとめて扱う必要があるのでソースファイル群という単位でソース情報を持っているものと思われる
fset := token.NewFileSet()
// ソースコードを構文木に変換
// 第二引数にファイル名を渡すとファイルを、第三引数にソースコードの文字列を渡すと文字列を変換する
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
// 構文木を見やすく表示する
// 前記の構文木データはこれで表示したもの
ast.Print(fset, f)
}
おまけ
-
go/parser
を使うとGoの仕様に基づいて文字列をパースして構文木に変換することができるが、goyacc
を使うと自分でパースするルールの定義と構文木の定義と構文木の作成ができるので、DSL言語の作成や独自形式の設定ファイルの作成なんかもできるようになる。はず。
構文木データから目的の構文データを検索する
- 構文木データから任意の目的の構文データを検索するために、とりあえず
go/ast
のInspect
関数の使い方をおぼえる -
func Inspect(node Node, f func(Node) bool)
はnode
で構文木データを受け取ってf
の関数をルートノードから順番に各ノードに適用する -
f
の中では受け取ったast.Node
を任意の型にキャストして、成功したものを目的のデータとして扱うことができる - 以下のコードは構文木データの中から構造体宣言のフィールド定義部分だけ検索して表示する例
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
src := `
package main
type MyStruct struct {
N int
S string
}
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
// 構文木データの中から構造体宣言のフィールド定義部分だけ検索して表示する
case *ast.Field:
// ここでは ast.Print だけしているが外部の変数に入れておけばフィールド定義に対応したコードの生成などに利用できる
ast.Print(fset, x)
fmt.Println("")
}
return true
})
}
構文データを使ってソースコードを生成する
- 前段で取得した構文データを使ってソースコードの生成をする
-
fmt.Sprintf
を使ったりtext/template
を使ったり実装は様々だがgo/format
を使うとフォーマットしてくれるしコンパイルエラーがあれば教えてもらえる -
以下のコードは構造体のフィールド名で定数を定義した何の意味もないソースコードを生成する例
- generator のコードはほぼ stringer パッケージのコピー
package main
import (
"log"
"fmt"
"bytes"
"go/ast"
"go/token"
"go/parser"
"go/format"
)
type generator struct {
buf *bytes.Buffer
}
func (g *generator) printf(format string, args ...interface{}) {
fmt.Fprintf(g.buf, format, args...)
}
func (g *generator) format() []byte {
src, err := format.Source(g.buf.Bytes())
if err != nil {
// Should never happen, but can arise when developing this code.
// The user can compile the output to see the error.
log.Printf("warning: internal error: invalid Go generated: %s", err)
log.Printf("warning: compile the package to analyze the error")
return g.buf.Bytes()
}
return src
}
func main() {
src := `
package main
type MyStruct struct {
N int
S string
}
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
g := &generator{buf: new(bytes.Buffer)}
g.printf("package main\n")
ast.Inspect(f, func(n ast.Node) bool {
switch x := n.(type) {
case *ast.Field:
g.printf("const %s = 1\n", x.Names[0].Name)
}
return true
})
fmt.Println(string(g.format()))
}