golang.org/x/tools/go/analysis を使ってコードを自動修正する

これは Go3 Advent Calendar 2019 の 3 日目の記事です。

今年 2 月にリリースされた Go 1.12 において、 公式の静的解析ツールである go vetgolang.org/x/tools/go/analysis を使う形に書き直されました

Analyzer

golang.org/x/tools/go/analysisAnalyzer (静的解析モジュール) を作るためのパッケージで、 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 には singlecheckermultichecker 、そして unitchecker の 3 種類があり、 go vetunitchecker を使って作られています。

SuggestedFix

今年の 6 月、この golang.org/x/tools/go/analysisSuggestedFix (修正案) という機能が入りました。これにより、今までのように解析して見つかった問題を報告するだけでなく、その修正案を提示できるようになりました。 SuggestedFix には Message (メッセージ) と複数の TextEdit (テキスト編集) が含まれます。

type SuggestedFix struct {
    Message   string
    TextEdits []TextEdit
}

TextEditPos (コードの開始位置) と End (コードの終了位置) 、そして NewText (新しいテキスト) から構成されます。

type TextEdit struct {
    Pos     token.Pos
    End     token.Pos
    NewText []byte
}

そして SuggestedFixDiagnostic に複数紐づけることができます。つまり 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 を作ってみました。

github.com

-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 として実装しています。

github.com

SuggestedFix の適用

SuggestedFix の適用方法は現状 2 通り用意されているようです。

1 つは前述したサンプルの例で出た -fix を使う方法で、 singlecheckermultichecker はこのフラグが渡されると SuggestedFix を自動で適用する仕組みになっています。 go vet に使われている unitchecker はこのフラグに対応しておらず、現状だと SuggestedFix の適用はできないようでした。

もう 1 つは gopls を使う方法です。 SuggestedFix は既に gopls に統合されているようですが、私が使っている vim-go はまだ該当のメソッド (textDocument/codeAction) に対応していないようで今回は試せませんでした。

gopls 自体もまだ開発途中のようですが、近い将来に Analyzer が提示する修正案を様々なエディタから適用できるようになるかもしれません。

参考