こわくない!今日からはじめるGo言語コード生成

  • 27
    いいね
  • 0
    コメント

はじめに

  • Go言語のできる人が「ルーティング定義からコントローラを自動生成している」とか言っているのを聞いていてすごい人はすごいなー、とか思っていたけど自分でツールを作ってみてコード生成とか意外とできるということがわかった。
  • なのでコード生成をやったことがない人向けにコード生成意外と大変じゃないよっていうのが伝わるといいなと思って手順をまとめてみる。

コード生成の大まかな手順

  1. ソースコードを読み込んで構文木データに変換する
  2. 構文木データから目的の構文データを検索する
  3. 構文データを使ってソースコードを生成する

ソースコードを読み込んで構文木データに変換する

  • 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/astInspect 関数の使い方をおぼえる
  • 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()))
}