Go/analysis
Go Analysis フレームワーク は、Go プログラムの静的解析ツールを作成するためのフレームワークです。
正式なパッケージパスは golang.org/x/tools/go/analysis であり、標準ライブラリには含まれませんが、Go のツールチェーン配布物の src/cmd/vendor/golang.org/x/tools/go/analysis として同梱されています。
go vet および go fix はこのフレームワークを利用して実装されています。
概要
[編集]静的解析(static analysis)とは、プログラムを実際に実行せずにソースコードを検査し、誤りや改善点を報告する処理です。go/analysis はそのような解析を「モジュラー(modular)」に、すなわちパッケージ単位で独立して実行できるよう設計されています。
フレームワークの役割は、解析ロジック(Analyzer)と、それを実行する駆動プログラム(Driver)とのインターフェースを統一することにあります。
これにより、同一の Analyzer を go vet、IDE、CI システムなど様々な Driver で再利用できます。
┌─────────────────────────────┐
│ Driver(go vet / go fix / 独自ツール など) │
│ │
│ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │Analyzer A│ │Analyzer B│ │Analyzer C│ ... │
│ └─────┘ └─────┘ └─────┘ │
└─────────────────────────────┘
主要な型
[編集]Analyzer
[編集]Analyzer は解析器を静的に記述する型です。
解析の名前・ドキュメント・依存関係・実行関数などをまとめて定義します。
type Analyzer struct { Name string // コマンドラインなどで使う識別子(Go の識別子として有効であること) Doc string // ドキュメント。最初の "\n\n" より前の部分がタイトルになる URL string // 追加情報へのリンク(省略可) Flags flag.FlagSet // 解析の動作を制御するフラグ群 Run func(*Pass) (any, error) // 解析本体 RunDespiteErrors bool // 型エラーがあるパッケージでも実行するか Requires []*Analyzer // 事前に実行が必要な他の Analyzer(水平依存) ResultType reflect.Type // Run が返す結果の型 FactTypes []Fact // パッケージ間で共有する Fact の型(垂直依存) }
典型的な定義例を以下に示します。
package unusedresult
var Analyzer = &analysis.Analyzer{
Name: "unusedresult",
Doc: "check for unused results of calls to some functions",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
// 解析ロジックをここに記述する
return nil, nil
}
複数の Analyzer を Driver に登録するには次のようにします。
import (
"unusedresult"
"nilness"
"printf"
)
var analyses = []*analysis.Analyzer{
unusedresult.Analyzer,
nilness.Analyzer,
printf.Analyzer,
}
Pass
[編集]Pass は「特定の Analyzer を特定のパッケージに適用する 1 回の作業単位」を表す。
Run 関数に渡され、解析に必要な情報の取得と診断の報告を行うためのインターフェースとなります。
type Pass struct {
Analyzer *Analyzer // 実行中の Analyzer
Fset *token.FileSet // ファイル位置情報
Files []*ast.File // 各ファイルの構文木(AST)
OtherFiles []string // Go 以外のファイル名(アセンブリなど)
IgnoredFiles []string // 現在のビルド設定では無視されるファイル名
Pkg *types.Package // パッケージの型情報
TypesInfo *types.Info // 構文木の型情報
TypeErrors []types.Error // 型エラー(RunDespiteErrors が true のとき)
Module *Module // パッケージが属するモジュール情報
ResultOf map[*Analyzer]any // 依存 Analyzer の実行結果
Report func(Diagnostic) // 診断メッセージを報告する関数
ReadFile func(filename string) ([]byte, error) // ファイル内容を読み込む関数
// Fact のインポート/エクスポート関数(後述)
}
診断を報告する際には、便利なヘルパーメソッドが用意されています。
| メソッド | 説明 |
|---|---|
pass.Reportf(pos, format, args...)
|
位置とフォーマット文字列で診断を報告する |
pass.ReportRangef(rng, format, args...)
|
AST ノードの範囲(Range インターフェース)で診断を報告する
|
Diagnostic
[編集]Diagnostic は解析が検出した問題を表す型です。
type Diagnostic struct {
Pos token.Pos // 問題のある位置
End token.Pos // 問題の終端(省略可)
Category string // 分類(省略可)
Message string // メッセージ本文
URL string // 追加ドキュメントへのリンク(省略可)
SuggestedFixes []SuggestedFix // 修正提案のリスト(省略可)
Related []RelatedInformation // 関連情報(省略可)
}
SuggestedFix は TextEdit の集合として修正内容を表し、
IDE やエディタがユーザーに自動修正を提案する際に利用されます。
Diagnostic に severity(重大度)フィールドがないのは設計上の判断であり、
重要度の判断は Driver やユーザー設定に委ねられています。
Fact
[編集]Fact は、パッケージをまたいで解析結果を伝播させるための中間情報を表すインターフェースです。
これがモジュラー解析を実現する核心的な仕組みです。
コンパイラの型チェッカーが別コンパイルで型情報をエクスポートするのと同様に、Fact は解析情報を「別解析」として伝播させる。
パッケージ Q を解析
└─ "log.Printf は printf wrapper だ" という Fact を記録
↓ gob でシリアライズ・保存
パッケージ P を解析(Q をインポートしている)
└─ Q の Fact を読み込み → P 内の log.Printf 呼び出しもチェック
Fact を利用する Analyzer は FactTypes に型を宣言する必要があります。
var Analyzer = &analysis.Analyzer{
Name: "printf",
FactTypes: []analysis.Fact{new(isWrapper)},
// ...
}
type isWrapper struct{} // *types.Func f が「printf wrapper である」ことを表す
func (*isWrapper) AFact() {} // Fact インターフェースを満たすためのダミーメソッド
Fact の記録と取得には Pass の以下の関数を使用します。
// オブジェクトへの Fact の記録・取得
pass.ExportObjectFact(obj, new(isWrapper))
var fact isWrapper
if pass.ImportObjectFact(obj, &fact) {
// fact が存在する場合の処理
}
// パッケージへの Fact の記録・取得
pass.ExportPackageFact(fact)
pass.ImportPackageFact(pkg, fact)
Fact は encoding/gob でシリアライズされるため、決定的なエンコーディングが求められます。
また、Fact の型はポインタでなければなりません。
Analyzer の依存関係
[編集]依存には 2 種類あります。
| 種別 | フィールド | 方向 | 説明 |
|---|---|---|---|
| 水平依存 | Requires
|
同一パッケージ・異なる Analyzer | 別の Analyzer の実行結果を Pass.ResultOf で受け取る
|
| 垂直依存 | FactTypes
|
同一 Analyzer・異なるパッケージ | Fact を介して上位パッケージに情報を伝播させる |
水平依存(Requires) 垂直依存(FactTypes)
inspect ──→ printf pkg Q の解析
(同パッケージ内で └─ Fact を記録
結果を受け渡す) ↓
pkg P の解析(Q に依存)
└─ Q の Fact を参照
水平依存のグラフは非循環(DAG)でなければなりません。
analysis.Validate 関数で循環の有無を含む基本的な整合性チェックができます。
スタンドアロンコマンドの作成
[編集]singlechecker および multichecker サブパッケージを使うと、Analyzer をコマンドとして簡単に公開できます。
単一 Analyzer のコマンド
[編集]package main
import (
"golang.org/x/tools/go/analysis/passes/findcall"
"golang.org/x/tools/go/analysis/singlechecker"
)
func main() { singlechecker.Main(findcall.Analyzer) }
複数 Analyzer のコマンド
[編集]package main
import (
"golang.org/x/tools/go/analysis/multichecker"
"golang.org/x/tools/go/analysis/passes/printf"
"golang.org/x/tools/go/analysis/passes/nilness"
"golang.org/x/tools/go/analysis/passes/shadow"
)
func main() {
multichecker.Main(
printf.Analyzer,
nilness.Analyzer,
shadow.Analyzer,
)
}
テスト
[編集]analysistest サブパッケージを使うと、Analyzer のテストを簡潔に記述できます。
テストデータのソースコード内に // want ... コメントを書くことで、期待される診断メッセージを宣言的に指定できます。
import "golang.org/x/tools/go/analysis/analysistest"
func TestAnalyzer(t *testing.T) {
testdata := analysistest.TestData()
analysistest.Run(t, testdata, MyAnalyzer, "mypackage")
}
テストデータの例:
package mypackage
func f() {
fmt.Printf("%d", "not a number") // want <code>wrong type for arg</code>
}
配布形態
[編集]golang.org/x/tools/go/analysis は標準ライブラリではなく、
Go モジュールとして独立して管理されているパッケージです。
ただし、go vet や go fix がこのパッケージを利用しているため、
Go ツールチェーンのソース配布物には
src/cmd/vendor/golang.org/x/tools/go/analysis として含まれています。
ユーザーが自作の Analyzer を書く場合は、go.mod に依存を追加して利用します。
go get golang.org/x/tools/go/analysis
標準付属の Analyzer(passes)
[編集]go/analysis/passes/ 以下には以下の Analyzer が用意されています。
go vet や go fix に組み込まれているものも多くあります。
ctrlflow と inspect は診断を出力せず、他の Analyzer が利用する基盤的な passes です。
| Analyzer | 検出・処理内容 |
|---|---|
appends
|
append の引数が 1 つしかない呼び出し
|
asmdecl
|
アセンブリファイルと Go 宣言の不一致 |
assign
|
無意味な代入 |
atomic
|
sync/atomic パッケージの典型的な誤用
|
bools
|
論理演算子にまつわる典型的な誤り |
buildtag
|
ビルドタグの書式誤り |
cgocall
|
cgo のポインタ渡し規則の違反 |
composite
|
キーなし複合リテラル |
copylock
|
値渡しされたロック |
ctrlflow
|
制御フローグラフ(CFG)の生成(他の Analyzer が利用) |
defers
|
defer 文の典型的な誤り
|
directive
|
Go ツールチェーンディレクティブの誤り |
errorsas
|
errors.As の第 2 引数がポインタでない
|
framepointer
|
フレームポインタを保存前に破壊するアセンブリコード |
hostport
|
IPv4 専用となる net.Dial のアドレス書式
|
httpresponse
|
HTTP レスポンスの誤った取り扱い |
ifaceassert
|
成立しえないインターフェース間の型アサーション |
inline
|
//go:fix inline ディレクティブによるインライン展開
|
inspect
|
AST インスペクターの提供(他の Analyzer が利用) |
loopclosure
|
ネストした関数からのループ変数参照 |
lostcancel
|
context.CancelFunc の呼び忘れ
|
modernize
|
古い Go コードを新しい書き方に改善する提案 |
nilfunc
|
nil との無意味な関数比較 |
printf
|
fmt.Printf 系のフォーマット文字列と引数の不整合
|
shift
|
整数幅を超えるビットシフト |
sigchanyzer
|
signal.Notify へのバッファなしチャネルの渡し誤り
|
slog
|
log/slog のキーと値のペアの不整合
|
stdmethods
|
標準インターフェースに似たメソッドのシグネチャ誤り |
stdversion
|
使用中の Go バージョンより新しい標準ライブラリシンボルの参照 |
stringintconv
|
整数から文字列への型変換の誤り |
structtag
|
構造体フィールドタグの書式誤り |
testinggoroutine
|
テストのゴルーチン内からの Fatal 呼び出し
|
tests
|
テスト・サンプル関数の典型的な誤り |
timeformat
|
time.Format / time.Parse の不正なフォーマット文字列
|
unmarshal
|
Unmarshal 系関数への非ポインタ・非インターフェース渡し
|
unreachable
|
到達不能コード |
unsafeptr
|
uintptr から unsafe.Pointer への不正な変換
|
unusedresult
|
純粋関数の戻り値の未使用 |
waitgroup
|
sync.WaitGroup の単純な誤用
|
go vet で実行される Analyzer の一覧は go vet を、
go fix で利用されるものは go fix] を参照してください。