コード生成用のツールに欲しいもの1 出力先の指定

はじめに

(個人的なコード生成のための環境を作っている。まだ完成には程遠いので実際のチュートリアルなどは作らない)

コード生成用のツールに欲しいものを思いつくままにあげていく。

出力先の指定

コード生成で欲しいのは出力先を指定できることかもしれない。これは特にデバッグ出力ができると嬉しい。

完全に動作することがわかって状態でのコード生成では全く要らないものではあるのだけれど。コード生成の出力結果が不安定であった場合にデバッグがしたい。

不安定というのは以下の2種類がある。

  • 入力となる設定に不備がある
  • コード生成の実装に不備がある

設定に不備がある場合には少しずつの入力の結果を変えて試したいかもしれない。コード生成の実装に不備がある場合には実装部分まで潜ってテストを書いたりするかもしれない。ただ、まずは最初の第一歩として不備の結果を再現する最小のサブセットを見つけるところから始めることが多いと思う。

最小のサブセットを見つけるときや初めの一歩のタイミングでは、依存もなるべく最小限にしたい。なるべくなら1ファイルで完結させたい(依存ライブラリのimportは仕方がないかもしれない。とは言えなるべく依存も減らしたいところではある)。

例えば、設定ファイルを受け取って、直接何らかのパスに書き込む形のコマンドはデバッグがしにくい。以下のようなもの。

$ go run main.go --pkg github.com/podhmo/x/y --name S
# $GOPATH/src/github.com/podhmo/x/y/s_gen.go などに出力

これは入力対象のファイル(設定ファイル)と出力対象のファイルと変換用のツールの3つを確認する必要が出てくる。 個人的には、特にGOPATH以下を前提としなければいけないコードになるのが結構苦痛だったりする(挙動を確かめるために /tmp 以下のファイルであったり、個人的なsandbox的なディレクトリに雑にコードを書いて試すということが多いので)。

1ファイルで完結するというのは以下のようなもの。

go run main.go
# 出力結果が標準出力に出力される

出力先の指定はディレクトリ単位であってほしい

goのパッケージの出力のことを考えると出力先が1つであることは少ない。例えばStringerみたいなものを考えたときに、1つのパッケージ内の複数の型を指定したいことがしばしばある。ほとんどコストが0であれば都度呼び出すということでも良いかもしれないけれど。ちょっと凝った処理だったりを書いてしまった場合には困るので(go/astやgo/typesを使わないコード生成の場合にはそんなに読み込みに時間は掛からないのだけれど。それならたぶんコード生成ツール自体をgoで書く必要はあんまりない)。

ということはコード生成結果を標準出力に吐いて終了と言うようなことはできない。デフォルトではあくまで出力先は複数であって欲しい。

一方でデバッグ時には全てコンソール上に出力されて欲しい。

そんなわけで以下に対して1ファイルで完結するような動作が求められる感じ。

  • 複数の入力ファイル
  • 複数の出力ファイル

現在の状況

現在のところでも一応不格好だけれど。複数の入力と複数の出力をシミュレートした状態を1ファイルで試すことができるようにしている。

入力としてmessage.goを利用し、出力としてhello.goとbye.goを生成するというようなコマンドを作ってみる。

もう少し丁寧に書くと以下の様な感じ。

入力ファイル

  • testcode/message.go

出力ファイル

  • (testcode/message.go)
  • testcode/hello.go
  • testcode/bye.go

実行結果は以下のような感じ。

$ go run main.go
2018/04/04 00:43:39 package ./testcode is not found, creating.
open  message.go
----------------------------------------
package testcode

type Message string

const (
    MessageHello    = Message("HELLO")
    MessageBye  = Message("BYE")
)
open  bye.go
----------------------------------------
package testcode
func Bye() string {
    return string(MessageBye)
}
open  hello.go
----------------------------------------
package testcode
func Hello() string {
    return string(MessageHello)
}

これは以下の様なコードで作れる(まだAPIなどは安定していないので変わることはある)

main.go

package main

import (
    "log"

    "github.com/podhmo/handwriting"
    "golang.org/x/tools/go/loader"
)

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    // 入力となるようなsource codeをコード内に含められる
    source := `
package testcode

type Message string
const (
  MessageHello = Message("HELLO")
  MessageBye = Message("BYE")
)
`
    c := &loader.Config{}
    f, err := c.ParseFile("message.go", source)
    if err != nil {
        return err
    }

    // 入力ファイルを複数にすることも可能だけれど。今はひとつだけ
    c.CreateFromFiles("./testcode", f)
    p, err := handwriting.New(
        "./testcode",
        handwriting.WithConfig(c),       // x/toolsのloader経由で*ast.Fileで作られたpackageを受け取れる
        handwriting.WithConsoleOutput(), // これをつけると出力は標準出力になる(debug print)
    )
    if err != nil {
        return err
    }

    // hello.goとbye.goの複数の出力が可能
    p.File("hello.go").Code(func(f *handwriting.File) error {
        f.Out.WithBlock("func Hello() string", func() {
            f.Out.Println(`return string(MessageHello)`)
        })
        return nil
    })

    p.File("bye.go").Code(func(f *handwriting.File) error {
        f.Out.WithBlock("func Bye() string", func() {
            f.Out.Println(`return string(MessageBye)`)
        })
        return nil
    })
    return p.Emit()
}