コード生成用のツールに欲しいもの2 パッケージの情報の利用

はじめに

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

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

パッケージの情報の利用

goの標準ライブラリににgo/typesという素晴らしいパッケージがある。せっかくなのでコード生成ツールなどにも利用したい。

型情報を取り扱って計算するということもできそうで夢は広がるけれど。今回はそこまでは触れない。今回の記事では表示などについてのみ触れる。

前回のコードを変えてパッケージの情報を見るようにしてみる

前回の記事でのコードに変更を加えて、パッケージの保持する情報を使うように変えてみたい。

前回のおさらいも兼ねて出力結果を再掲すると。以下のようなmessage.goというコードから。

message.go

package testdata

type Message string
const (
    MessageHello = Message("HELLO")
    MessageBye = Message("BYE")
)

以下の様な出力を得るコードを書いていた。

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

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)
}

戻り値の型を変えてみる

戻り値の型を変えてみたい。今まではstringだったのだけれど。定義したMessage型を返すようにしたい。もちろん"Message"という文字列を直接入力してしまえばかのうではあるけれど。それではあまり意味が無い。

今回はしっかりと go/types パッケージの値を利用して出力するように変えてみる。

具体的には、helloの出力部分を以下の様に変える。

   p.File("hello.go").Code(func(f *handwriting.File) error {
        message := f.PkgInfo.Pkg.Scope().Lookup("Message")
        f.Out.WithBlock(fmt.Sprintf("func Hello() %s", message.Name()), func() {
            f.Out.Println(`return MessageHello`)
        })
        return nil
    })

Code() が引数に取る *handwriting.File*loader.PackageInfo を保持していて、この値から、 *go/types/Package の値が取れる。後は通常通りにscopeから値を取り出して利用する(nilチェックは省いている)。

ちなみに変更前は以下の様なコードだった。

   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
    })

importを考慮してみる

実は先程の対応では不十分だったりする。importを考慮していない。例えば現在時刻を受け取って文字列を返すみたいなHelloWithNowみたいな関数を作ってみる(今回の戻り値はstring)。

注意点として、現在時刻を受け取る部分の引数部分に time.Time という表示で出力される必要がある。timeパッケージをimportして使っているので Time ではだめ。

期待する出力結果は以下のようなコード。

func HelloWithNow(now time.Time) string {
    return fmt.Sprintf("%s %s\n", MessageHello, now)
}

先程のコードでの型部分の出力のコードは fmt.Sprintf("func Hello() %s", message.Name() だった。message.Name()は単に自身の名前を文字列として返すだけなのでimportが考慮できていない。

importを考慮するにはResolverというものを使う。

    f2 := p.File("hello2.go")
    f2.Import("time")
    f2.Code(func(f *handwriting.File) error {
        t := f.Prog.Package("time").Pkg.Scope().Lookup("Time")
        f.Out.WithBlock(fmt.Sprintf("func HelloWithNow(now %s) string", f.Resolver.TypeName(t.Type())), func() {
            f.Out.Printfln(`return fmt.Sprintf("%%s %%s\n", MessageHello, now)`)
        })
        return nil
    })

timeパッケージに依存するのでimportする必要がある。あとは、f.Progに *loader/Program の値が格納されていて、そこからparse済みの*loader/PackageInfo の値が取得できる。f.Resolver で取り出したResolverの TypeName()types/Typeインターフェイスを実装する値を渡してあげると良い感じに出力してくれる。

$ go run main.go
... snip
open  hello2.go
----------------------------------------
package testcode

import (
    "time"
)

func HelloWithNow(now time.Time) string {
    fmt.Sprintf("%s %s\n", MessageHello, now)
}

別名でimportした場合

別名でのimportもサポートしたい。例えば以下のようなimportであった場合。

import xtime "time"

これは先程の f.Import("time") 部分を f.ImportWithName("time", "xtime") に変えてあげれば対応できる。

   f2 := p.File("hello2.go")
    f2.ImportWithName("time", "xtime")
    f2.Code(func(f *handwriting.File) error {
        t := f.Prog.Package("time").Pkg.Scope().Lookup("Time")
        f.Out.WithBlock(fmt.Sprintf("func HelloWithNow(now %s) string", f.Resolver.TypeName(t.Type())), func() {
            f.Out.Printfln(`return fmt.Sprintf("%%s %%s\n", MessageHello, now)`)
        })
        return nil
    })

大丈夫。

$ go run main.go
... snip
open  hello2.go
----------------------------------------
package testcode

import (
    xtime "time"
)

func HelloWithNow(now xtime.Time) string {
    return fmt.Sprintf("%s %s\n", MessageHello, now)
}

戻り値のタプルを考慮してみる

関数の戻り値で厄介なのはtupleのとき。今度は戻り値で現在時刻とメッセージ(message.goで定義した型)を返すようにしてみる。

期待する出力結果は以下のようなコード。

func HelloWithNow2() (Message, time.Time) {
    now := time.Now()
    return MessageHello, now
}

タプルの部分を直接 fmt.Sprintf("(%s, %s)", "Message", "time.Time") となるように書いてみても良いけれど。0~N個の引数を取り、0~N個の戻り値を返すというような関数のコードをを生成したい場合にちょっと困る(このような処理はmock用のコードの自動生成などでしばしば現れる)。

これは、素直に types.Tuple の値を自分で作る(このあたりは何かヘルパー関数があっても良いかもしれない)。そして型名の部分にはTypeNameForResults()を使う。

   f3 := p.File("hello3.go")
    f3.Import("time")
    f3.Code(func(f *handwriting.File) error {
        timepkg := f.Prog.Package("time").Pkg

        messageT := f.PkgInfo.Pkg.Scope().Lookup("Message").Type()
        timeT := timepkg.Scope().Lookup("Time").Type()
        retvalT := types.NewTuple(
            types.NewVar(token.NoPos, f.PkgInfo.Pkg, "", messageT),
            types.NewVar(token.NoPos, timepkg, "", timeT),
        )
        f.Out.WithBlock(fmt.Sprintf("func HelloWithNow2() %s", f.Resolver.TypeNameForResults(retvalT)), func() {
            f.Out.Println("now := time.Now()")
            f.Out.Println("return MessageHello, now")
        })
        return nil
    })

TypeNameForResults() を使う理由はtupleの長さによって表示が変わるため。

長さ0のとき func F0() { }

長さ1のとき func F1() X { }

長さ2のとき func F2() (X,Y) { }

長さNのとき func F2() (X0,X1,...,Xn) { }

特に0のときと1のときにカッコなどが表示されてほしくない。

$ go run main.go
... snip
open  hello3.go
----------------------------------------
package testcode

import (
    "time"
)

func HelloWithNow2() (Message, time.Time) {
    now := time.Now()
    return MessageHello, now
}