Goは go/ast
パッケージによって抽象構文木(AST)を簡単に入手することができます。ただ、GoのASTはあくまで構文チェックレベルであり型情報までは確認できていません。
ここでは go/types パッケージを使って型チェックを使った静的解析を行なってみましょう。なお、このCodelabs内では今後 go/types パッケージの go/
を省略し、単純に types
パッケージと記載します。
今回は types.Object
、types.Type
を駆使しながらコード中に定義された構造体が特定のインターフェースを実装している型を見つけてみます。
なお、本コードラボでは静的解析の基礎や go/ast
パッケージについては深く解説しないため、事前に以下のコードラボで学習しておくことをおすすめします。
まずは types
パッケージを使ってインターフェースと型を比較してみましょう。最初に以下のようなコードに対して静的解析を行います。I
インターフェースを *S
と Y
が実装しているサンプルコードです。
package p
type I interface{
Hoge() string
}
type S struct {
}
func (s *S) Hoge() string{
return ""
}
type Y struct {
hoge string
}
func (y Y) Hoge() string{
return ""
}
すでに go/ast
パッケージを利用してASTを取得したことがあるならば、parser.ParseFile
関数などの使い方を知っていると思います。 types
パッケージによる型チェックは、types.Config
構造体の Check
メソッドを呼ぶところから始まります。
func (conf *Config) Check(path string, fset *token.FileSet,
files []*ast.File, info *Info) (*Package, error)
詳細な型チェックを行うためには、*ast.File
などの情報のほかに、types.Info
も必要になりますが最初は利用しないので nil
を入れます。戻り値の types.Package
は types.Config
の初期化時に定められたインポート設定などを元にしてAST中のオブジェクトの型を評価した結果を内部の格納します。
conf := types.Config{}
f, _ := parser.ParseFile(fset, "p", code, parser.ParseComments)
pkg, err := conf.Check("p", fset, []*ast.File{f}, nil)
戻り値の pkg
( *types.Package
)に格納された型情報は types.Package.Scope
メソッドによって得られる types.Scope
の中にあります。 Scope
はある条件下で解決されたオブジェクトの名前と型 のマッピングです。 Scope
の中から自分がほしいオブジェクトを取り出すには、 types.Scope.Lookup
メソッドを使います。
Lookup
メソッドで取得できる types.Object
インターフェースを実装したオブジェクトが型チェックをおこなう単位要素になっていきます。
types.Object
インターフェースは Type
メソッドを持ち、そのオブジェクトの types.Type
を取得することができます。types.Type
こそが今回解析したい型情報です。
実際に Scope
の中から S
構造体などの型を取り出し、型を取得するコードが以下になります。ここで、 types
パッケージは types.Implements
関数という「第一引数の tyeps.Type
が第二引数の *types.Interface
を実装しているとき true
を返す」関数をもっているので、これを利用して I
インターフェースを実装しているか確認してみます。
st := pkg.Scope().Lookup("S").Type()
yt := pkg.Scope().Lookup("Y").Type()
it := pkg.Scope().Lookup("I").Type()
if types.Implements(st, it.Underlying().(*types.Interface)) {
fmt.Println(st, "implements", it)
}
if types.Implements(yt, it.Underlying().(*types.Interface)) {
fmt.Println(yt, "implements", it)
}
注意してほしいのは、pkg.Scope().Lookup("I").Type()
は *types.Named
型の値です( interface{ Hoge() string }
というインターフェースである I
という名前の型)。なので、 types.Type.Underlying
メソッドを利用して、 I
の基底となるインターフェースを取り出してから比較しています。
type Type interface {
// Underlying returns the underlying type of a type.
Underlying() Type
// String returns a string representation of a type.
String() string
}
実行結果は次のようになります。 S
はI
を実装していないのでしょうか?
p.Y implements p.I
... I
を実装しているのは S
ではなく *S
でしたね。types.Implements
関数で I
と比較する *S
に対応する types.Type
を作り出すには types.NewPointer
を使う必要があります。
st := pkg.Scope().Lookup("S").Type()
yt := pkg.Scope().Lookup("Y").Type()
it := pkg.Scope().Lookup("I").Type()
pst := types.NewPointer(st)
if types.Implements(pst, it.Underlying().(*types.Interface)) {
fmt.Println(pst, "implements", it)
}
if types.Implements(yt, it.Underlying().(*types.Interface)) {
fmt.Println(yt, "implements", it)
}
こんどは期待通り I
インターフェースを実装している結果が確認できました。
*p.S implements p.I p.Y implements p.I
それでは今度は先程省略した types.Info
を使った型チェックをやってみましょう。ast.File
の中から error
型を実装した構造体を探してみます。
types.Info
には以下の情報が含まれています。
| 概要 |
| 式の型の評価結果。ある式がどんな型になるのか。定数式の場合は値も取れる |
| 識別子の定義情報。どこでその識別子が定義されたのか |
| 識別子の使用情報。どこでその識別子が使用されているのか |
これらの情報は、公開されているフィールドなので直接参照することも可能です。また、後述するtypes.Info
のメソッドを通しても利用可能です。
先ほどは決め打ちで型の名前を指定してインターフェースと比較していましたが、今度は ast.Inspect
を使って取得できた型名全てと error
型を比較してみましょう。 error
型の情報は types.Universe
という組み込み型の定義が保持されている types.Scope
から取得することができます。 error
型からインターフェースの定義を取得する方法は I
インターフェースでも行なった基底型を取得する方法を使います。
// error型の定義を取得する
errType := types.Universe.Lookup("error").Type()
// error型の基底となっているインターフェースを取得する
it := errType.Underlying().(*types.Interface)
比較する error
型のインターフェースがあれば、あとは types.Info
の中の情報を探検するだけです。識別子に対応する types.Object
を取得するには types.Info.ObjectOf
メソッドを使います。式を評価した結果の types.Type
を取得したい場合は types.Info.TypeOf
メソッドを使います。
func (info *Info) ObjectOf(id *ast.Ident) Object
func (info *Info) TypeOf(e ast.Expr) Type
types.Info.ObjectOf
メソッドを使って error
型のインターフェースを満たす型を探すコードの ast.Inspect
部分が以下です。
ast.Inspect(f, func(n ast.Node) bool {
switch n := n.(type) {
// 識別子のみ処理する
case *ast.Ident:
// types.Infoの中からtypes.Objectを取得する
obj := info.ObjectOf(n)
if obj == nil {
return true
}
// 型名じゃなかったら無視する
if _, ok := obj.(*types.TypeName); !ok {
return true
}
typ := obj.Type()
// 構造体か
if _, ok := typ.Underlying().(*types.Struct); ok {
if types.Implements(typ, it) {
fmt.Println(fset.Position(obj.Pos()), typ, "implements", errType)
return true
}
// ポインタ型としてerrorインターフェースを満たしていないか
ptr := types.NewPointer(typ)
if types.Implements(ptr, it) {
fmt.Println(fset.Position(obj.Pos()), ptr, "implements", errType)
}
}
}
return true
})
今回は p
パッケージの Y
が error
型のインターフェースを実装していることを解析できました。(解析したコードの内容はソースコード全体の冒頭を確認してください)
p:15:6 p.Y implements error
go/types
パッケージを使った型チェックを使った静的解析を学びました。応用を考えたいときは go/types
パッケージのGo Docを見ながらPlaygroudや手元で簡単なコードを動かしながら確かめていくのがおすすめです。
このチュートリアルではPlaygroud上でファイル相当の文字列を読み込むのみでした。ぜひ実際に analysis
パッケージを使ってCLIツールを作成し、皆さんの手元にあるGoのコードを型チェックしてみましょう。
analysis
パッケージを使ってCLIツールをつくるときに参考になる資料types.Type
の型情報を使ってある型があるインターフェースを満たしているか型チェックすることができましたtypes.Scope.Lookup
メソッドを使って期待する値のオブジェクトを取得することができましたtypes.Info.ObjectOf
メソッドを使って識別子の型情報を取得することができましたtypes.Info.TypeOf
メソッドを使って式の型情報の取得もチャレンジしてみてくださいerror
インターフェースを実装する型を探すことができました