x/tools/go/analysis/analysistest: support expectations for related information
要約
概要
analysistest パッケージの // want コメントを拡張し、解析器(Analyzer)が出す診断の「関連情報(analysis.Diagnostic.Related)」もテストで検証できるようにする提案です。行に名前を付ける新しい // anchor コメントも追加されます。
ステータス変更
likely_accept → accepted
2026年11月4日に likely accept と判断され、その後1週間異論が出なかったため、11月12日に @aclements が「No change in consensus」として承認しました。最終的な仕様は、提案者 @tcard の当初案ではなく、@aclements が出した「anchor 方式」の対案です。x/tools のメンテナーである @adonovan もこの方式を支持しました。
技術的背景
現状の問題点
現在の want コメントで検証できるのは、診断メッセージとファクト(Fact。解析器がパッケージ間で受け渡す情報)だけです。
// want "diag" "diag2" x:"fact1"
診断のカテゴリ、URL、修正候補(suggested fixes)、関連情報は検証できません。これらを確かめるには analysistest.Run が返す Result を自分で調べるしかありません。提案者は、これでは素の analysis/checker を使うのとほとんど変わらず、analysistest を使う意味が薄れると指摘しました。今回の提案は、このうち関連情報だけを対象にしています。
関連情報をテストする手段がないことは、標準の解析器の設計にも影響していました。たとえば lostcancel は、本来なら Related で示すべき位置を "... defined on line %d" のようにメッセージ本文に行番号として埋め込んでいます。
提案された解決策(最終仕様)
// anchor <name>コメント: ファイル内の行に名前を付けます。コメントを探すルールは// wantと同じです。名前は[a-zA-Z_][a-zA-Z0-9_]*に一致する必要があります。@<anchor>:<string>フィールド:// wantに追加できる新しい項目です。<anchor>はパッケージ内のどのファイルにあるアンカー名でも指定できます。<string>は Go の文字列リテラルで書いた正規表現で、RelatedInformation.Messageと照合します。@*:"..."と書くと位置を無視してメッセージだけを照合します。関連情報がパッケージ外の位置を指す場合に使います。
- 照合のルール
want行に関連情報フィールドが1つでもある場合は、Diagnostic.Relatedスライス全体と、順序どおりに完全一致しなければなりません。- 関連情報フィールドがない場合は、その行の診断の関連情報は無視されます。そのため既存のテストは影響を受けません。
これによって何ができるようになるか
- 「この変数の宣言はここ」「この return 文が問題」のように、別の位置を指す関連情報を、位置まで含めて検証できます。
- テストで検証できるようになるので、解析器を書く人は行番号をメッセージに埋め込むのをやめて、Related を使う設計に移りやすくなります。
- 関連情報は CI でよく使われる singlechecker などのコマンドラインツールでは常に出力されるため、エディタの対応状況に関係なく品質を保証できます。
- 位置の指定に行番号ではなく名前を使うので、離れた場所に行を挿入してもテストが壊れません。
コード例
// Before: Result を手動で調べる必要があった
results := analysistest.Run(t, testdata, lostcancel.Analyzer, "a")
for _, r := range results {
for _, d := range r.Diagnostics {
if len(d.Related) != 1 || !strings.Contains(d.Related[0].Message, "declaration of var") {
t.Errorf("unexpected related info: %v", d.Related)
}
// 位置の検証には Fset から行番号を引く処理がさらに必要
}
}
// After: テストデータの中で宣言的に書ける
func f(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx) // anchor decl
if cond {
return // want "this return statement may be reached" @decl:"declaration of var cancel"
}
cancel()
}
// パッケージ外の位置を指す関連情報は、メッセージだけで照合する
x() // want "diag" @*:"defined in another module"
議論のハイライト
- 行番号の指定方法の変遷: 当初案は
20:"extra"のような絶対行番号でした。@adonovan が壊れやすいと指摘し、+3や-3といった相対行番号の案に改められました。最終的には @aclements の提案した名前付きアンカー方式が採用されました。@adonovan は、gopls の marker テストの@locに近い仕組みだとして賛成しました。 - 終了位置(End)は検証しない: 相対行番号の
-が、範囲を表すPos-Endの区切りと衝突するため、End の検証は外されました。もともと通常の診断でも End は検証していません。 - 照合の厳しさの変更: 当初案は「マッチしない関連情報があってもよい」という緩い照合でした。最終仕様では、関連情報を1つでも書いた場合は全体を順序どおりに完全一致させる、厳密な照合になりました。
- クライアント対応の問題: @adonovan が lostcancel に Related を使うパッチを当てて試したところ、表示できたのは VS Code だけで、Emacs (eglot)、Zed、Helix では表示されませんでした。これに対して提案者は、解析器は最終的に CI で動くことが多く、その場合 Related は必ず出力されると反論し、受け入れられました。
- 今後の展望: @adonovan は、analysistest に出力が型チェックを通るかの確認や txtar 形式のテストデータ対応などを加える構想を示しました。ただし、どれも今回の提案とは独立した話だとしています。