静的解析で型を扱う #golang

December 25, 2018

この記事はQiitaの記事をエクスポートしたものです。内容が古くなっている可能性があります。

この記事はGopher道場アドベントカレンダーの24日目の記事です。

型情報を取得する

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

なお、analysisパッケージについては、メルカリのアドベントカレンダーに「Goにおける静的解析のモジュール化について」という記事を書いてますので、そちらをご覧ください。

Pass構造体のTypesInfoフィールドの型は*types.Infoです。types.Info構造体のTypesフィールドから式(ast.Expr)の型と値の情報であるtypes.TypeAndValue型の値が取得できます。

型(types.Type)だけの情報がほしい場合は、*types.Info型のTypeOfメソッドから取得できます。

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

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

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

// 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
}

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

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

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

組み込み型を取得する

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

intType := types.Typ[types.Int]

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

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

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

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

この場合は、error型であるため、名前付きの型を表す*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型の値を取得する必要があります。

型を比較する

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

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

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

typesパッケージには、Identicalかどうか調べるためのtypes.Identical関数があります。types.Identical関数は2つのtypes.Type型の値を引数にとり、2つの値が表す型がIdenticalかどうかを判定します。

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

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

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

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

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関数:第1引数のインタフェース型が第2引数の型に型アサーションできるかどうか調べる
  • types.AssignableTo関数:第1引数の型の値を第2引数の型の変数に代入可能かどうか調べる
  • types.Comparable関数:第1引数の型が比較可能かどうか調べる(関数やスライスなどは比較できない)
  • types.ConvertibleTo関数:第1引数の型の値が第2引数の変数にキャスト可能かどうか調べる

まとめ

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

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