モジュール化された静的解析の実装を追ってみよう #golang

# はじめに

この記事では、[Goにおける静的解析のモジュール化について](https://tech.mercari.com/entry/2018/12/16/150000)という記事で調子に乗って書ききれなかった詳細な実装の解説を行います。
そのため、まずは「Goにおける静的解析のモジュール化について」を読んでからこの記事を読んだほうが良いでしょう。

## findcallの実装の詳細

[golang.org/x/tools/go/analysis/passes/findcall](https://godoc.org/golang.org/x/tools/go/analysis/passes/findcall)パッケージは、指定した名前の関数の呼び出し部分を探す簡単な`Analyzer`の実装例として提供されています。

このパッケージで提供されている`findcall.Analyzer`は次のような`analysis.Analyzer`型の変数になっています。

```go
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`パッケージを用いてプログラム引数として渡されるようになっています。
ここでは探す関数の名前を指定できるようにしています。

```go
var name string // -name flag

func init() {
	Analyzer.Flags.StringVar(&name, "name", name, "name of the function to find")
}
```

`Run`フィールドで指定した`run`関数の中身を見てみましょう。
少し長いので`Diagnostics`として出力している前半部分と、`Fact`として出力している後半部分に分けて見ていきましょう。

## 関数呼び出し部分をチェックする

`run`関数の前半部分は、次のようになっています。

```go
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`を使っている後半部分を見ていきましょう。

```go
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`フィールドは、キーを識別子、値を対応する型情報としたマップで、関数定義や変数定義などの識別子を定義する部分の型情報を得ることができます。