大規模なコードベースでリファクタリングを省エネ化するために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 := []struct { name string args args want bool }{ { name: "empty: false", args: args{[]string{}, ""}, want: false, }, { name: "found: true", args: args{[]string{"a", "b", "c"}, "b"}, want: true, }, { name: "not found: false", args: args{[]string{"a", "b"}, "c"}, want: false, }, } for _, tt := range tests { t.Run(tt.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で試行錯誤して上手くいったらまた記事を書きます。