Goは go/ast パッケージによって抽象構文木(AST)を簡単に入手することができます。ただ、GoのASTはあくまで構文チェックレベルであり型情報までは確認できていません。

ここでは go/types パッケージを使って型チェックを使った静的解析を行なってみましょう。なお、このCodelabs内では今後 go/types パッケージの go/ を省略し、単純に types パッケージと記載します。

今回は types.Objecttypes.Type を駆使しながらコード中に定義された構造体が特定のインターフェースを実装している型を見つけてみます。

なお、本コードラボでは静的解析の基礎や go/ast パッケージについては深く解説しないため、事前に以下のコードラボで学習しておくことをおすすめします。

まずは types パッケージを使ってインターフェースと型を比較してみましょう。最初に以下のようなコードに対して静的解析を行います。I インターフェースを *SY が実装しているサンプルコードです。

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.Packagetypes.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 メソッドを使います。

types.Object インターフェースと types.Type インターフェース

Lookup メソッドで取得できる types.Object インターフェースを実装したオブジェクトが型チェックをおこなう単位要素になっていきます。

types.Object インターフェースは Type メソッドを持ち、そのオブジェクトの types.Type を取得することができます。types.Type こそが今回解析したい型情報です。

types.Scope.Lookupを使って型情報を取り出す

実際に 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
}

実行結果は次のようになります。 SI を実装していないのでしょうか?

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 のフィールド名

概要

Types

式の型の評価結果。ある式がどんな型になるのか。定数式の場合は値も取れる

Defs

識別子の定義情報。どこでその識別子が定義されたのか

Uses

識別子の使用情報。どこでその識別子が使用されているのか

これらの情報は、公開されているフィールドなので直接参照することも可能です。また、後述するtypes.Info のメソッドを通しても利用可能です。

error型の情報を取得する

先ほどは決め打ちで型の名前を指定してインターフェースと比較していましたが、今度は ast.Inspect を使って取得できた型名全てと error 型を比較してみましょう。 error 型の情報は types.Universe という組み込み型の定義が保持されている types.Scope から取得することができます。 error 型からインターフェースの定義を取得する方法は I インターフェースでも行なった基底型を取得する方法を使います。

// error型の定義を取得する
errType := types.Universe.Lookup("error").Type()
// error型の基底となっているインターフェースを取得する
it := errType.Underlying().(*types.Interface)

ast.Inspectを使って型チェックをする

比較する 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 パッケージの Yerror 型のインターフェースを実装していることを解析できました。(解析したコードの内容はソースコード全体の冒頭を確認してください)

p:15:6 p.Y implements error

go/types パッケージを使った型チェックを使った静的解析を学びました。応用を考えたいときは go/types パッケージのGo Docを見ながらPlaygroudや手元で簡単なコードを動かしながら確かめていくのがおすすめです。

このチュートリアルではPlaygroud上でファイル相当の文字列を読み込むのみでした。ぜひ実際に analysis パッケージを使ってCLIツールを作成し、皆さんの手元にあるGoのコードを型チェックしてみましょう。

analysis パッケージを使ってCLIツールをつくるときに参考になる資料

What you'll learn