Go言語で書いたソースコードを解析する。

Go言語ではgo/astgo/parsergo/tokenパッケージを使用することで、Go言語で書かれたソースコードを解析することができます。
解析したソースコードを表現する抽象構文木(AST:abstract syntax tree)の形で出力されます。
文章では難しいので実際のサンプルで試してみたいと思います。


以下の様な解析対象のソースコードを用意します。

package main

import (
    "fmt"
    "time"
)

type MyInterface interface {
    Greeting()
}

type MyStruct struct {
    Name string
}

func (m *MyStruct) Greeting() {
    fmt.Printf("%v, %sさん、こんにちは!\n", time.Now(), m.Name)
}

func (m *MyStruct) Message(s string) time.Time {
    fmt.Printf("%s さん、%s\n", m.Name, s)
    return time.Now()
}

func main() {
    ms := MyStruct{"山田"}
    ms.Greeting()

    fmt.Println(ms.Message("さようなら"))
}

このソースコードを解析するサンプルです。
(酷いネストですがご容赦ください)

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    fset := token.NewFileSet()
    file, err := parser.ParseFile(fset, "sample.go", nil, 0)
    if err != nil {
        panic(err)
    }

    for _, decl := range file.Decls {
        switch td := decl.(type) {
        case *ast.GenDecl:
            switch td.Tok {
            case token.IMPORT:
                fmt.Println("### import")
                for _, sp := range td.Specs {
                    s := sp.(*ast.ImportSpec)
                    fmt.Println(s.Path.Value)
                }

            case token.TYPE:
                fmt.Println("### type")
                for _, sp := range td.Specs {
                    s := sp.(*ast.TypeSpec)
                    fmt.Println(s.Name)

                    switch t := s.Type.(type) {
                    case *ast.InterfaceType:
                        for _, m := range t.Methods.List {
                            fmt.Println(m)
                        }
                    case *ast.StructType:
                        for _, f := range t.Fields.List {
                            fmt.Println(f)
                        }
                    default:
                        fmt.Println(3, t)
                    }
                }
            case token.CONST:
            case token.VAR:
            default:

            }
        case *ast.FuncDecl:
            fmt.Println("### function")
            fmt.Println(td.Name)
            if td.Recv != nil {
                fmt.Println(td.Recv.List[0].Type)
            }
            if td.Type.Params != nil && td.Type.Params.NumFields() > 0 {
                fmt.Println("##### args")
                for _, p := range td.Type.Params.List {
                    fmt.Println(p.Type, p.Names)
                }
            }
            if td.Type.Results != nil && td.Type.Results.NumFields() > 0 {
                fmt.Println("##### returns")
                for _, r := range td.Type.Results.List {
                    fmt.Println(r.Type, r.Names)
                }
            }
        default:
        }

        fmt.Println()
    }
}

実行結果は以下のとおりです。

$ go run parse_code.go
### import
"fmt"
"time"

### type
MyInterface
&{<nil> [Greeting] 0xc0840058a0 <nil> <nil>}

### type
MyStruct
&{<nil> [Name] string <nil> <nil>}

### function
Greeting
&{148 MyStruct}

### function
Message
&{258 MyStruct}
##### args
string [s]
##### returns
&{time Time} []

### function
main


parser.ParseFile関数にtoken.FileSet、ソースファイル名、ソース、parser.Modeを指定して呼び出します。
ソースファイル名とソースはいずれかを指定します。
戻り値としてast.Fileとerrorが返ってきます。このast.Fileが解析されたソースコードの情報を保持しています。
複数のソースコード(パッケージ)を一気に解析するには、parser.ParseDir関数を使用します。戻り値はパッケージごとのマップになります。


ast.File.Declsを参照すると、ソースコードのトップレベルに定義されているast.Declを実装したXXXXDeclの一覧を取得できます。
一覧の中には、ast.GenDeclast.FuncDecelがあります。


例えば、GenDeclでGenDecl.Tokがtoken.TYPEの場合だと(case token.TYPE:)、
トップレベルに定義されているtype宣言なので、interface定義やstruct定義を取得することができます。


FuncDeclの場合だと、トップレベルに定義されている関数を取得することができます。
またその関数がレシーバーと指定定義されているかどうか、パラメータはどうか、戻り値はどうかなど詳細な情報まで取得できます。


このように解析されたソースコード木構造を辿って行くことでソースコードの情報を取得できるようです。