SYM's Tech Knowledge Index & Creation Records

「INPUT:OUTPUT=1:1以上」を掲げ構築する Tech Knowledge Stack and Index. by SYM@設計者足るため孤軍奮闘する IT Engineer.

golang AST & Jennifer によるコード自動生成

golang AST & Jennifer によるコード自動生成

モチベーション:以下と似ている

ref: entity からコード自動生成した話

ベースとなるソースファイルから AST 取得し必要情報抽出 ⇒ jennifer にてソース自動生成

template を使わない理由

  • template と埋め込むコードの2つを管理するのが手間なため(コードのみで済むのは1つの利点に思う)
  • 特殊ケース等あり、無理に共通化しようとすると煩雑化しやすいし、template 分けるにしても物がどんどん増えて、大変になりそうなイメージがあるため (物が増えてくるとこの辺のバランスとるのが大変になりそう。コードのみで済むなら1ケース1ソースにし共通化できる部分は外出しして管理しやすいと思われる)

AST(抽象構文木) 取得/解析コード

ソースファイルの情報を取得可能

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func Parse(filename string) error {
    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, filename, nil, parser.Mode(0))
    if err != nil {
        return err
    }
    // for _, d := range f.Decls {  // 1ファイルの全情報表示
    //  ast.Print(fset, d)
    //  fmt.Println()
    // }

    fieldNames := []string{}
    ast.Inspect(f, func(node ast.Node) bool {
        t, ok := node.(*ast.TypeSpec)
        if !ok {
            return true
        }

        st, ok := t.Type.(*ast.StructType)
        if !ok {
            return true
        }
        for _, field := range st.Fields.List {
            ast.Print(fset, field)
            fmt.Println()

            for _, nameNode := range field.Names {  // フィールド名取得
                fieldNames = append(fieldNames, nameNode.Name)
            }
        }
        return true
    })
    fmt.Print(fieldNames)
    return nil
}

解析結果

以下サンプルを解析した結果

type Move struct {
    id                 identifier.MoveId
    name               string
    description        *string  // *stringとするのはBatだがサンプルとして試す
    effects            *battles.Effects
    usedMember         []*Member
    leanableCharacters []Character
    leanablePokemons   []*identifier.PokemonId
    mixin.UpdateTimes
}

AST 上では

  • 1 フィールドの情報は、*ast.Field に格納されている
    • *ast.Field は Names と Type を持つ
    • a, b string のように 1 フィールドに複数変数定義した場合は全て Names に格納される
  • 各フィールドの型(Type)情報はそれぞれ以下の構造体で表現されている
    • 外部パッケージの型(例:identifier.MoveId): *ast.SelectorExpr
    • ポインタ型 (例:*string): *ast.StarExpr
    • 配列/スライス (例:Character): *ast.ArrayType
    • 末端の要素 (string、identifier、MoveId 等): *ast.Ident
    • 複合パターンも上記の組み合わせで表現: *identifier.PokemonId の場合 *ast.ArrayType -> *ast.StarExpr -> *ast.SelectorExpr
    • 埋め込みの場合は、Names がないだけで Type は構造に変わりなし

実際の結果

  • id identifier.MoveId
0  *ast.Field {
1  .  Names: []*ast.Ident (len = 1) {
2  .  .  0: *ast.Ident {
3  .  .  .  NamePos: <full path>\move.go:12:2
4  .  .  .  Name: "id"
5  .  .  .  Obj: *ast.Object {
6  .  .  .  .  Kind: var
7  .  .  .  .  Name: "id"
8  .  .  .  .  Decl: *(obj @ 0)
9  .  .  .  }
10  .  .  }
11  .  }
12  .  Type: *ast.SelectorExpr {
13  .  .  X: *ast.Ident {
14  .  .  .  NamePos: <full path>\move.go:12:16
15  .  .  .  Name: "identifier"
16  .  .  }
17  .  .  Sel: *ast.Ident {
18  .  .  .  NamePos: <full path>\move.go:12:27
19  .  .  .  Name: "HeldItemId"
20  .  .  }
21  .  }
22  }
  • name string
0  *ast.Field {
1  .  Names: []*ast.Ident (len = 1) {
2  .  .  0: *ast.Ident {
3  .  .  .  NamePos: <full path>\move.go:13:2
4  .  .  .  Name: "name"
5  .  .  .  Obj: *ast.Object {
6  .  .  .  .  Kind: var
7  .  .  .  .  Name: "name"
8  .  .  .  .  Decl: *(obj @ 0)
9  .  .  .  }
10  .  .  }
11  .  }
12  .  Type: *ast.Ident {
13  .  .  NamePos: <full path>\move.go:13:16
14  .  .  Name: "string"
15  .  }
16  }
  • description *string
     0  *ast.Field {
     1  .  Names: []*ast.Ident (len = 1) {
     2  .  .  0: *ast.Ident {
     3  .  .  .  NamePos: <full path>/move.go:14:2
     4  .  .  .  Name: "description"
     5  .  .  .  Obj: *ast.Object {
     6  .  .  .  .  Kind: var
     7  .  .  .  .  Name: "description"
     8  .  .  .  .  Decl: *(obj @ 0)
     9  .  .  .  }
    10  .  .  }
    11  .  }
    12  .  Type: *ast.StarExpr {
    13  .  .  Star: <full path>/move.go:14:14
    14  .  .  X: *ast.Ident {
    15  .  .  .  NamePos: <full path>/move.go:14:15
    16  .  .  .  Name: "string"
    17  .  .  }
    18  .  }
    19  }
  • effects *battles.Effects
0  *ast.Field {
1  .  Names: []*ast.Ident (len = 1) {
2  .  .  0: *ast.Ident {
3  .  .  .  NamePos: <full path>\move.go:15:2
4  .  .  .  Name: "battleEffects"
5  .  .  .  Obj: *ast.Object {
6  .  .  .  .  Kind: var
7  .  .  .  .  Name: "battleEffects"
8  .  .  .  .  Decl: *(obj @ 0)
9  .  .  .  }
10  .  .  }
11  .  }
12  .  Type: *ast.StarExpr {
13  .  .  Star: <full path>\move.go:15:16
14  .  .  X: *ast.SelectorExpr {
15  .  .  .  X: *ast.Ident {
16  .  .  .  .  NamePos: <full path>\move.go:15:17
17  .  .  .  .  Name: "battles"
18  .  .  .  }
19  .  .  .  Sel: *ast.Ident {
20  .  .  .  .  NamePos: <full paht>\move.go:15:25
21  .  .  .  .  Name: "BattleEffects"
22  .  .  .  }
23  .  .  }
24  .  }
25  }
  • ポインタ配列 usedMember []*Member
0  *ast.Field {
1  .  Names: []*ast.Ident (len = 1) {
2  .  .  0: *ast.Ident {
3  .  .  .  NamePos: <full path>\move.go:4:2
4  .  .  .  Name: "items"
5  .  .  .  Obj: *ast.Object {
6  .  .  .  .  Kind: var
7  .  .  .  .  Name: "items"
8  .  .  .  .  Decl: *(obj @ 0)
9  .  .  .  }
10  .  .  }
11  .  }
12  .  Type: *ast.ArrayType {
13  .  .  Lbrack: <full path>\move.go:4:8
14  .  .  Elt: *ast.StarExpr {
15  .  .  .  Star: <full path>\move.go:4:10
16  .  .  .  X: *ast.Ident {
17  .  .  .  .  NamePos: <full path>\move.go:4:11
18  .  .  .  .  Name: "User"
19  .  .  .  }
20  .  .  }
21  .  }
22  }
  • 配列 leanableCharacters []Character
0  *ast.Field {
1  .  Names: []*ast.Ident (len = 1) {
2  .  .  0: *ast.Ident {
3  .  .  .  NamePos: <full path>\move.go:14:2
4  .  .  .  Name: "leanablePokemons"
5  .  .  .  Obj: *ast.Object {
6  .  .  .  .  Kind: var
7  .  .  .  .  Name: "leanablePokemons"
8  .  .  .  .  Decl: *(obj @ 0)
9  .  .  .  }
10  .  .  }
11  .  }
12  .  Type: *ast.ArrayType {
13  .  .  Lbrack: <full path>\move.go:14:20
14  .  .  Elt: *ast.Ident {
15  .  .  .  NamePos: <full path>\move.go:14:22
16  .  .  .  Name: "Pokemon"
17  .  .  }
18  .  }
19  }
  • ポインタ配列: leanablePokemons []*indetifier.PokemonId
0  *ast.Field {
1  .  Names: []*ast.Ident (len = 1) {
2  .  .  0: *ast.Ident {
3  .  .  .  NamePos: <full path>\move.go:16:2
4  .  .  .  Name: "leanablePokemons"
5  .  .  .  Obj: *ast.Object {
6  .  .  .  .  Kind: var
7  .  .  .  .  Name: "leanablePokemons"
8  .  .  .  .  Decl: *(obj @ 0)
9  .  .  .  }
10  .  .  }
11  .  }
12  .  Type: *ast.ArrayType {
13  .  .  Lbrack: <full path>\move.go:16:19
14  .  .  Elt: *ast.StarExpr {
15  .  .  .  Star: <full path>\move.go:16:21
16  .  .  .  X: *ast.SelectorExpr {
17  .  .  .  .  X: *ast.Ident {
18  .  .  .  .  .  NamePos: <full path>\move.go:16:22
19  .  .  .  .  .  Name: "identifier"
20  .  .  .  .  }
21  .  .  .  .  Sel: *ast.Ident {
22  .  .  .  .  .  NamePos: <full path>\move.go:16:33
23  .  .  .  .  .  Name: "PokemonId"
24  .  .  .  .  }
25  .  .  .  }
26  .  .  }
27  .  }
28  }
  • 埋め込み mixin.UpdateTimes
0  *ast.Field {
1  .  Type: *ast.SelectorExpr {
2  .  .  X: *ast.Ident {
3  .  .  .  NamePos: <full path>\move.go:16:2
4  .  .  .  Name: "mixin"
5  .  .  }
6  .  .  Sel: *ast.Ident {
7  .  .  .  NamePos: <full path>\move.go:16:8
8  .  .  .  Name: "UpdateTimes"
9  .  .  }
10  .  }
11  }

jennifer によるコード生成

以下に example はあるものの読み取るのが大変なためケースごとに一部だけコードを記載

refs:

パッケージ

f := jen.NewFile("pkg")
// package pkg

インポート

// f.ImportName("github.com/Symthy/Product/internal/xxx", "xxx")  // どちらでも変わらない模様
f.ImportName("github.com/Symthy/Product/internal/xxx", "")
// import xxx "github.com/Symthy/internal/xxx"

f.ImportAlias("github.com/Symthy/Product/internal/yyy", "ailias")
// import ailias "github.com/Symthy/internal/yyy"

importNames := map[string]string{
    "github.com/Symthy/Product/internal/xxx": "xxx",
    "github.com/Symthy/Product/internal/yyy": "yyy",
    "github.com/Symthy/Product/internal/zzz": "zzz",
}
f.ImportNames(importNames)
// import (
//     xxx "github.com/Symthy/Product/internal/xxx"
//     yyy "github.com/Symthy/Product/internal/yyy"
//     zzz "github.com/Symthy/Product/internal/zzz"
// )

構造体

f.Type().Id("SampleBuilder").Struct(
  jen.Id("id").Qual("github.com/Symthy/Product/internal/domain/value", "Indetifier"),
  jen.Id("name").String(),
  jen.Id("description").String(),
  jen.Id("effects").Index().Op("*").Qual("github.com/Symthy/Product/internal/domain/battles", "Effects"),
)
// type SampleBuilder struct {
//     id          value.Identifier
//     name        string
//     description string
//     effects     []*battles.Effects
//}

// 以下のように組み立てることも可能
fieldStatements := []*jen.Statement
fieldStatements = append(fieldStatements, jen.Id("id").Qual("github.com/Symthy/Product/internal/domain/value", "Indetifier"))
//  : 略
f.Type().Id("SampleBuilder").StructFunc(func(g *jen.Group) {
    for _, fieldStatement := range fieldStatements {
        g.Add(fieldStatement)
    }
})

コンストラクタ or 関数

typeName := "SampleBuilder"
f.Func().Id("New"+typeName).
        Params().
        Op("*").Qual("", typeName).
        Block(
            jen.Return(jen.Op("&").Qual("", typeName).Block()),
        )
// func NewSampleBuilder() *SampleBuilder {
//     return &SampleBuilder{}
// }

メソッド (セッター)

receiverName = "s"
typeName := "SampleBuilder"
argVarName := "name"
f.Func().
        Params(jen.Id(receiverName).Op("*").Id(typeName)).   // pointer receiver
        Id("Name").                                          // func
        Params(jen.Id(argVarName).String()).                 // arguments
        Op("*").Qual("", typeName).                          // return type
        Block(
            jen.Id(receiverName).Op(".").Id("name").Op("=").Id(argVarName),
            jen.Return(jen.Id(receiverName)),
        )
// func (s *SampleBuilder) Name(name string) *SampleBuilder {
//     s.name = name
//     return s
// }

部分的にに作って、Add() で任意の要素に付け足すことが可能なため柔軟。

go での コード生成

  • go generate で完結するようにした方が良い
  • コード生成を行うための go ファイルに以下を追加すれば、 go generate ./... で実行できる
//go:generate go run .

ref: go generate のベストプラクティス