x/tools/go/analysis: add GoMod, ... fields to Module
要約
概要
x/tools/go/analysisパッケージのModule型に、go/packagesパッケージで既に提供されているDir、GoMod等のフィールドを追加する提案です。これにより、解析ツール(リンター)の実装者が、解析対象パッケージのgo.modファイルのパスやモジュールディレクトリを直接取得できるようになります。
ステータス変更
(空) → active
本提案は2026年2月11日にproposal review groupによってactiveステータスに移行されました。提案者のAkihiroSuda氏が依存関係の評判チェックツールgosocialcheckの実装でgo.mod(および同一ディレクトリのgo.sum)へのアクセスが必要となったことが発端です。当初はGoModフィールドのみの追加提案でしたが、メンテナのadonovan氏がレビュー中に「go/packages.Moduleと共通するフィールドをすべて追加すべき」と提案し、より包括的なAPI追加へと発展しました。
技術的背景
現状の問題点
現在、golang.org/x/tools/go/analysis.Module型には以下の3つのフィールドしかありません(提案#66315で追加):
type Module struct {
Path string // モジュールパス
Version string // モジュールバージョン
GoVersion string // モジュールで使用されているGoバージョン
}
一方、golang.org/x/tools/go/packages.Moduleには10個のフィールドがあり、Dir(モジュールディレクトリ)やGoMod(go.modファイルのパス)など、リンター実装で有用な情報が含まれています。
回避策として、Pass.Fsetのファイル名からディレクトリを推測することは可能ですが、パッケージパスとモジュールパスのサフィックス処理が複雑で、エラーが発生しやすい実装になります。また、go build -modfile alternate.modのような代替go.modを指定するケースでは、filepath.Join(Dir, "go.mod")という単純な結合では正確なパスを得られません。
提案された解決策
analysis.Module型を拡張し、packages.Moduleとcmd/go/internal/modinfo.ModulePublicに共通するすべてのフィールドを追加します:
type Module struct {
Path string // モジュールパス
Version string // モジュールバージョン
Replace *Module // このモジュールに置き換えられる
Time *time.Time // バージョンが作成された時刻
Main bool // これがメインモジュールか
Indirect bool // メインモジュールの間接的な依存のみか
Dir string // モジュールファイルを保持するディレクトリ
GoMod string // モジュールロード時に使用されたgo.modファイルのパス
GoVersion string // モジュールで使用されているGoバージョン
Error *ModuleError // モジュールロード時のエラー
}
type ModuleError struct {
Err string // エラー本体
}
これによって何ができるようになるか
- 依存関係の評判チェック: gosocialcheckのようなツールが、
go.mod/go.sumにアクセスして依存モジュールがCNCF Graduatedプロジェクト等の信頼できる組織で採用されているかを検証できます。 - モジュール置換の検証:
Replaceフィールドにより、go.modのreplaceディレクティブを検出し、ローカル依存やフォーク使用を警告するリンターが実装可能になります。 - Goバージョン互換性チェック: 既存の
GoVersionに加え、Mainフィールドでメインモジュールを識別し、依存関係のGoバージョン要件と比較する解析が容易になります。 - ワークスペース対応解析: メインモジュールと外部依存を区別し、プロジェクト固有のルールと外部ライブラリ向けルールを使い分けることができます。
コード例
// Before: go.modパスの推測(複雑でエラーが起きやすい)
func run(pass *analysis.Pass) (interface{}, error) {
// Pass.Fsetから任意のファイルパスを取得
// Module.PathとPackage.Pathの差分を計算
// ディレクトリからサフィックスを削除してModule.Dirを推測
// filepath.Join(moduleDir, "go.mod") を構築
// ※-modfileオプション使用時は正しいパスにならない
}
// After: 直接アクセス可能
func run(pass *analysis.Pass) (interface{}, error) {
if pass.Module == nil {
return nil, nil // モジュール情報なし
}
if pass.Module.GoMod != "" {
// go.modファイルを直接読み取り
data, err := os.ReadFile(pass.Module.GoMod)
// go.sumは同じディレクトリにある
sumPath := strings.TrimSuffix(pass.Module.GoMod, ".mod") + ".sum"
}
if pass.Module.Main && pass.Module.Replace != nil {
pass.Reportf(pass.Files[0].Pos(),
"main module uses replace directive")
}
}
議論のハイライト
- 最小セットvs最大セット: 当初Timothy King氏が
{Path, Version, GoVersion}の最小セットを提案しましたが、最終的にadonovan氏が「将来の追加を避けるため、packages.Moduleの全フィールドを一度に追加すべき」と主張し、この方針が採用されました。 - 代替ビルドシステムへの配慮: Bazel、Pants、Buck等の
go.modを使用しないビルドシステムでもフィールドを埋められるかが確認され、BazelのrulegoメンテナやPlease開発者から実装可能との回答を得ました。TimeやIndirect等の一部フィールドが埋められない可能性がありますが、部分的な情報でもPass.Module != nilで提供される方針です。 - GoModとDirの両方が必要な理由: コメントで「
DirがあればGoModはfilepath.Join(Dir, "go.mod")で計算できるのでは」という疑問が出ましたが、sudo-bmitch氏がgo build -modfile alternate.modのように代替go.modファイルを指定できるケースを指摘し、GoModフィールドの必要性が確認されました。 - nil処理:
Pass.Moduleはドライバによってはnilになる可能性があるため、解析ツール側で必ずnilチェックが必要です。また、Versionフィールドは(devel)や""(ワークスペースモジュール)になる場合があります。 - 実装PR: golang/tools#577で初期実装が提出されていますが、adonovan氏によるHold+1のため、フィールド追加の方針確定待ちの状態です。