大規模なコードベースでリファクタリングを省エネ化するために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"
"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のコードは書き換えられそうな気がする。
その他のツール
- TypeScriptだとts-morphというものがあり、これのGo版が欲しい...!!!
- Codemodというコード書き換えのためのSaaSがある。内部的にはast-grepなどを呼び出すっぽい
最後に
とりあえずast-grepで試行錯誤して上手くいったらまた記事を書きます。