大規模なコードベースでリファクタリングを省エネ化するためにcodemodを最近調べていて、軽く試行錯誤したのでそのメモ。
やりたいこと
例えば以下のようなTable Driven TestなコードをBEFOREからAFTERに書き換えたい。コード量が多いため人間がやるのは現実的ではなく、codemodで機械的に書き換えたい。
BEFORE
package main import ( "slices""testing" ) func TestContains(t *testing.T) { type args struct { ss []string s string } tests := map[string]struct { args args want bool }{ "empty: false": { args: args{[]string{}, ""}, want: false, }, "found: true": { args: args{[]string{"a", "b", "c"}, "b"}, want: true, }, "not found: false": { args: args{[]string{"a", "b"}, "c"}, want: false, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { if got := contains(tt.args.ss, tt.args.s); got != tt.want { t.Errorf("contains(): want=%v, got=%v", got, tt.want) } }) } } func contains(ss []string, s string) bool { return slices.Contains(ss, s) }
AFTER
func TestContains(t *testing.T) { type args struct { ss []string s string } tests := map[string]struct { args args want bool }{ "empty: false": { args: args{[]string{}, ""}, want: false, }, } ... }
DIFF
--- a/tools_eg/table_driven_test.go+++ b/tools_eg/table_driven_test.go@@ -10,29 +10,25 @@ func TestContains(t *testing.T) { ss []string s string } - tests := []struct {- name string+ tests := map[string]struct { args args want bool }{ - {- name: "empty: false",+ "empty: false": { args: args{[]string{}, ""}, want: false, }, - {- name: "found: true",+ "found: true": { args: args{[]string{"a", "b", "c"}, "b"}, want: true, }, - {- name: "not found: false",+ "not found: false": { args: args{[]string{"a", "b"}, "c"}, want: false, }, } - for _, tt := range tests {- t.Run(tt.name, func(t *testing.T) {+ for name, tt := range tests {+ t.Run(name, func(t *testing.T) { if got := contains(tt.args.ss, tt.args.s); got != tt.want { t.Errorf("contains(): want=%v, got=%v", got, tt.want) }
試したこと
eg
eg
というのは golang.org/x/tools 配下で提供されているコード書き換えのコマンドのこと(link)。例えば以下のようにdeprecatedな ioutil.ReadAll
を呼び出している箇所をtemplateのGoコードを用いて io.ReadAll
に書き換えることができる。
http_get.go
package main import ( "fmt""io/ioutil"//nolint:staticcheck"net/http" ) func main() { resp, err := http.DefaultClient.Get("https://github.com/golang/go") if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } fmt.Println(string(body)) }
http_get.template
package main import ( "io""io/ioutil" ) func before(r io.Reader) ([]byte, error) { return ioutil.ReadAll(r) } func after(r io.Reader) ([]byte, error) { return io.ReadAll(r) }
egを使って書き換える
$ eg -t http_get.template -w http_get.go
ただ、egでは先ほどのようなTable Driven Testの struct -> map[string]struct にするのは eg: map[string]struct{} is not a safe replacement for struct{name string}
のエラーでできなかった(ref)。
なので、次に紹介するast-grepを試してみた。
ast-grep
ast-grepは様々な言語に対応した、ASTベースでコードの検索、Linterの作成、コード書き換えを行えるコマンドラインツールである。YAMLファイルによる独自のルールを定義することによって、かなり複雑なパターンマッチングを行うことができる。
ASTツリーを表示しながらパターンにマッチした部分をハイライトするPlaygroundも用意されているので、AST初心者でも試行錯誤すればルールが定義できそうな雰囲気を感じる。自分はまだ使いこなせてないので、とりあえず紹介だけ。
これを使えばおそらく先ほどのTable Driven Testのコードは書き換えられそうな気がする。
その他のツール
最後に
とりあえずast-grepで試行錯誤して上手くいったらまた記事を書きます。