コンテンツにスキップ

Go/analysis

出典: フリー教科書『ウィキブックス(Wikibooks)』
< Go

Go Analysis フレームワーク は、Go プログラムの静的解析ツールを作成するためのフレームワークです。 正式なパッケージパスは golang.org/x/tools/go/analysis であり、標準ライブラリには含まれませんが、Go のツールチェーン配布物の src/cmd/vendor/golang.org/x/tools/go/analysis として同梱されています。 go vet および go fix はこのフレームワークを利用して実装されています。

概要

[編集]

静的解析(static analysis)とは、プログラムを実際に実行せずにソースコードを検査し、誤りや改善点を報告する処理です。go/analysis はそのような解析を「モジュラー(modular)」に、すなわちパッケージ単位で独立して実行できるよう設計されています。

フレームワークの役割は、解析ロジック(Analyzer)と、それを実行する駆動プログラム(Driver)とのインターフェースを統一することにあります。 これにより、同一の Analyzer を go vet、IDE、CI システムなど様々な Driver で再利用できます。

┌─────────────────────────────┐
│  Driver(go vet / go fix / 独自ツール など)             │
│                                                          │
│   ┌─────┐   ┌─────┐   ┌─────┐       │
│   │Analyzer A│   │Analyzer B│   │Analyzer C│  ...  │
│   └─────┘   └─────┘   └─────┘       │
└─────────────────────────────┘

主要な型

[編集]

Analyzer

[編集]

Analyzer は解析器を静的に記述する型です。 解析の名前・ドキュメント・依存関係・実行関数などをまとめて定義します。

type Analyzer struct {
    Name             string        // コマンドラインなどで使う識別子(Go の識別子として有効であること)
    Doc              string        // ドキュメント。最初の "\n\n" より前の部分がタイトルになる
    URL              string        // 追加情報へのリンク(省略可)
    Flags            flag.FlagSet  // 解析の動作を制御するフラグ群
    Run              func(*Pass) (any, error) // 解析本体
    RunDespiteErrors bool          // 型エラーがあるパッケージでも実行するか
    Requires         []*Analyzer   // 事前に実行が必要な他の Analyzer(水平依存)
    ResultType       reflect.Type  // Run が返す結果の型
    FactTypes        []Fact        // パッケージ間で共有する Fact の型(垂直依存)
}

典型的な定義例を以下に示します。

package unusedresult

var Analyzer = &analysis.Analyzer{
    Name: "unusedresult",
    Doc:  "check for unused results of calls to some functions",
    Run:  run,
}

func run(pass *analysis.Pass) (interface{}, error) {
    // 解析ロジックをここに記述する
    return nil, nil
}

複数の Analyzer を Driver に登録するには次のようにします。

import (
    "unusedresult"
    "nilness"
    "printf"
)

var analyses = []*analysis.Analyzer{
    unusedresult.Analyzer,
    nilness.Analyzer,
    printf.Analyzer,
}

Pass

[編集]

Pass は「特定の Analyzer を特定のパッケージに適用する 1 回の作業単位」を表す。 Run 関数に渡され、解析に必要な情報の取得と診断の報告を行うためのインターフェースとなります。

type Pass struct {
    Analyzer     *Analyzer        // 実行中の Analyzer
    Fset         *token.FileSet   // ファイル位置情報
    Files        []*ast.File      // 各ファイルの構文木(AST)
    OtherFiles   []string         // Go 以外のファイル名(アセンブリなど)
    IgnoredFiles []string         // 現在のビルド設定では無視されるファイル名
    Pkg          *types.Package   // パッケージの型情報
    TypesInfo    *types.Info      // 構文木の型情報
    TypeErrors   []types.Error    // 型エラー(RunDespiteErrors が true のとき)
    Module       *Module          // パッケージが属するモジュール情報
    ResultOf     map[*Analyzer]any // 依存 Analyzer の実行結果
    Report       func(Diagnostic) // 診断メッセージを報告する関数
    ReadFile     func(filename string) ([]byte, error) // ファイル内容を読み込む関数
    // Fact のインポート/エクスポート関数(後述)
}

診断を報告する際には、便利なヘルパーメソッドが用意されています。

メソッド 説明
pass.Reportf(pos, format, args...) 位置とフォーマット文字列で診断を報告する
pass.ReportRangef(rng, format, args...) AST ノードの範囲(Range インターフェース)で診断を報告する

Diagnostic

[編集]

Diagnostic は解析が検出した問題を表す型です。

type Diagnostic struct {
    Pos            token.Pos          // 問題のある位置
    End            token.Pos          // 問題の終端(省略可)
    Category       string             // 分類(省略可)
    Message        string             // メッセージ本文
    URL            string             // 追加ドキュメントへのリンク(省略可)
    SuggestedFixes []SuggestedFix     // 修正提案のリスト(省略可)
    Related        []RelatedInformation // 関連情報(省略可)
}

SuggestedFixTextEdit の集合として修正内容を表し、 IDE やエディタがユーザーに自動修正を提案する際に利用されます。 Diagnostic に severity(重大度)フィールドがないのは設計上の判断であり、 重要度の判断は Driver やユーザー設定に委ねられています。

Fact

[編集]

Fact は、パッケージをまたいで解析結果を伝播させるための中間情報を表すインターフェースです。 これがモジュラー解析を実現する核心的な仕組みです。

コンパイラの型チェッカーが別コンパイルで型情報をエクスポートするのと同様に、Fact は解析情報を「別解析」として伝播させる。

パッケージ Q を解析
  └─ "log.Printf は printf wrapper だ" という Fact を記録
        ↓ gob でシリアライズ・保存
パッケージ P を解析(Q をインポートしている)
  └─ Q の Fact を読み込み → P 内の log.Printf 呼び出しもチェック

Fact を利用する Analyzer は FactTypes に型を宣言する必要があります。

var Analyzer = &analysis.Analyzer{
    Name:      "printf",
    FactTypes: []analysis.Fact{new(isWrapper)},
    // ...
}

type isWrapper struct{} // *types.Func f が「printf wrapper である」ことを表す

func (*isWrapper) AFact() {} // Fact インターフェースを満たすためのダミーメソッド

Fact の記録と取得には Pass の以下の関数を使用します。

// オブジェクトへの Fact の記録・取得
pass.ExportObjectFact(obj, new(isWrapper))

var fact isWrapper
if pass.ImportObjectFact(obj, &fact) {
    // fact が存在する場合の処理
}

// パッケージへの Fact の記録・取得
pass.ExportPackageFact(fact)
pass.ImportPackageFact(pkg, fact)

Fact は encoding/gob でシリアライズされるため、決定的なエンコーディングが求められます。 また、Fact の型はポインタでなければなりません。

Analyzer の依存関係

[編集]

依存には 2 種類あります。

種別 フィールド 方向 説明
水平依存 Requires 同一パッケージ・異なる Analyzer 別の Analyzer の実行結果を Pass.ResultOf で受け取る
垂直依存 FactTypes 同一 Analyzer・異なるパッケージ Fact を介して上位パッケージに情報を伝播させる
水平依存(Requires)           垂直依存(FactTypes)

  inspect ──→ printf           pkg Q の解析
  (同パッケージ内で              └─ Fact を記録
   結果を受け渡す)                      ↓
                               pkg P の解析(Q に依存)
                                 └─ Q の Fact を参照

水平依存のグラフは非循環(DAG)でなければなりません。 analysis.Validate 関数で循環の有無を含む基本的な整合性チェックができます。

スタンドアロンコマンドの作成

[編集]

singlechecker および multichecker サブパッケージを使うと、Analyzer をコマンドとして簡単に公開できます。

単一 Analyzer のコマンド

[編集]
package main

import (
    "golang.org/x/tools/go/analysis/passes/findcall"
    "golang.org/x/tools/go/analysis/singlechecker"
)

func main() { singlechecker.Main(findcall.Analyzer) }

複数 Analyzer のコマンド

[編集]
package main

import (
    "golang.org/x/tools/go/analysis/multichecker"
    "golang.org/x/tools/go/analysis/passes/printf"
    "golang.org/x/tools/go/analysis/passes/nilness"
    "golang.org/x/tools/go/analysis/passes/shadow"
)

func main() {
    multichecker.Main(
        printf.Analyzer,
        nilness.Analyzer,
        shadow.Analyzer,
    )
}

テスト

[編集]

analysistest サブパッケージを使うと、Analyzer のテストを簡潔に記述できます。 テストデータのソースコード内に // want ... コメントを書くことで、期待される診断メッセージを宣言的に指定できます。

import "golang.org/x/tools/go/analysis/analysistest"

func TestAnalyzer(t *testing.T) {
    testdata := analysistest.TestData()
    analysistest.Run(t, testdata, MyAnalyzer, "mypackage")
}

テストデータの例:

package mypackage

func f() {
    fmt.Printf("%d", "not a number") // want <code>wrong type for arg</code>
}

配布形態

[編集]

golang.org/x/tools/go/analysis は標準ライブラリではなく、 Go モジュールとして独立して管理されているパッケージです。 ただし、go vetgo fix がこのパッケージを利用しているため、 Go ツールチェーンのソース配布物には src/cmd/vendor/golang.org/x/tools/go/analysis として含まれています。

ユーザーが自作の Analyzer を書く場合は、go.mod に依存を追加して利用します。

go get golang.org/x/tools/go/analysis

標準付属の Analyzer(passes)

[編集]

go/analysis/passes/ 以下には以下の Analyzer が用意されています。 go vetgo fix に組み込まれているものも多くあります。 ctrlflowinspect は診断を出力せず、他の Analyzer が利用する基盤的な passes です。

Analyzer 検出・処理内容
appends append の引数が 1 つしかない呼び出し
asmdecl アセンブリファイルと Go 宣言の不一致
assign 無意味な代入
atomic sync/atomic パッケージの典型的な誤用
bools 論理演算子にまつわる典型的な誤り
buildtag ビルドタグの書式誤り
cgocall cgo のポインタ渡し規則の違反
composite キーなし複合リテラル
copylock 値渡しされたロック
ctrlflow 制御フローグラフ(CFG)の生成(他の Analyzer が利用)
defers defer 文の典型的な誤り
directive Go ツールチェーンディレクティブの誤り
errorsas errors.As の第 2 引数がポインタでない
framepointer フレームポインタを保存前に破壊するアセンブリコード
hostport IPv4 専用となる net.Dial のアドレス書式
httpresponse HTTP レスポンスの誤った取り扱い
ifaceassert 成立しえないインターフェース間の型アサーション
inline //go:fix inline ディレクティブによるインライン展開
inspect AST インスペクターの提供(他の Analyzer が利用)
loopclosure ネストした関数からのループ変数参照
lostcancel context.CancelFunc の呼び忘れ
modernize 古い Go コードを新しい書き方に改善する提案
nilfunc nil との無意味な関数比較
printf fmt.Printf 系のフォーマット文字列と引数の不整合
shift 整数幅を超えるビットシフト
sigchanyzer signal.Notify へのバッファなしチャネルの渡し誤り
slog log/slog のキーと値のペアの不整合
stdmethods 標準インターフェースに似たメソッドのシグネチャ誤り
stdversion 使用中の Go バージョンより新しい標準ライブラリシンボルの参照
stringintconv 整数から文字列への型変換の誤り
structtag 構造体フィールドタグの書式誤り
testinggoroutine テストのゴルーチン内からの Fatal 呼び出し
tests テスト・サンプル関数の典型的な誤り
timeformat time.Format / time.Parse の不正なフォーマット文字列
unmarshal Unmarshal 系関数への非ポインタ・非インターフェース渡し
unreachable 到達不能コード
unsafeptr uintptr から unsafe.Pointer への不正な変換
unusedresult 純粋関数の戻り値の未使用
waitgroup sync.WaitGroup の単純な誤用

go vet で実行される Analyzer の一覧は go vet を、 go fix で利用されるものは go fix] を参照してください。

関連項目

[編集]
  • go vetGo Analysis フレームワーク を用いた静的検査コマンド
  • go fixGo Analysis フレームワーク を用いたコード自動修正コマンド

外部リンク

[編集]