golang.org/x/tools/go/analysis を使ってコードを自動修正する
これは Go3 Advent Calendar 2019 の 3 日目の記事です。
今年 2 月にリリースされた Go 1.12 において、 公式の静的解析ツールである go vet が golang.org/x/tools/go/analysis を使う形に書き直されました。
Analyzer
golang.org/x/tools/go/analysis は Analyzer (静的解析モジュール) を作るためのパッケージで、 go vet が行う様々な処理は個別の Analyzer として分離されこのパッケージ配下に格納されています。これらは外部からも参照できるようになっており、サードパーティの Analyzer から参照することもできます。
Diagnostic
Analyzer は、解析して見つかった問題を Diagnostic (診断) として報告します。 Diagnostic には Pos (コードの開始位置) と Message (メッセージ) が含まれます。
type Diagnostic struct { Pos token.Pos Message string ... }
例えば Printf() の書式識別子と引数の型が合っていないと go vet は診断を出します。
$ cat main.go package main import "fmt" func main() { fmt.Printf("%s", 1) }
$ go vet . ./main.go:6:2: Printf format %s has arg 1 of wrong type int
このように問題があったコードの位置 (./main.go:6:2) とメッセージ (Printf format %s has arg 1 of wrong type int) を出すことでユーザーに修正を促す仕組みになっています。
Analyzer は checker というパッケージを通して実行可能ファイルにすることができます。 checker には singlechecker と multichecker 、そして unitchecker の 3 種類があり、 go vet は unitchecker を使って作られています。
SuggestedFix
今年の 6 月、この golang.org/x/tools/go/analysis に SuggestedFix (修正案) という機能が入りました。これにより、今までのように解析して見つかった問題を報告するだけでなく、その修正案を提示できるようになりました。 SuggestedFix には Message (メッセージ) と複数の TextEdit (テキスト編集) が含まれます。
type SuggestedFix struct { Message string TextEdits []TextEdit }
TextEdit は Pos (コードの開始位置) と End (コードの終了位置) 、そして NewText (新しいテキスト) から構成されます。
type TextEdit struct { Pos token.Pos End token.Pos NewText []byte }
そして SuggestedFix は Diagnostic に複数紐づけることができます。つまり 1 つの診断に対して複数の修正案を提示できるということですね。
type Diagnostic struct { Pos token.Pos Message string SuggestedFixes []SuggestedFix ... }
SuggestedFix に複数の TextEdit を含めているのは修正案に柔軟性を持たせるためでしょう。例えば「関数に不要な引数が含まれる」という診断に対する修正案には、関数そのものに対する編集だけでなくその呼び出し元に対する編集も含めることができます。
func add(a, b, c int) int { // ^^^ ここを編集する (1 つ目の TextEdit) return a + b } func main() { fmt.Println(add(1, 2, 0)) // ^^^ ここも編集する (2 つ目の TextEdit) }
SuggestedFix のサンプルとして、引数が 1 つの fmt.Printf() を fmt.Print() に修正する Analyzer を作ってみました。
-fix を付けて実行することで SuggestedFix を適用し、コードを自動修正することができます。
$ cat main.go package main import "fmt" func main() { fmt.Printf("Hello, World") }
$ fmtprintf -fix .
./main.go:6:2: fmt.Printf call which have one argument can be replaced with fmt.Print
$ cat main.go package main import "fmt" func main() { fmt.Print("Hello, World") }
それから今年リリースされた Goa v3 へ移行するため v1 から v3 へのアップグレードツールを作っているのですが、こちらも SuggestedFix を使う Analyzer として実装しています。
SuggestedFix の適用
SuggestedFix の適用方法は現状 2 通り用意されているようです。
1 つは前述したサンプルの例で出た -fix を使う方法で、 singlechecker と multichecker はこのフラグが渡されると SuggestedFix を自動で適用する仕組みになっています。 go vet に使われている unitchecker はこのフラグに対応しておらず、現状だと SuggestedFix の適用はできないようでした。
もう 1 つは gopls を使う方法です。 SuggestedFix は既に gopls に統合されているようですが、私が使っている vim-go はまだ該当のメソッド (textDocument/codeAction) に対応していないようで今回は試せませんでした。
gopls 自体もまだ開発途中のようですが、近い将来に Analyzer が提示する修正案を様々なエディタから適用できるようになるかもしれません。