はじめに
みなさん、メリークリスマス!
22日の枠に欠員が出たので、go/parser
パッケージを使ってAST(抽象構文木)を取得する方法についてまとめたいと思います。
go
パッケージについては、簡単な式の評価機を作ってみるという記事も書いているので、そちらもぜひ読んで下さい。
なお、この記事を執筆時点のGoの最新バージョンは1.7.4です。
ノード
go/parser
パッケージでは、いくつかのParse
と名前のついた関数でAST(抽象構文木)を取得することができます。ASTの各ノードを表す型は、go/ast
パッケージで提供されています。そしてそれらの型は、以下のast.Node
インタフェースを実装するように定義されています。
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
ast.Node
インタフェースは、Pos
メソッドとEnd
メソッドを持ち、共にtoken.Pos
型の値を返します。token.Pos
はノードのコード上の位置を表します。
token.Pos
が具体的にどんな値を表すかは後述しますので、ここでは、ノードのコード上の位置という理解でよいでしょう。
go/ast
パッケージのドキュメントを見ると分かるように、ast.Node
インタフェースを実装した型が多数存在することが分かります。
ドキュメントを見るだけでは、一体どの型が何を表し、どうやったら取得できるのか謎だと思いますが、それはgo/parse
パッケージが提供するParse
系の関数から取得できるノードから順にみていくとよいでしょう。
そこで、この記事ではgo/parser
パッケージの提供するParse
系の関数について詳しくみていきたいと思います。
パースする
ASTを取得するには、いくつか方法があります。
簡単な式の評価機を作ってみるで紹介した、ParseExpr
は、式を表すASTのノードである、ast.Expr
を返します。
この他にも、Parse
系のメソッドは以下のように、いくつかあります。
上記の何が違うのかというと、パースの対象となるソースコードの範囲です。
ParseDir
はディレクトリ以下にあるファイルが対象であるのに対して、ParseFile
はファイルを対象としています。また、ParseExpr
とParseExprFrom
は式を対象にしています。
そして、それぞれの関数によって、返されるノードの種類が異なります。
各関数のシグニチャを並べて、比べてみましょう。
func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error)
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)
func ParseExpr(x string) (ast.Expr, error)
func ParseExprFrom(fset *token.FileSet, filename string, src interface{}, mode Mode) (ast.Expr, error)
ast.ParseExpr
以外は、token.FileSet
型のポインタを引数に取るようです。まずは、このtoken.FileSet
が何者なのかみてみましょう。
ちなみに、ast.ParseExpr
は内部で以下のようにast.ParseExprFrom
を呼んでいるようです。
func ParseExpr(x string) (ast.Expr, error) {
return ParseExprFrom(token.NewFileSet(), "", []byte(x), 0)
}
token.FileSet
サンプルをみる限り、Parse
系に渡すtoken.File
は、どうやらtoken.NewFileSet
関数で生成するようです。
つまり、初期化したものをそのまま渡すということは、Parse
系の関数によって、渡したtoken.FileSet
に何かしらの変更が加えられることが予想できます。
具体的に、go/parser
パッケージのソースコードを読むと以下のように、*token.FileSet
型のAddFile
メソッドが呼ばれていることが分かります。
func (p *parser) init(fset *token.FileSet, filename string, src []byte, mode Mode) {
p.file = fset.AddFile(filename, -1, len(src))
...
}
*parser
型のinit
メソッドは、ast.ParseFile
からとast.ParseExprFrom
から呼ばれています。なお、ast.ParseDir
はファイルごとの処理を、ast.ParseFile
にまかせているので、実質ast.ParseDir
を読んでも*token.FileSet
型のAddFile
メソッドが呼ばれます。
さて、AddFile
メソッドは何をしているのでしょうか?
ソースコードをみてみると、以下のようになっています。
func (s *FileSet) AddFile(filename string, base, size int) *File {
s.mutex.Lock()
defer s.mutex.Unlock()
if base < 0 {
base = s.base
}
if base < s.base || size < 0 {
panic("illegal base or size")
}
// base >= s.base && size >= 0
f := &File{s, filename, base, size, []int{0}, nil}
base += size + 1 // +1 because EOF also has a position
if base < 0 {
panic("token.Pos offset overflow (> 2G of source code in file set)")
}
// add the file to the file set
s.base = base
s.files = append(s.files, f)
s.last = f
return f
}
*.parser
型のinit
メソッドでは、base
として-1
を渡しています。そのため、上記のコードを読む限り、それまで追加されたファイルのサイズの合計+新しく追加されるファイルのサイズをそのファイルのbase
として計算しています。そして、そのbase
を元にtoken.File
型の値を生成し、内部に記録していきます。
どうやら、このbase
は、*token.FileSet
型のPosition
メソッドで使用され、ast.Node
インタフェースのPos
メソッドやEnd
メソッドから取得できるtoken.Pos
からファイル名や行数などを取得するために用いられるようです。
つまり、*token.FileSet
型のAddFile
メソッドで追加された順に、base
をどんどんファイルサイズで加算していき、その値を元にノードの位置をtoken.Pos
という値で表すことで、1つ数値でどのファイルのどの行なのかという情報を表せるようにしているようです。
さて、token.FileSet
についてなんとなく分かったところで、各Parser
系の関数についてみていきましょう。
parser.ParseDir
parser.ParseDir
関数のシグニチャは以下の通りです。
func ParseDir(fset *token.FileSet, path string, filter func(os.FileInfo) bool, mode Mode) (pkgs map[string]*ast.Package, first error)
第2引数にパス、第3引数にフィルター関数、最後にモードを返しています。
モードについては後述するとして、第2引数と第3引数について説明したいと思います。
第2引数のパスは、パース対象の.go
ファイルが入ったディレクトリです。このパスは、os.Open
によって開かれ、内部のファイル一覧が取得されます。
そして、.go
ファイルかつ第3引数のフィルターがtrue
を返すファイルのみを対象にパースが行われます。なお、フィルターがnil
の場合には、.go
ファイルが対象になります。具体的には、ドキュメントかソースコードを見るとわかるでしょう。
ast.ParseDir
関数の戻り値は、map[string]*ast.Package
とエラーです。
第1戻り値は、キーがパッケージ名で値が*ast.Package
型の値のマップです。
上述のとおり、ast.ParserDir
関数は内部でast.ParseFile
関数を呼び出しているため、その結果がast.Package
としてまとめられ、このマップに格納されています。
なお、第2戻り値の名前がfirst
になっていることから分かるように、ast.ParseFile
関数を呼んだ結果のうち最初のエラーのみが返却されます。また、エラーが置きてもすべてのファイルについてast.ParseFile
が実行されるようです。
戻り値のast.Package
は以下のように定義されている構造体です。もちろん、ast.Node
インタフェースを実装しています。
type Package struct {
Name string // package name
Scope *Scope // package scope across all files
Imports map[string]*Object // map of package id -> package object
Files map[string]*File // Go source files by filename
}
どうやらソースコードを見る限り、parser.ParseDir
関数で取得した、ast.Package
には、Imports
とScope
は設定されていないようです。
parser.ParseFile
続いて、parser.ParseFile
関数についてみてましょう。
シグニチャは以下のようになっていました。
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)
第2引数と第3引数が謎ですね。
ドキュメントとソースコードを読むと、第3引数のsrc
がnil
の場合、第2引数のファイル名を使ってソースコードがos.ReadFile
されるようです。
そして、src
は、以下の4種類を受け付けるようです。
string
[]byte
*bytes.Buffer
io.Reader
いずれも最終的には[]byte
型としてソースコードを扱っているようです。
また、ast.ParseFile
関数の戻り値は、*ast.File
型の値とエラーです。
ast.File
(token.File
とは違うので注意)は、以下のように定義されています。
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
Scope *Scope // package scope (this file only)
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file
Comments []*CommentGroup // list of all comments in the source file
}
Decls
フィールドから定義された関数や変数や型が取得できそうです。
また、Comments
からはファイル中のコメントの一覧が取得できるようです。色々使えそうですね。
parser.ParseExpr
parser.ParseExpr
とparser.ParseExprFrom
についてはここでは詳しく述べません。
簡単な式の評価機を作ってみるにも挙動を書いているので、ぜひそちらをご覧ください。
parser.Mode
さて、parser.ParseExpr
以外のParse
系の関数の最後の引数がparser.Mode
です。
これは、パースする際にどうパースするのかを表したフラグで以下のように定義されています。
type Mode uint
const (
PackageClauseOnly Mode = 1 << iota // stop parsing after package clause
ImportsOnly // stop parsing after import declarations
ParseComments // parse comments and add them to AST
Trace // print a trace of parsed productions
DeclarationErrors // report declaration errors
SpuriousErrors // same as AllErrors, for backward-compatibility
AllErrors = SpuriousErrors // report all errors (not just the first 10 on different lines)
)
それぞれの意味を説明していきましょう。
-
PackageClauseOnly
:package
の部分までパースする -
ImportsOnly
:import
の部分までパースする -
ParseComments
: コメントをパースして、ASTに追加する -
Trace
: パースの過程をトレースする -
DeclarationErrors
: 定義エラーを見つける(多重定義とか) -
SpuriousErrors
: 互換のためにある。AllErrors
と同じ -
AllErrors
: すべてのエラーを出す。そうじゃないと10個まで。
ast.Mode
はフラグのため、上記の機能を追加しない場合は、基本的に0
を指定しておけば問題ありません。
なお、エラーについては実際には、scanner.ErrorList
が返却されるため、parser.AllErrors
を指定した場合には、以下のように取得すると良いでしょう。
fset := token.NewFileSet()
_, err := parser.ParseFile(fset, "sample.go", []byte(src), parser.DeclarationErrors|parser.AllErrors)
if err != nil {
switch err := err.(type) {
case scanner.ErrorList:
for _, e := range err {
fmt.Printf("%s:%d:%d %s\n", e.Pos.Filename, e.Pos.Line, e.Pos.Column, e.Msg)
}
default:
fmt.Println(err)
}
return
}
ちなみに、parser.AllErrors
を指定しない場合には、10個目のエラーまでしか、scanner.ErrorList
に含まれません。その10個目までしか返却されないようにするのに、panic
を使って大域脱出(こことここ)をしていたのが面白かったです。
まとめ
この記事では、主にgo/parser
が提供するParse
系関数をベースにその周辺の型や関数について説明しました。
Parse
系の関数で得られた、ast.Node
インタフェースを実装したASTのノードをいじることで色々楽しいことができるので、ぜひやってみてください。
go
パッケージの他のサブパッケージについてもまた記事を書きたいと思います。