Goの静的解析 ~go vet編~

コンパイラによってキャッチされない不具合や懸念を検出するためのものとして、静的解析ツールがある。
Goの静的解析ツールはいくつかあるが、ここでは go vetについて紹介する。

## go vetとは
Go言語に標準で組み込まれた静的解析ツール。
言語標準ツールのため言語のバージョンアップと同じタイミングで更新される。
公式ドキュメントは[こちら](https://pkg.go.dev/cmd/vet)。

## 使用可能なルール
`go tool vet help`を実行すると、使用可能なルールの一覧が表示される。Go1.22.3 時点では32のルールが存在する。

```
$ go tool vet help
...
Registered analyzers:

    appends      check for missing values after append
    asmdecl      report mismatches between assembly files and Go declarations
    assign       check for useless assignments
    atomic       check for common mistakes using the sync/atomic package
    bools        check for common mistakes involving boolean operators
    buildtag     check //go:build and // +build directives
    cgocall      detect some violations of the cgo pointer passing rules
    composites   check for unkeyed composite literals
    copylocks    check for locks erroneously passed by value
    defers       report common mistakes in defer statements
    directive    check Go toolchain directives such as //go:debug
    errorsas     report passing non-pointer or non-error values to errors.As
    framepointer report assembly that clobbers the frame pointer before saving it
    httpresponse check for mistakes using HTTP responses
    ifaceassert  detect impossible interface-to-interface type assertions
    loopclosure  check references to loop variables from within nested functions
    lostcancel   check cancel func returned by context.WithCancel is called
    nilfunc      check for useless comparisons between functions and nil
    printf       check consistency of Printf format strings and arguments
    shift        check for shifts that equal or exceed the width of the integer
    sigchanyzer  check for unbuffered channel of os.Signal
    slog         check for invalid structured logging calls
    stdmethods   check signature of methods of well-known interfaces
    stringintconv check for string(int) conversions
    structtag    check that struct field tags conform to reflect.StructTag.Get
    testinggoroutine report calls to (*testing.T).Fatal from goroutines started by a test
    tests        check for common mistaken usages of tests and examples
    timeformat   check for calls of (time.Time).Format or time.Parse with 2006-02-01
    unmarshal    report passing non-pointer or non-interface values to unmarshal
    unreachable  check for unreachable code
    unsafeptr    check for invalid conversions of uintptr to unsafe.Pointer
    unusedresult check for unused results of calls to some functions
...
```


`appends`ルールは、スライスに追加すべき値を渡していない append 呼び出しを検出する。

```go
s := []string{"a", "b", "c"}
_ = append(s)  // append with no values
```

`unmarshal`ルールは引数の型がポインターまたはインターフェースではない `json.Unmarshal` などの関数呼び出しを検出する。

```go
import (
	"encoding/json"
	"io"
)

type Person struct {
	Name string
	Age  int
}

func handler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Failed to read request body", http.StatusBadRequest)
		return
	}

	var person Person
	err = json.Unmarshal(body, person) //  call of Unmarshal passes non-pointer as second argument
	if err != nil {
		http.Error(w, "Failed to unmarshal JSON", http.StatusBadRequest)
		return
	}
}
```

各ルールの詳細については、`go tool vet help {ルール名}`を実行すると表示される。

```
$ go tool vet help appends
appends: check for missing values after append

This checker reports calls to append that pass
no values to be appended to the slice.

        s := []string{"a", "b", "c"}
        _ = append(s)

Such calls are always no-ops and often indicate an
underlying mistake.
```

## go vet の実行方法
```
go vet ./...
```

デフォルトでは全てのルールが有効で、特定のルールを実行するには実行オプションとして`-{ルール名}`を付与する。

```appendsルールだけを有効にする例
$ go vet -appends ./...
```

## go vet -vettoolで実行する静的解析ツール
go vetのオプション`-vettool`を指定して、追加で実行できるルールがある。

- [defers](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers)
- [fieldalignment](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment)
- [findcall](https://pkg.go.dev/golang.org/x/tools@v0.24.0/go/analysis/passes/findcall)
- [nilness](https://pkg.go.dev/golang.org/x/tools@v0.24.0/go/analysis/passes/nilness)
- [shadow](https://pkg.go.dev/golang.org/x/tools@v0.24.0/go/analysis/passes/shadow)

各ルールの詳細は「[go vet に含まれないスタンドアロンな静的解析ツールたち](https://future-architect.github.io/articles/20231005a/)」で解説されているのでそちらを参照ください。
ここでは nilness, shadow について紹介する。

### nilness
例えば if 文で nil 検査をしているにもかかわらず、ポインタ型のフィールドにアクセスしようとするとpanicが生じる。
nillnessはそういった実装を検出してくれる。

```go
type X struct{ f int }

func Fnilness(x *X) {
	if x == nil {
		fmt.Println(x.f) // nil dereference in field selection
		return
	}
	fmt.Println(x.f)
}
```

### shadow
シャドウイングを検知してくれるツール。
シャドウイングとは、コードブロックの外側にある変数と同名の変数をコードブロックの内側で宣言した時、内側からは内側の変数にアクセスするというもの（外側の変数にアクセスできない）。

以下のコードでは 変数名`err`が シャドウイングに当たり、`f.Read()`でエラーが発生してもfor文の外側で宣言された `err` には格納されないため、`BadRead()`は常に nil を返すことになる。

```go
func BadRead(f *os.File, buf []byte) error {
	var err error
	for {
		_, err := f.Read(buf) // declaration of "err" shadows declaration
		if err != nil {
			break
		}
	}
	return err
}
```

これを改善するにはfor文の内側で `err` が再定義されないように `_, err = f.Read(buf)` とする。

### vettoolの実行方法
各vettoolをインストールする。

```
go install golang.org/x/tools/go/analysis/passes/nilness/cmd/nilness@latest
go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest
```

go vet の `-vettool` には複数のルールを指定できないため、個別に実行してあげる必要がある。

```
go vet ./...
go vet -vettool=$(which nilness) ./...
go vet -vettool=$(which shadow) ./...
```

## 指摘を無視したい時
`go vet`による指摘は基本的には直すのが良い。
しかし、ライブラリや自動生成ファイルなどの自分たちで管理していないコードは静的解析対象外にしたいケースもある。

例えば、次のようなディレクトリ構成で `mock` ディレクトリには mockgen を使って自動生成したファイルを格納しており対象外にしたいとする。

```
├── go
    ├── app
    ├── domain
    ├── go.mod
    ├── go.sum
    ├── infra
    ├── lib
    ├── main.go
    ├── mock  // 静的解析対象外にしたい
    └── presentation
```

この時、次のコマンドを実行すると `mock` ディレクトリ配下のコード以外を静的解析の対象にできる。

```
go vet $(go list ./... | grep -v '{module-name}/mock')
```

`{module-name}`はgo.modに書かれた module名で、`grep -v '{module-name}/mock'`によって mockディレクトリ配下にあるパッケージを除いた全パッケージを指定している。