静的解析で型を扱う #golang

この記事は[Gopher道場アドベントカレンダー](https://qiita.com/advent-calendar/2018/gopherdojo)の24日目の記事です。

## 型情報を取得する

静的解析を行う際に型情報を取得するには、[go/types](https://golang.org/pkg/go/types)パッケージを用います。
[golang.org/x/tools/go/analysis](https://golang.org/x/tools/go/analysis)パッケージを使っている場合は、自分で型チェックのプロセスを行う必要がなく、[analysis.Pass](https://golang.org/x/tools/go/analysis/#Pass)構造体の`TypesInfo`フィールドから簡単に取得できます。

なお、`analysis`パッケージについては、メルカリのアドベントカレンダーに「[Goにおける静的解析のモジュール化について](https://tech.mercari.com/entry/2018/12/16/150000)」という記事を書いてますので、そちらをご覧ください。

`Pass`構造体の`TypesInfo`フィールドの型は`*types.Info`です。[types.Info](https://golang.org/pkg/go/types/#Info)構造体の`Types`フィールドから式（`ast.Expr`）の型と値の情報である[types.TypeAndValue](https://golang.org/pkg/go/types/#TypeAndValue)型の値が取得できます。

型（`types.Type`）だけの情報がほしい場合は、`*types.Info`型の[TypeOfメソッド](https://golang.org/pkg/go/types/#Info.TypeOf)から取得できます。

たとえば、以下のコードの`println(v + 2)`の`v + 2`の部分の型を取得したいと考えます。
変数`v`は`int`型であるため、`v + 2`も`int`型であることは予想がつきます。

```go
func main() {
	v := 1
	println(v + 2) // want `int`
}
```

`v + 2`は二項演算式で、抽象構文木上では、`*ast.BinaryExpr`型で表現されています。
上記のコードには他に二項演算式が出てきません。
そのため、次のように雑に`inspect.Preorder`で抽象構文木をトラバースして見つけた二項演算式の型を`TypeOf`メソッドを用いて取得してみます。

```go
// AnalyzerのRunフィールドに設定される関数
func run(pass *analysis.Pass) (interface{}, error) {
	inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
	inspect.Preorder(nil, func(n ast.Node) {
		switch n := n.(type) {
		case *ast.BinaryExpr:
			fmt.Println(pass.TypesInfo.TypeOf(n))
		}
	})
	return nil, nil
}
```

この`Analyzer`を`singlechecker`として前述のコードに対して実行してやると、以下のように表示されます。

```
(...省略...)/a.go:5:10: int
```

うまく型情報が取れていることが分かります。

## 組み込み型を取得する

さて、`int`型や`string`型に対応する`types.Type`型の値を取得するにはどうすればよいでしょうか？
`int`型や`string`型の組み込み型の場合は、次のように`types`パッケージの変数`Typ`から取得できます。

```go
intType := types.Typ[types.Int]
```

なお、組み込み型の中でも`error`型は`types.Typ`から取得できません。
そのため、次のように組み込みの識別を保持しているUniverseスコープから名前を指定して型を取得する必要があります。

```go
errType := types.Universe.Lookup("error").Type()
```

変数`types.Universe`は`*types.Scope`型であるため、`Lookup`メソッドで識別子名に対応するオブジェクトを取得することができます。
この場合は型名に対応する[*types.TypeName](https://golang.org/pkg/go/types/#TypeName)型の値を取得できます。
`types.TypeName`型は型名を表しており、「型」自体を表すものではないため、`Type`メソッドから型を取得します。

`Type`メソッドの戻り値は`types.Type`型です。`types.Type`はインタフェースであるため、実際には何かしらの具象型が取得されます。

この場合は、`error`型であるため、名前付きの型を表す[*types.Named](https://godoc.org/go/types#Named)型の値が取得できます。

## ベースの型を取得する

名前付き型を表す`types.Named`型からベースとなっている型を取得するためにはどうすればよいのでしょうか？

例えば、`type Hex int`のように定義した場合、`Hex`型を表す`*types.Named`型の値から`int`型を取得したいと考えます。`types`パッケージで定義されているGoの型を表す型は`types.Type`インタフェースを実装しています。`types.Type`インタフェースが持つ`Underlying`メソッドを用いることでベースとなる型を取得することができます。

`type`キーワードを使って定義する名前付きの型は、`type Hex int`のように別の名前付き型をベースにするものだけではなく、`type error interface{ Error() string }`のように型リテラルをベースにする型も存在します。

`Underlying`メソッドを使うことで、ベースとなっている型リテラルを取得することができます。`error`型を表す`*types.Named`型の値がもつ`Underlying`メソッドから取得できる値は、`*types.Interface`型の値です。この値を用いることでインタフェースを実装しているかどうかなどを調べることができます。

なお、スコープを表す`*types.Scope`から取得できる型は名前付きなので、インタフェースや構造体そのものに関する情報を取得するには、`Underlying`メソッド使って`*types.Interface`型や`*types.Struct`型の値を取得する必要があります。

## 型を比較する

さて、任意の式`expr`が`int`型かどうか判定する場合にはどうすれば良いでしょうか？
単純に考えると、次のように`==`で比較するという方法があります。

```go
intType := types.Typ[types.Int]
if pass.TypesInfo.TypeOf(expr) == intType {
    pass.Reportf(n.Pos(), "int")
}
```

`types.Typ`で管理されている値を使う`int`型の場合はこれでも良さそうですが、他の型の場合は単純な比較はできません。型の比較方法は型の種類によって異なり、[言語仕様](https://golang.org/ref/spec#Type_identity)でも`Identical`である型について詳しく規程されています。

`types`パッケージには、`Identical`かどうか調べるための[types.Identical](https://golang.org/pkg/go/types/#Identical)関数があります。`types.Identical`関数は2つの`types.Type`型の値を引数にとり、2つの値が表す型が`Identical`かどうかを判定します。

```go
intType := types.Typ[types.Int]
if types.Identical(pass.TypesInfo.TypeOf(expr), intType) {
    pass.Reportf(n.Pos(), "int")
}
```

## インタフェースを実装しているか判定する

インタフェースは`types.Interface`型によって表されます。[types.Implements](https://golang.org/pkg/go/types/#Implements)関数は、第1引数に`types.Type`型の値、第2引数に`*types.Interface`型の値を受け付けます。戻り値は`bool`型の値で、第1引数で渡した値が表す型が、第2引数で渡した値が表すインタフェースを実装しているかどうかを返します。

例えば、任意の型を表す`types.Type`型の変数`t`が組み込み型の`error`インタフェースを実装しているかどうかをチェックするには次のように書きます。

```go
errType := types.Universe.Lookup("error").Type()
if types.Implements(t, errType.Underlying()) {
    fmt.Println(t, "implements error")
}
```

`errType`は`*types.Named`型の値であるため、`Underlying`メソッドで`*types.Interface`型の値を取得する必要があります。

## 代入可能か変換可能かなどを取得する

`types.Implements`関数以外にも、次のような指定した型の性質を調べる関数が用意されています。

* [types.AssertableTo](https://golang.org/pkg/go/types/#AssertableTo)関数：第1引数のインタフェース型が第2引数の型に型アサーションできるかどうか調べる
* [types.AssignableTo](https://golang.org/pkg/go/types/#AssignableTo)関数：第1引数の型の値を第2引数の型の変数に代入可能かどうか調べる
* [types.Comparable](https://golang.org/pkg/go/types/#Comparable)関数：第1引数の型が比較可能かどうか調べる（関数やスライスなどは比較できない）
* [types.ConvertibleTo](https://golang.org/pkg/go/types/#ConvertibleTo)関数：第1引数の型の値が第2引数の変数にキャスト可能かどうか調べる

## まとめ

この記事では、静的解析で型を扱うための方法について説明しました。
年末の時間を使ってみなさんもぜひ静的解析に取り組んでみてください！

それでは良いお年を！！！