go test ./skeleton/stepX/...
を実行し、現状のテストの失敗状況を確認して課題を把握しますskeleton/stepX/analyzer.go
を編集し、解析ロジックを少しずつ追加しながら再実行して振る舞いの変化を確かめますgo test ./skeleton/stepX/...
を繰り返し、挙動を検証しますsolution/stepX/analyzer.go
を読み、差分の理由を言語化しますskeleton/stepX/analyzer.go
をテストが通るように修正してください:
cd suggestedfix/skeleton/stepX
# テストを実行
go test -v -count=1 .
go version
このワークショップでは、以下の構成で作業します:
suggestedfix/
├── skeleton/ # 各ステップのスケルトンコード
│ ├── step1/
│ │ ├── analyzer.go # 実装する解析器
│ │ ├── analyzer_test.go # テストコード
│ │ └── testdata/ # テスト用データ
│ ├── step2/
│ └── step3/
└── solution/ # 各ステップの完成コード
├── step1/
├── step2/
└── step3/
cd suggestedfix
go mod init suggestedfix
go get golang.org/x/tools/go/analysis
go get golang.org/x/tools/go/analysis/passes/inspect
go get golang.org/x/tools/go/ast/inspector
✅ inspector
を使用して AST から interface{}
型リテラルを効率的に見つける
静的解析ツールを作成する第一歩として、コードから特定の型(今回は interface 型)を検出する方法を学びます。
go/ast/inspector
パッケージは、AST を効率的に走査するための最適化されたツールです。analysis パッケージでは、inspect.Analyzer
を通じて提供されます。
Inspector は AST を事前にインデックス化することで高速な走査を実現します:
// Preorder: 親ノードを子ノードより先に訪問
inspect.Preorder(nodeFilter, func(n ast.Node) {
// 親→子の順序で処理
})
// Postorder: 子ノードを親ノードより先に訪問
inspect.Postorder(nodeFilter, func(n ast.Node) {
// 子→親の順序で処理
})
// 型のゼロ値ポインタを使って、対象とする型を指定
nodeFilter := []ast.Node{
(*ast.InterfaceType)(nil), // InterfaceType のみ
(*ast.FuncDecl)(nil), // 複数指定も可能
}
この配列に指定された型のノードのみがコールバック関数に渡されます。
大規模なコードベース(1000ファイル)での比較:
つまり、特定の型のノードのみを処理したい場合、Inspector は50倍以上高速です。
AST(Abstract Syntax Tree)は、ソースコードの構文構造を木構造で表現したものです。コンパイラやツールがコードを理解し操作するための中間表現として使用されます。
Go の AST は go/ast
パッケージで定義されており、すべてのノードは ast.Node
インターフェースを実装しています:
type Node interface {
Pos() token.Pos // ノードの開始位置
End() token.Pos // ノードの終了位置
}
主要なノードタイプ:
ast.Expr
を実装*ast.Ident
: 識別子(変数名、型名など)*ast.BasicLit
: リテラル(数値、文字列など)*ast.CallExpr
: 関数呼び出し*ast.InterfaceType
: インターフェース型ast.Stmt
を実装*ast.AssignStmt
: 代入文*ast.IfStmt
: if 文*ast.ForStmt
: for 文ast.Decl
を実装*ast.GenDecl
: 汎用宣言(var, const, type, import)*ast.FuncDecl
: 関数宣言AST を走査する主な方法は3つあります:
ast.Inspect(node, func(n ast.Node) bool {
// すべてのノードに対して実行
return true // false を返すと子ノードをスキップ
})
type visitor struct{}
func (v *visitor) Visit(n ast.Node) ast.Visitor {
// ノード処理
return v
}
ast.Walk(&visitor{}, node)
// ソースコード例
var x interface{} // 空のインターフェース
type Reader interface { // メソッドを持つインターフェース
Read([]byte) (int, error)
}
// AST表現
*ast.GenDecl // 宣言ノード
└── *ast.ValueSpec // 値の仕様
└── Type: *ast.InterfaceType // インターフェース型
└── Methods: *ast.FieldList // メソッドリスト
└── List: []*ast.Field // 各メソッド
type Pass struct {
Analyzer *Analyzer // 実行中のアナライザー
Fset *token.FileSet // ファイル位置情報
Files []*ast.File // 解析対象ファイル
ResultOf map[*Analyzer]interface{} // 依存アナライザーの結果
Report func(Diagnostic) // 診断を報告
// ... 他のフィールド
}
pass.ResultOf[inspect.Analyzer]
から Inspector インスタンスを取得できます。
AST ノードの位置は token.Pos で表現されます:
pass.Fset
を使って実際のファイル位置に変換可能Pos()
はノードの開始位置、End()
は終了位置inspector.Preorder
で特定の型のノードのみを効率的に走査*ast.InterfaceType
)を理解することが重要pass.Report
で検出結果を報告(Diagnostic 構造体を使用)✅ 検出したインターフェース型から、interface{}
のみを特定して報告する
すべてのインターフェース型を検出するだけでなく、メソッドを持たない空のインターフェース(interface{}
)のみを特定する方法を学びます。
InterfaceType
の詳細構造InterfaceType
struct の定義InterfaceType
の構造:
type InterfaceType struct {
Interface token.Pos // "interface" キーワードの位置
Methods *FieldList // メソッドのリスト
Incomplete bool // 構文エラーがある場合 true
}
type FieldList struct {
Opening token.Pos // 開き括弧 '{' の位置
List []*Field // フィールド(メソッド)のリスト
Closing token.Pos // 閉じ括弧 '}' の位置
}
type Field struct {
Doc *CommentGroup // ドキュメントコメント
Names []*Ident // フィールド/メソッド名
Type Expr // 型表現
Tag *BasicLit // フィールドタグ(インターフェースでは nil)
Comment *CommentGroup // 行コメント
}
// 1. 空のインターフェース(Go 1.18以前のスタイル)
var x interface{}
// → Methods: nil または Methods.List: []
// 2. any 型(Go 1.18以降)
var y any
// → これは *ast.Ident であり、*ast.InterfaceType ではない!
// 3. メソッドを持つインターフェース
type Writer interface {
Write([]byte) (int, error)
}
// → Methods.List: []*Field{...} (1つの要素)
// 4. 埋め込みインターフェース
type ReadWriter interface {
io.Reader
io.Writer
}
// → Methods.List: []*Field{...} (2つの要素、Names は nil)
// 5. 型制約インターフェース(ジェネリクス)
type Number interface {
~int | ~float64
}
// → Methods.List に型要素が含まれる
空のインターフェースかどうかを判定する際の考慮点:
interface{}
と書かれたinterface { }
のようにスペース付きで書かれたインターフェースの Field は以下のパターンがあります:
// メソッド定義
Read([]byte) (int, error)
// → Names: []*Ident{"Read"}
// → Type: *ast.FuncType
// 埋め込みインターフェース
io.Reader
// → Names: nil
// → Type: *ast.SelectorExpr または *ast.Ident
// 型要素(ジェネリクス)
~int
// → 特殊な型表現
skeleton/step2/analyzer.go
を修正:
Step1では全てのインターフェース型を検出しましたが、Step2では空のインターフェース(interface{}
)のみを検出するように修正します。
solution/step2/analyzer.go
を開き、条件判定の実装方法を確認Inspector を使用する場合、nodeFilter で指定した型のノードのみが渡されるため、型アサーションは常に成功します。しかし、防御的プログラミングとして ok
パターンを使うことも可能です:
iface, ok := n.(*ast.InterfaceType)
if !ok {
return // これは実際には到達しない
}
Go の AST では、オプショナルな要素は nil になることがあります:
Methods
フィールドが nil の場合Methods.List
が nil の場合(通常は空スライス []
ですが)両方のケースを考慮した条件分岐が必要です。
アナライザー全体で一貫したメッセージを使用するため、定数として定義することが推奨されます:
const message = "interface{} can be replaced with any"
// 置換対象(空のインターフェース)
var x interface{} // → var x any
// 置換対象外(メソッドを持つインターフェース)
type Writer interface {
Write([]byte) (int, error) // これは置換しない
}
Methods
フィールド)を理解する✅ 診断に自動修正機能を追加し、エディタやツールで簡単に適用できるようにする
単なる警告だけでなく、具体的な修正方法を提供する SuggestedFix の実装方法を学びます。これにより、開発者は手動で修正する手間を省けます。
type Diagnostic struct {
Pos token.Pos // 診断の開始位置
End token.Pos // 診断の終了位置
Category string // カテゴリ(オプション)
Message string // エラーメッセージ
SuggestedFixes []SuggestedFix // 修正提案(複数可)
Related []RelatedInfo // 関連情報
}
type SuggestedFix struct {
Message string // 修正の説明(エディタに表示)
TextEdits []TextEdit // 実際の編集内容
}
type TextEdit struct {
Pos token.Pos // 編集開始位置
End token.Pos // 編集終了位置
NewText []byte // 置換後のテキスト
}
TextEdit は、ソースコードの特定範囲を新しいテキストで置換します:
// 元のコード: "interface{}"
// Pos: 'i' の位置
// End: '}' の次の位置
// NewText: []byte("any")
// 結果: "any"
重要な特性:
1つの診断に複数の修正方法を提案できます:
SuggestedFixes: []analysis.SuggestedFix{
{
Message: "Replace with any",
TextEdits: []analysis.TextEdit{{
Pos: pos, End: end,
NewText: []byte("any"),
}},
},
{
Message: "Replace with generic type",
TextEdits: []analysis.TextEdit{{
Pos: pos, End: end,
NewText: []byte("T"),
}},
},
}
// 正しい位置の取得
iface := n.(*ast.InterfaceType)
pos := iface.Pos() // "interface" キーワードの開始位置
end := iface.End() // "}" の次の位置
// 間違った例
pos := iface.Interface // これも同じだが、Pos() メソッドを使うべき
手動修正の問題点:
自動修正のメリット:
skeleton/step3/analyzer.go
を修正:
Step2では空のインターフェースを検出できるようになりました。Step3では、これに自動修正機能を追加します。
interface{}
→ any
)solution/step3/analyzer.go
を読解し、SuggestedFix の実装方法を理解Diagnostic は問題を報告し、SuggestedFix はその解決方法を提供します:
pass.Report(analysis.Diagnostic{
Pos: pos, // 診断範囲の開始
End: end, // 診断範囲の終了
Message: "問題の説明", // ユーザーに表示される診断メッセージ
SuggestedFixes: []analysis.SuggestedFix{
// 0個以上の修正提案
},
})
// 修正前
var data interface{}
func Process(v interface{}) {}
// 自動修正後
var data any
func Process(v any) {}
テストでは analysistest
パッケージを使用しており、testdata/src/a/a.go
のコメントで期待される診断を指定しています:
func example1(x interface{}) interface{} { // want "interface{} can be replaced with any"
return x
}
// want
コメントがある行で、指定されたメッセージの診断が報告されることを検証します。
更に analysistest.RunWithSuggestedFixes を使用することで、SuggestedFixes の適用もテストできます。 golden ファイル(a.go.golden
)に修正後のコードを保存し、テストで自動的に比較します。
このコードラボで学んだこと:
学んだ技術は以下のような場面で活用できます:
solution/*
のコードを詳しく読み、実装の詳細を確認ok
パターンで型アサーションをチェックするのか理解するsinglechecker
と multichecker
の違いを理解する