モジュール化された静的解析の実装を追ってみよう #golang
December 16, 2018
この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。
はじめに
この記事では、Goにおける静的解析のモジュール化についてという記事で調子に乗って書ききれなかった詳細な実装の解説を行います。 そのため、まずは「Goにおける静的解析のモジュール化について」を読んでからこの記事を読んだほうが良いでしょう。
findcallの実装の詳細
golang.org/x/tools/go/analysis/passes/findcallパッケージは、指定した名前の関数の呼び出し部分を探す簡単なAnalyzer
の実装例として提供されています。
このパッケージで提供されているfindcall.Analyzer
は次のようなanalysis.Analyzer
型の変数になっています。
var Analyzer = &analysis.Analyzer{
Name: "findcall",
Doc: Doc,
Run: run,
RunDespiteErrors: true,
FactTypes: []analysis.Fact{new(foundFact)},
}
Name
フィールドはAnalyzer
の名前、Doc
フィールドは静的解析ツールのユーザのためのAnalyzer
の説明です。
Run
フィールドがAnalyzer
の実際の処理になります。ここではrun
という名前のパッケージ関数が指定されています。
RunDespiteErrors
フィールドは構文解析や型チェックが何かしらの理由でエラーを出していても解析を行うかどうかのフラグです。
FactTypes
フィールドは、このAnalyzer
で出力されるFact
の型を設定されるフィールドです。
Analyzer
へのオプションは、次のようにflags
パッケージを用いてプログラム引数として渡されるようになっています。
ここでは探す関数の名前を指定できるようにしています。
var name string // -name flag
func init() {
Analyzer.Flags.StringVar(&name, "name", name, "name of the function to find")
}
Run
フィールドで指定したrun
関数の中身を見てみましょう。
少し長いのでDiagnostics
として出力している前半部分と、Fact
として出力している後半部分に分けて見ていきましょう。
関数呼び出し部分をチェックする
run
関数の前半部分は、次のようになっています。
func run(pass *analysis.Pass) (interface{}, error) {
for _, f := range pass.Files {
ast.Inspect(f, func(n ast.Node) bool {
if call, ok := n.(*ast.CallExpr); ok {
var id *ast.Ident
switch fun := call.Fun.(type) {
case *ast.Ident:
id = fun
case *ast.SelectorExpr:
id = fun.Sel
}
if id != nil && !pass.TypesInfo.Types[id].IsType() && id.Name == name {
pass.Reportf(call.Lparen, "call of %s(...)", id.Name)
}
}
return true
})
}
// (....略....)
}
run
関数の引数には、*analysis.Pass
型の値が引数として渡されています。
analysis.Pass
型は構造体で、Analyzer
に抽象構文木や型情報の提供を行うために用いられます。
また、依存しているAnalyzer
があればそこで得られた情報を渡すこともできます。
run
関数の前半部分では、構文解析を行った結果である抽象構文木をGoファイル単位でチェックを行っています。
これらの抽象構文木はpass.Files
フィールドから取得することができ、Goファイル単位の構文解析結果である*ast.File
のスライスになっています。
構文解析を行った結果である抽象構文木は、木構造になっているので木のノードを再帰的にトラバースしながら解析を行っていきます。
ここでは標準で提供されているgo/ast
パッケージのInspect
関数を用いて抽象構文木をトラバースしています。
ast.Inspect
関数は各ノードを辿りながら引数で渡した関数を適用していきます。
この関数は引数に抽象構文木のノードを取るような関数になっています。
ast.Inspect
関数の引数で渡した関数の中では、対象のノードが関数呼び出しを表す*ast.CallExpr
型かどうかをチェックしています。
関数呼び出しの場合は、指定された名前かどうかチェックする必要がありますが、関数呼び出しにも複数あるため、場合分けしてチェックしています。
パッケージ関数や変数に入った関数の呼び出しは、f()
のように関数名や変数名のあとに()
をつけて呼び出します。
ast.CallExpr
型のFun
フィールドにはf()
のf
の部分の情報が入っています。
f
は識別子(変数名や関数名など)であるため、Fun
フィールドがそれを表す*ast.Ident
型かどうかを型スイッチでチェックしています。
一方、メソッドや関数の入ったフィールドを使ったv.m()
のような関数呼び出しも考えられます。
その場合には、ast.CallExpr
型のFun
フィールドには、v.m
の部分を表す*ast.SelectorExpr
型の値が入ります。
ast.SelectorExpr
型はメソッドやフィールドの.
(ドット)を使った参照を表す型です。
さて、今回は関数名がほしいのでv.m
のm
の部分を取得しなければいけません。
これはast.SelectorExpr
型のSel
フィールドから取得できます。
2パターンの関数呼び出しの関数名部分は、id
という変数に代入されます。
id
は*ast.Ident
型で識別子を表すため、Name
フィールドと指定された関数の名前を比較しています。
比較した結果、名前が同じの場合はpass
のReportf
フィールドで提供されているDiagnostics
として結果を報告する関数で出力しています。
なお、Goの文法上xxx(yyyy)
のような記述を行うのは関数呼び出しだけではありません。
int64(100)
のような型のキャストもこのような形式で記述します。
そのため、型のキャストも抽象構文木のノードとしては、ast.CallExpr
として表されます。
int64(100)
のようなキャストをチェック対象から弾くために、if
の条件に!pass.TypesInfo.Types[id].IsType()
が追加されています。
id
で表される識別子がint64
のような型名かどうかを、pass.TypeInfo
フィールドで提供されている型情報から取得しています。
pass.TypeInfo.Types
は、任意の式がどのような型や値になるかという情報が保持されています。
この場合はid
が型を表す識別子かどうかをチェックしています。
関数定義部分のチェック
次にFact
を使っている後半部分を見ていきましょう。
func run(pass *analysis.Pass) (interface{}, error) {
// (....略....)
// Export a fact for each matching function.
//
// These facts are produced only to test the testing
// infrastructure in the analysistest package.
// They are not consumed by the findcall Analyzer
// itself, as would happen in a more realistic example.
for _, f := range pass.Files {
for _, decl := range f.Decls {
if decl, ok := decl.(*ast.FuncDecl); ok && decl.Name.Name == name {
if obj, ok := pass.TypesInfo.Defs[decl.Name].(*types.Func); ok {
pass.ExportObjectFact(obj, new(foundFact))
}
}
}
}
return nil, nil
}
後半部分は、関数定義から指定した名前の関数を定義しているものを見つけて、Fact
としてその情報を出力しています。
関数定義は、ファイルを表すast.File
型のDecls
フィールドから取得できます。
Decls
フィールドは、Goファイルのトップレベルで定義できるものが保持されています。
そのため、Decls
フィールドには、パッケージのインポート、定数・変数・型の定義を表す*ast.GenDecl
型と関数定義を表す*ast.FuncDecl
型の2種類の型の値が入ります。
*ast.FuncDecl
型の値であるかどうかを型アサーションで絞り、その名前が指定した関数名かどうかをチェックしています。
decl.Name
フィールドからは、直接文字列が取れるのではなく、*ast.Ident
型の値が取得できるので、decl.Name.Name
と記述する必要があります。
対象の関数定義が指定した名前の関数を定義するものだった場合は、Fact
としてその情報を出力する必要があります。
そのため、Fact
として紐付ける型情報をpass.TypesInfo.Defs
フィールドのマップから識別子を指定して取得しています。
pass.TypesInfo.Defs
フィールドは、キーを識別子、値を対応する型情報としたマップで、関数定義や変数定義などの識別子を定義する部分の型情報を得ることができます。