薄いブログ

技術の雑多なことを書く場所

GoでASTと戯れる

GoでAST(Abstract Syntax Tree)を使ってソースコードを解析, 生成を行うに際してライブラリを作ったりしたので紹介したいと思います.

AST

Goは標準で go/ast でASTを扱うことができます.

便利なものもありますし詳しく記述されているのでgo/astのgodocを見るのが一番良いとは思うのですが,

とりあえず静的解析がしたい場合には

akito0107.hatenablog.com

http://goast.yuroyoro.net/

のようなツールを使って実際のコードがどういったASTになるのかを知りながら進めるのが良いかなと思います.

はじめてのAST

はじめに僕が作ったASTを使うツールは WithContextという終わり方をしていて第一引数がcontext.Contextの関数に常時context.Background()を渡す関数を生成するものでした.

github.com

Goを始めて2ヶ月くらいでAPI Clientを作っているときでした. その当時の僕はcontext.Contextを何も理解しておらず, とりあえずcontext.Backgroud()を渡しておけば良いと思っていました. しかし, どこかで第一引数はcontext.Contextを渡すようにするといいという情報を目にして思考停止して第一引数はcontext.Contextにしていました. API Clientで使用するエンドポイントが増えるたびに, リファクタで変数が変わるたびに人間が修正するのは正気じゃないと思ったのがきっかけでした. その程度の理解度の人間でも扱えるので, 非常に良くできていると思います.

実際に動作させると以下の様になります.

$ go get github.com/orisano/nocontext
$ cat main.go
package main

import "context"

func main() {}

func GetFooWithContext(ctx context.Context, a, b int) {

}

上のようなスカスカの関数があるだけのファイルを入力として与えると

$ nocontext -f main.go
func GetFoo(a, b int) {
    GetFooWithContext(context.Background(), a, b)
}

という風な関数を出力してくれます. 当時はgo generateなどをほとんど意識していなかったので全然使えません.

このくらいならsedでできるみたいな声が聞こえて来そうですが, 意外と引数の型やreturn文のパターン, 引数の一覧を展開するなどを考えるとASTで処理するのが最も良い様に思えます.

StructからInterfaceを生成したい

外部ライブラリでstructで実装が提供される場合があり, リファクタやテストのためのmockingためにinterfaceにしなければいけないことがありました. インターフェースは小さくせよとプログラミング言語Goには書いてありますが, 人間は怠惰なもので大きなinterfaceを作ってしまいます. すべてのメソッドをinterfaceにするのはなかなかに骨の折れる作業です. 複数の外部ライブラリの実装を共通で扱えるようにするためのinterface化の作業は辛いです.

上で挙げた問題を解決するためにツールを作成しました.

https://github.com/orisano/impast#interfacer

$ go get -u github.com/orisano/impast/cmd/interfacer
$ interfacer -out HTTPClient net/http.Client
type HTTPClient interface {
    Do(req *http.Request) (*http.Response, error)
    Get(url string) (resp *http.Response, err error)
    Head(url string) (resp *http.Response, err error)
    Post(url, contentType string, body io.Reader) (resp *http.Response, err error)
    PostForm(url string, data url.Values) (resp *http.Response, err error)
}
$ interfacer -out Conn database/sql.DB database/sql.Tx
type Conn interface {
    Exec(query string, args ...interface{}) (sql.Result, error)
    ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
    Prepare(query string) (*sql.Stmt, error)
    PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
    Query(query string, args ...interface{}) (*sql.Rows, error)
    QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
    QueryRow(query string, args ...interface{}) *sql.Row
    QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
}

一つでも複数でも共通のメソッドを求めることができます. 構造体の埋込に関しても対応しています. -pkgを指定するとコンパイル可能な形で出力されるのでgo generateからで使用することができます.

InterfaceからMockが作りたい

github.com

recruit-tech.co.jp

ではソースコード生成部は文字列連結のような形で実現しましたが, やはりいろんなケースに対応するのは難しいと感じたので完全にASTでやりたいという気持ちがありました.

これはこれで純粋に自分でMock生成部を実装したいと思ったので実装しました.

https://github.com/orisano/impast#mocker

$ go get -u github.com/orisano/impast/cmd/mocker
$ mocker -pkg io -type ReadWriter
type ReadWriterMock struct {
    ReadMock    func(p []byte) (n int, err error)
    WriteMock   func(p []byte) (n int, err error)
}

func (mo *ReadWriterMock) Read(p []byte) (n int, err error) {
    return mo.ReadMock(p)
}

func (mo *ReadWriterMock) Write(p []byte) (n int, err error) {
    return mo.WriteMock(p)
}

というようなフィールドをベースとしたMockの実装を生成してくれます. 引数の名前が指定されている場合はそちらを優先し,されていない場合は連番でつけます.

InterfaceからStubを作りたい

GoLandではcmd+Nでinterfaceのstubの実装が生成できます. IDEから独立した形でこの機能を提供したいと思ったので実装しました.

www.jetbrains.com

のCode generationにある機能です.

https://github.com/orisano/impast#stuber

$ go get -u github.com/orisano/impast/cmd/stuber
$ stuber -pkg net -implement Conn -export -name c -type "*MyConn"
func (c *MyConn) Read(b []byte) (n int, err error) {
    panic("implement me")
}

func (c *MyConn) Write(b []byte) (n int, err error) {
    panic("implement me")
}

func (c *MyConn) Close() error {
    panic("implement me")
}

func (c *MyConn) LocalAddr() net.Addr {
    panic("implement me")
}

func (c *MyConn) RemoteAddr() net.Addr {
    panic("implement me")
}

func (c *MyConn) SetDeadline(t time.Time) error {
    panic("implement me")
}

func (c *MyConn) SetReadDeadline(t time.Time) error {
    panic("implement me")
}

func (c *MyConn) SetWriteDeadline(t time.Time) error {
    panic("implement me")
}

必要なメソッドの中身をpanic("implement me")で生成します. すでにあるメソッドを考慮してくれたりはしません.

まとめ

  • structからinterfaceを生成するツール
  • interfaceからmockを生成するツール
  • interfaceからstubを生成するツール

この3つのツールがあればinterfaceから定義するスタイルでも, 実装からするスタイルでもストレスのない開発が実現できると思います.

あとお気づきの方もいるとは思いますが, 上のツール群は以下のライブラリのコマンドとして実装されています.

github.com

impastは外部ライブラリのASTを読みたいと思ったときに簡単には実現できなかったことから作成したライブラリです. go/buildImportを使ってディレクトリだけ求めてparser.ParseDirを使用して実現しています.

importする機能だけでなく, 上の3つのツールを作る上で必要な*ast.Packageに関するユーティリティ関数が提供されていたりします.

上記のツールでもいいですし, impastそのものでもいいのでぜひ使ってみてフィードバックをいただけると嬉しいです.

良いと思ったらGitHubのStarをしていただけると励みになります.