1. 各ステップの「学習内容」と「ゴール」を読み、追加したい診断や修正提案の振る舞いを整理します
  2. まず go test ./skeleton/stepX/... を実行し、現状のテストの失敗状況を確認して課題を把握します
  3. skeleton/stepX/analyzer.go を編集し、解析ロジックを少しずつ追加しながら再実行して振る舞いの変化を確かめます
  4. 期待どおりの診断や SuggestedFix が出るまで go test ./skeleton/stepX/... を繰り返し、挙動を検証します
  5. 完成形の考え方を整理するために、対応する solution/stepX/analyzer.go を読み、差分の理由を言語化します
  6. 行き詰まったら「ヒント」や参考資料で理解を補強し、再び skeleton に戻って実装をブラッシュアップします

実装タスク

skeleton/stepX/analyzer.go をテストが通るように修正してください:

cd suggestedfix/skeleton/stepX

# テストを実行
go test -v -count=1 .

前提条件

Goインストールの確認

go version

作業ディレクトリの構成

このワークショップでは、以下の構成で作業します:

suggestedfix/
├── skeleton/      # 各ステップのスケルトンコード
│   ├── step1/
│   │   ├── analyzer.go       # 実装する解析器
│   │   ├── analyzer_test.go  # テストコード
│   │   └── testdata/         # テスト用データ
│   ├── step2/
│   └── step3/
└── solution/      # 各ステップの完成コード
    ├── step1/
    ├── step2/
    └── step3/

Goモジュールの初期化

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 型)を検出する方法を学びます。

Inspector の仕組みと利点

Inspector とは

go/ast/inspector パッケージは、AST を効率的に走査するための最適化されたツールです。analysis パッケージでは、inspect.Analyzer を通じて提供されます。

内部動作の仕組み

Inspector は AST を事前にインデックス化することで高速な走査を実現します:

  1. 事前インデックス化: AST 全体を一度走査し、ノードタイプごとにインデックスを作成
  2. 型フィルタリング: 指定された型のノードのみを効率的に訪問
  3. メモリ共有: 複数のアナライザー間でインデックスを共有

Preorder と Postorder

// Preorder: 親ノードを子ノードより先に訪問
inspect.Preorder(nodeFilter, func(n ast.Node) {
    // 親→子の順序で処理
})

// Postorder: 子ノードを親ノードより先に訪問
inspect.Postorder(nodeFilter, func(n ast.Node) {
    // 子→親の順序で処理
})

nodeFilter の仕組み

// 型のゼロ値ポインタを使って、対象とする型を指定
nodeFilter := []ast.Node{
    (*ast.InterfaceType)(nil),  // InterfaceType のみ
    (*ast.FuncDecl)(nil),       // 複数指定も可能
}

この配列に指定された型のノードのみがコールバック関数に渡されます。

パフォーマンスの違い

大規模なコードベース(1000ファイル)での比較:

つまり、特定の型のノードのみを処理したい場合、Inspector は50倍以上高速です。

AST の基礎知識

AST とは何か

AST(Abstract Syntax Tree)は、ソースコードの構文構造を木構造で表現したものです。コンパイラやツールがコードを理解し操作するための中間表現として使用されます。

Go における AST ノード

Go の AST は go/ast パッケージで定義されており、すべてのノードは ast.Node インターフェースを実装しています:

type Node interface {
    Pos() token.Pos // ノードの開始位置
    End() token.Pos // ノードの終了位置
}

主要なノードタイプ:

AST の走査方法

AST を走査する主な方法は3つあります:

  1. ast.Inspect: 再帰的にすべてのノードを訪問
ast.Inspect(node, func(n ast.Node) bool {
    // すべてのノードに対して実行
    return true // false を返すと子ノードをスキップ
})
  1. ast.Walk: Visitor パターンを使用
type visitor struct{}
func (v *visitor) Visit(n ast.Node) ast.Visitor {
    // ノード処理
    return v
}
ast.Walk(&visitor{}, node)
  1. inspector.Preorder/inspector.Postorder: 効率的な型フィルタリング(後述)

InterfaceType ノードの詳細

// ソースコード例
var x interface{}           // 空のインターフェース
type Reader interface {     // メソッドを持つインターフェース
    Read([]byte) (int, error)
}

// AST表現
*ast.GenDecl                // 宣言ノード
  └── *ast.ValueSpec        // 値の仕様
      └── Type: *ast.InterfaceType  // インターフェース型
          └── Methods: *ast.FieldList // メソッドリスト
              └── List: []*ast.Field   // 各メソッド

実装で必要な知識

analysis.Pass の役割

type Pass struct {
    Analyzer   *Analyzer        // 実行中のアナライザー
    Fset       *token.FileSet   // ファイル位置情報
    Files      []*ast.File      // 解析対象ファイル
    ResultOf   map[*Analyzer]interface{} // 依存アナライザーの結果
    Report     func(Diagnostic) // 診断を報告
    // ... 他のフィールド
}

pass.ResultOf[inspect.Analyzer] から Inspector インスタンスを取得できます。

token.Pos の概念

AST ノードの位置は token.Pos で表現されます:

必要な修正箇所

  1. nodeFilter の設定
  2. Preorder コールバック内での診断報告

ポイント

ゴール

✅ 検出したインターフェース型から、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 // 行コメント
}

様々なインターフェース型の AST 表現

// 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 に型要素が含まれる

空のインターフェース判定の詳細

空のインターフェースかどうかを判定する際の考慮点:

  1. Methods が nil の場合: 明示的に interface{} と書かれた
  2. Methods.List が空配列の場合: interface { } のようにスペース付きで書かれた
  3. Methods.List に要素がある場合: メソッドまたは埋め込み型がある

Field の解釈

インターフェースの 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{})のみを検出するように修正します。

  1. 空のインターフェースの判定を追加
    • InterfaceType の Methods フィールドをチェック
    • 空の場合(nil または List が空)のみレポート
  2. 適切なメッセージで報告
    • より詳細な診断情報を提供
  3. solution を読み合わせ
    • solution/step2/analyzer.go を開き、条件判定の実装方法を確認

実装で必要な知識

型アサーションの安全性

Inspector を使用する場合、nodeFilter で指定した型のノードのみが渡されるため、型アサーションは常に成功します。しかし、防御的プログラミングとして ok パターンを使うことも可能です:

iface, ok := n.(*ast.InterfaceType)
if !ok {
    return // これは実際には到達しない
}

nil チェックの重要性

Go の AST では、オプショナルな要素は nil になることがあります:

両方のケースを考慮した条件分岐が必要です。

診断メッセージの一貫性

アナライザー全体で一貫したメッセージを使用するため、定数として定義することが推奨されます:

const message = "interface{} can be replaced with any"

なぜ空のインターフェースだけを対象にするのか?

// 置換対象(空のインターフェース)
var x interface{}  // → var x any

// 置換対象外(メソッドを持つインターフェース)
type Writer interface {
    Write([]byte) (int, error)  // これは置換しない
}

ポイント

✅ チェックリスト

ゴール

✅ 診断に自動修正機能を追加し、エディタやツールで簡単に適用できるようにする

学習内容

単なる警告だけでなく、具体的な修正方法を提供する SuggestedFix の実装方法を学びます。これにより、開発者は手動で修正する手間を省けます。

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 の動作原理

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() メソッドを使うべき

なぜ SuggestedFix が重要か?

手動修正 vs 自動修正

手動修正の問題点:

自動修正のメリット:

実装タスク

skeleton/step3/analyzer.go を修正:

Step2では空のインターフェースを検出できるようになりました。Step3では、これに自動修正機能を追加します。

  1. SuggestedFix を作成
  2. Diagnostic に追加
    • SuggestedFixes フィールドに設定
    • 既存の診断メッセージはそのまま
  3. 完成版で適用例を確認
    • solution/step3/analyzer.go を読解し、SuggestedFix の実装方法を理解

実装で必要な知識

DiagnosticSuggestedFix の関係

Diagnostic は問題を報告し、SuggestedFix はその解決方法を提供します:

pass.Report(analysis.Diagnostic{
    Pos:     pos,              // 診断範囲の開始
    End:     end,              // 診断範囲の終了
    Message: "問題の説明",      // ユーザーに表示される診断メッセージ
    SuggestedFixes: []analysis.SuggestedFix{
        // 0個以上の修正提案
    },
})

修正提案の構成要素

  1. Message: エディタの Quick Fix メニューに表示される説明
  2. TextEdits: 実際に適用される編集操作のリスト

実装時の考慮事項

実際の適用例

// 修正前
var data interface{}
func Process(v interface{}) {}

// 自動修正後
var data any
func Process(v any) {}

analysistest の仕組み

テストでは 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)に修正後のコードを保存し、テストで自動的に比較します。

ポイント

✅ チェックリスト

このコードラボで学んだこと:

📚 Step 1: Inspector の活用

📚 Step 2: 条件による絞り込み

📚 Step 3: 自動修正の提供

学んだ技術は以下のような場面で活用できます:

  1. コードマイグレーション
    • 古いAPIから新しいAPIへの移行
    • 非推奨機能の置き換え
  2. コード品質の向上
    • コーディング規約の自動適用
    • アンチパターンの検出と修正
  3. リファクタリング支援
    • 大規模な構造変更の自動化
    • 一貫性のある変更の適用
  1. 理解を深める
    • solution/* のコードを詳しく読み、実装の詳細を確認
    • なぜ ok パターンで型アサーションをチェックするのか理解する
  2. 応用練習
    • 他の修正提案(例:error チェック、命名規則)を実装してみる
    • 複数の SuggestedFix を提供するアナライザーを作成
  3. さらなる学習