タラバガニー設計局stalins.clubNOTE/notes/generic-method-static-analysis

静的解析から見たジェネリクスメソッド

Go 1.27 のジェネリクスメソッドは、go/types や go/ssa を使うツールから見ると、他の種類のメソッドとは違う挙動をいくつも持つ。象限の定義は メソッドの4象限、MethodSet まわりの前提は types.MethodSet (メソッドセット) を参照。以下は go1.27rc3 (darwin/arm64) / x/tools v0.49.0 で実測した挙動。

1. ssa.Program.MethodValue が generic method に対して nil を返す。 types.MethodSet (メソッドセット) にある通り types.MethodSet には入るので、1.18 以降広く書かれてきた

mset := prog.MethodSets.MethodSet(T)
for sel := range mset.Methods() {
    function(prog.MethodValue(sel))  // generic method で nil
}

の形がそのまま nil 参照で panic する。これが ssautil.AllFunctions そのものであり、SSA ベースの linter が軒並み踏んだ。現在の x/tools は sel.Obj().(*types.Func).Signature().TypeParams() == nil で弾いている。

2. ssa.Function.TypeParams() は go/types の同名メソッドと別物。 SSA 側はレシーバ分とメソッド自身の型パラメータを連結して持つため、(Gen[int]).D[string]TypeArgs=[int string] になる。SSA の fn.TypeParams().Len() > 0 では判定できず、判定は必ず go/types 側で行う必要がある。

3. Selection.Obj() が象限ごとに違うものを返す。 C 象限 (o.C) は origin (Q が残ったまま) が返るが、B 象限 (g.B) は合成オブジェクト、D 象限 (g.D) は「P だけ置換され Q は残る」半分だけインスタンス化された状態になる。typeutil.Callee が doc と食い違っていた #80533 の背景で、現在は Func.Origin() で正規化する修正が入っている。

4. 型引数が2箇所に分かれている。 Info.Instances はメソッド自身の型引数だけを持ち、g.DP=int はレシーバ型 Gen[int] から別途取る必要がある。コンパイラの dictionary 構築と同じ考え方 (GC shape stenciling と dictionary)。

5. generic method はパッケージメンバにならない。 p.Func("E") (generic function) は取れるが p.Func("C") (generic method) は nil。「パッケージ内の全関数を列挙する」形のコードはジェネリック関数を拾ってもジェネリックメソッドを静かに取りこぼす。

6. 呼び出しは必ず static で invoke にはならない。 interface が generic method を持てない以上、動的ディスパッチの的になることは原理的にありえない。x/tools の go/callgraph/rta/rta.go のコメント // skip generic methods since interfaces don't have them が一行で言い切っている。

7. BuilderMode によって見えるものが変わる。 デフォルトでは未インスタンス化の (Ord).C(Ord).C[int](Ord).C[string] が別々の *ssa.Function として3回来るが、InstantiateGenerics を付けるとインスタンスだけになる。

一度直して漏れた例として govulncheck v1.4.0 の panic (#80055) がある。最初の修正 CL 788380 は「メソッド自身が型パラメータを持つ場合」しか弾いておらず、generic method の中のクロージャが parameterized type を interface にボックス化する経路が残っていた (panic: ForEachElement called on type containing *types.TypeParam)。修正は CL 786280。教訓は、型パラメータは「シグネチャ」「レシーバ」「本体のクロージャ」の3経路から入ってくるということ。

言語側では「generic method は interface を満たさない」(ジェネリクスメソッドはインターフェースを満たさない) が妥協点として語られるが、静的解析側ではこれは純粋な単純化として効いている。invoke の的になり得ないぶん、呼び出しグラフ構築の候補から最初から外してよいからである。実際に壊れたツールの一覧は linter が Go 1.27 に追いついていない

出典

Go Release Party 1.27 の発表資料リポジトリ static-analysis.md §2〜§6、memo.md §2、#80055#80533

▸ ノート一覧に戻る