プログラミング
Go分析フレームワーク:Goチームによるモジュラー静的解析
Go Analysis Framework: modular static analysis by go team (pkg.go.dev)
要約
この記事は、Go言語のコードに対するモジュラー静的解析ツールを構築するためのフレームワークである`go/analysis`パッケージを紹介しています。このフレームワークは、解析ロジックを`Analyzer`として定義し、`Pass`オブジェクトを通じてパッケージ情報や他の解析結果にアクセスできるようにします。これにより、`vet`のようなコマンドラインツールからIDEまで、様々なドライバープログラムで解析機能を容易に統合・再利用できます。
全文翻訳
ドキュメント ¶
概要 ¶
背景 ¶
静的解析とは、Goコードのパッケージを検査し、一連の診断(通常はコードのエラー)を報告する関数、あるいはリファクタリングの提案やその他の事実といった他の結果を生成する関数です。エラーを報告する解析は、非公式には「チェッカー」と呼ばれます。例えば、printfチェッカーはfmt.Printfフォーマット文字列のエラーを報告します。「モジュラー」解析とは、一度に1つのパッケージを検査するものの、低レベルのパッケージから情報を保存し、高レベルのパッケージを検査する際にそれを使用できる解析であり、ツールチェーンにおける別コンパイルに類似しています。printfチェッカーはモジュラーです。関数(例: log.Fatalf)がfmt.Printfに委譲していることを発見すると、その事実を記録し、他のパッケージからの呼び出しも含め、その関数への呼び出しもチェックします。
共通のインターフェースを実装することで、様々なソースからのチェッカーを、コマンドラインツール(vetなど)、テキストエディタやIDE、ビルド・テストシステム(go build、Bazel、Buckなど)、テストフレームワーク、コードレビューツール、コードベースインデクサー(SourceGraphなど)、ドキュメントビューア(godocなど)、大規模コードベース向けのバッチパイプラインなど、幅広いドライバープログラムに容易に選択、組み込み、再利用できます。
アナライザー ¶
APIの主要な型はAnalyzerです。Analyzerは、解析関数を静的に記述します:その名前、ドキュメント、フラグ、他のアナライザーとの関係、そしてもちろんそのロジックです。解析を定義するために、ユーザーはAnalyzer型の(論理的に定数である)変数を宣言します。以下は、go/analysis/passes/サブディレクトリ内のアナライザーの1つからの典型的な例です。
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) {
...
}
解析ドライバーは、一連の解析を実行し、それらが報告する診断を出力するvetのようなプログラムです。ドライバープログラムは、必要なAnalyzerのリストをインポートする必要があります。通常、各Analyzerは個別のパッケージに存在します。既存のドライバーに新しいAnalyzerを追加するには、リストに項目を追加します。
import (
"unusedresult"
"nilness"
"printf"
)
var analyses = []*analysis.Analyzer{
unusedresult.Analyzer,
nilness.Analyzer,
printf.Analyzer,
}
ドライバーは、名前、フラグ、ドキュメントを使用して、実行する解析を説明するオンラインヘルプを提供できます。docコメントには、短い1行の概要が含まれ、オプションで段落の説明が続きます。Analyzer型には、上記以外のフィールドがあります。
type Analyzer struct {
Name string
Doc string
Flags *flag.FlagSet
Run func(*Pass) (interface{}, error)
RunDespiteErrors bool
ResultType reflect.Type
Requires []*Analyzer
FactTypes []Fact
}
Flagsフィールドは、解析の動作を制御する名前付き(グローバル)フラグ変数のセットを宣言します。vetとは異なり、解析フラグはコマンドラインFlagSetに直接宣言されるわけではありません。フラグ変数を設定するのはドライバーの責任です。単一の解析aのドライバーは、そのフラグfをコマンドラインで直接-fとして公開するかもしれませんが、複数の解析のドライバーは、曖昧さを避けるためにフラグ名を解析名でプレフィックスする(-a.f)かもしれません。IDEはグラフィカルインターフェースを通じてフラグを公開する可能性があり、バッチパイプラインは設定ファイルからそれらを構成するかもしれません。フラグの動作例については、「findcall」アナライザーを参照してください。RunDespiteErrorsフラグは、解析が悪型付けされたコードを処理できるように装備されているかどうかを示します。そうでない場合、ドライバーはパースまたは型エラーがあった場合に解析をスキップします。オプションのResultTypeフィールドは、この解析によって計算され、他の解析で利用可能になる結果値の型を指定します。Requiresフィールドは、この解析が依存し、その結果にアクセスできる可能性のある解析のリストを指定し、ドライバーが解析を実行できる順序を制約します。FactTypesフィールドは、モジュラリティに関するセクションで説明されています。analysisパッケージは、AnalyzerのRequiresグラフが非循環であること、その事実および結果の型が一意であることなどの基本的な健全性チェックを実行するためのValidate関数を提供します。最後に、Runフィールドには、ドライバーが単一のパッケージに対して解析を実行するために呼び出す関数が含まれています。ドライバーはそれにPass型のインスタンスを渡します。
パス ¶
Passは、単一の作業単位を記述します:特定のAnalyzerを特定のGoコードパッケージに適用することです。Passは、解析対象のパッケージに関する情報をAnalyzerのRun関数に提供し、ドライバーに診断やその他の情報を報告するための操作をRun関数に提供します。
type Pass struct {
Fset *token.FileSet
Files []*ast.File
OtherFiles []string
IgnoredFiles []string
Pkg *types.Package
TypesInfo *types.Info
ResultOf map[*Analyzer]interface{}
Report func(Diagnostic)
...
}
Fset、Files、Pkg、TypesInfoフィールドは、単一のGoコードパッケージの構文ツリー、型情報、ソース位置を提供します。OtherFilesフィールドは、パッケージの一部であるアセンブリなどの非Goファイルのリストを提供します。同様に、IgnoredFilesフィールドは、現在のビルド構成ではパッケージの一部ではないが、他のビルド構成の一部である可能性のあるGoおよび非Goソースファイルのリストを提供します。これらのファイルのコンテンツはPass.ReadFileを使用して読み取ることができます。「asmdecl」または「buildtags」アナライザーで非Goファイルをロードし、それらに対する診断を報告する例を参照してください。ResultOfフィールドは、この解析が要求するアナライザーによって計算された結果を提供します。これはAnalyzer.Requiresフィールドで表現されています。ドライバーは、要求されたアナライザーを最初に実行し、それらの結果をこのマップで利用可能にします。各Analyzerは、Analyzer.ResultTypeフィールドで記述された型の値を返す必要があります。例えば、「ctrlflow」アナライザーは、パッケージ内の各関数に対する制御フローグラフを提供する*ctrlflow.CFGsを返します(golang.org/x/tools/go/cfgを参照)。「inspect」アナライザーは、他のAnalyzerがパッケージの構文ツリーをより効率的にトラバースできるようにする値を返します。「buildssa」アナライザーは、SSA形式の中間表現を構築します。これらのAnalyzerのそれぞれは、コアAPIへの依存関係を追加せずに、後続のAnalyzerの機能を拡張するため、分析ツールは必要な拡張機能にのみ料金を支払います。
Report関数は、ソース位置に関連付けられたメッセージである診断を発行します。ほとんどの解析では、診断がその主な結果です。便宜上、Passは、文字列をフォーマットすることによって新しい診断を報告するためのヘルパーメソッドReportfを提供します。Diagnosticは次のように定義されます。
type Diagnostic struct {
Pos token.Pos
Category string // optional
Message string
}
オプションのCategoryフィールドは、解析が複数の種類の診断を生成する場合にメッセージの種類を分類する短い識別子です。Diagnostic構造体には、その重大度を示すフィールドがありません。なぜなら、アナライザーとその診断の相対的な重要性に関する意見は、ユーザー間で大きく異なるからです。このフレームワークの設計では、各アナライザーに診断の重大度を特定する責任を負わせません。代わりに、ドライバーは、ユーザーの好みに応じて、生成したアナライザーとオプションのカテゴリに基づいて診断のフィルタリングと優先順位付けをユーザーがカスタマイズできるようにすると予想されます。ほとんどのアナライザーは型付けされたGo構文ツリーを検査しますが、asmdeclやbuildtagのような少数派は、Goソースファイルの生のテキストや、アセンブリのような非Goファイルを検査します。生のテキストファイルの行に対して診断を報告するには、次のシーケンスを使用します。
content, err := pass.ReadFile(filename)
if err != nil {
...
}
tf := fset.AddFile(filename, -1, len(content))
tf.SetLinesForContent(content)
...
pass.Reportf(tf.LineStart(line), "oops")
モジュラー解析wi