この記事は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
の部分の型を取得したいと考えます。
変数v
はint
型であるため、v + 2
もint
型であることは予想がつきます。
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
}
このAnalyzer
をsinglechecker
として前述のコードに対して実行してやると、以下のように表示されます。
(...省略...)/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
型の値を取得する必要があります。
型を比較する
さて、任意の式expr
がint
型かどうか判定する場合にはどうすれば良いでしょうか?
単純に考えると、次のように==
で比較するという方法があります。
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引数の変数にキャスト可能かどうか調べる
まとめ
この記事では、静的解析で型を扱うための方法について説明しました。
年末の時間を使ってみなさんもぜひ静的解析に取り組んでみてください!
それでは良いお年を!!!