モジュール化された静的解析の実装を追ってみよう #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.mmの部分を取得しなければいけません。 これは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フィールドは、キーを識別子、値を対応する型情報としたマップで、関数定義や変数定義などの識別子を定義する部分の型情報を得ることができます。