GoでASTと戯れる
GoでAST(Abstract Syntax Tree)を使ってソースコードを解析, 生成を行うに際してライブラリを作ったりしたので紹介したいと思います.
AST
Goは標準で go/ast
でASTを扱うことができます.
便利なものもありますし詳しく記述されているのでgo/astのgodocを見るのが一番良いとは思うのですが,
とりあえず静的解析がしたい場合には
のようなツールを使って実際のコードがどういったASTになるのかを知りながら進めるのが良いかなと思います.
はじめてのAST
はじめに僕が作ったASTを使うツールは WithContext
という終わり方をしていて第一引数がcontext.Context
の関数に常時context.Background()
を渡す関数を生成するものでした.
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が作りたい
ではソースコード生成部は文字列連結のような形で実現しましたが, やはりいろんなケースに対応するのは難しいと感じたので完全に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から独立した形でこの機能を提供したいと思ったので実装しました.
の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から定義するスタイルでも, 実装からするスタイルでもストレスのない開発が実現できると思います.
あとお気づきの方もいるとは思いますが, 上のツール群は以下のライブラリのコマンドとして実装されています.
impastは外部ライブラリのASTを読みたいと思ったときに簡単には実現できなかったことから作成したライブラリです. go/build
のImport
を使ってディレクトリだけ求めてparser.ParseDir
を使用して実現しています.
importする機能だけでなく, 上の3つのツールを作る上で必要な*ast.Package
に関するユーティリティ関数が提供されていたりします.
上記のツールでもいいですし, impastそのものでもいいのでぜひ使ってみてフィードバックをいただけると嬉しいです.
良いと思ったらGitHubのStarをしていただけると励みになります.