go言語のASTの全てのコメントは、*ast.File以下の子孫ノードから集める事はできないという話
*ast.File
はファイル中のすべてのコメントを持っている
goでASTを取り出すと、通常はファイル単位で扱うことになる。この時の値の型は *ast.File
。ここで対応するファイル中のすべてのコメントは ast.File
のCommentsフィールドにある。コメントは *ast.CommentGroup
という型(複数行コメント等に対応するためのCommentではなくCommentGroupというcollectionになっている)。
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 }
ところで、このCommentsフィールドはそのファイルに含まれる全てのコメントと言ったのは間違いではなく。例えば、子孫ノード中に含まれるコメントも全て格納されている。
例として以下のような関数が定義されているとする。
// F : do something func F() { return 1 }
この時取り出された *ast.File
はもちろん上の // F : do something
というコメントを保持している。
*ast.File
の子孫ノードが持つコメント
先程の関数Fに対応するASTのノードは *ast.FuncDecl
という型の値になる。これらの値が *ast.File
の子孫ノードとして存在する。これらの型もコメントを持つ。
// A FuncDecl node represents a function declaration. type FuncDecl struct { Doc *CommentGroup // associated documentation; or nil Recv *FieldList // receiver (methods); or nil (functions) Name *Ident // function/method name Type *FuncType // function signature: parameters, results, and position of "func" keyword Body *BlockStmt // function body; or nil for external (non-Go) function }
ちなみに先程の関数Fがparseされて作られたノードは、以下の様にDocというフィールドにコメントを持っている。Text()というメソッドで文字列化した値が取れる。
// F に*ast.FuncDeclが入っているとする F.Doc.Text() // => "F : do something\n"
同様に幾つかのstructはDocというフィールドとCommentというフィールドを持っている。
例えばstructとそのfield定義。
// S : type S struct { // Name : Name string // name }
ここでSというstructの型の定義は、*ast.TypeSpec
。その中でのNameフィールドの定義は *ast.Field
。それぞれDocとCommentというフィールドを持っている。
type TypeSpec struct { Doc *CommentGroup // associated documentation; or nil Name *Ident // type name Assign token.Pos // position of '=', if any Type Expr // *Ident, *ParenExpr, *SelectorExpr, *StarExpr, or any of the *XxxTypes Comment *CommentGroup // line comments; or nil } type Field struct { Doc *CommentGroup // associated documentation; or nil Names []*Ident // field/method/parameter names; or nil if anonymous field Type Expr // field/method/parameter type Tag *BasicLit // field tag; or nil Comment *CommentGroup // line comments; or nil }
*ast.File
の子孫ノードのコメントを集めれば全てのコメントになる?
ここからがこの記事の主題。 *ast.File
はファイル中に含まれるすべてのコメントを保持している。ここで子孫ノードのコメントの値を集めることでもファイル中に含まれるすべてのコメントの値を手にすることはできるか?という話。
もし仮にできるのだとしたら以下のようなコードですべてのコメントを手にすることができる。
// f は *ast.File var comments []*ast.CommentGroup ast.Inspect(f, func(node ast.Node) bool { if node != nil { if x, ok := node.(*ast.CommentGroup); ok { comments = append(comments, x) } } return true })
ast.Inspectはすべてのノードをたどるwalker関数。戻り値がtrueならさらに深く調べる。falseなら中断する。今回は子孫ノードが保持するすべてのCommentGroupを集められるはず。
子孫ノードのすべてのコメントを集めてもダメ
結論から言うと、子孫ノードのすべてのコメントを集めてもファイル中のすべてのコメントにはならない。
例えば行毎のコメントなど子孫ノードには含まれない。ここでの行毎のコメントとは以下のようなもののこと。
// F : func F(x, y int) int { if x > 0 { // x is must be positive <- これ fmt.Println("hmm") } return x + y }
また関数と関数のようなトップレベルの定義の間に含まれるコメントも含まれない。
// F : func F(x, y int) int { return x + y } // top level comment // G : func G(x, y int) int { return x - y }
そんなわけで各ノードに対応するコメントというものを単純に示す事はできず。ast.Node
(すべてのastのノードの値が実装しているinterface) の Pos()
などで得られる座標を元に頑張るしか無い(goのASTはFST(Full Syntax Tree)ではない)。
コード例
コード例
package main import ( "fmt" "go/ast" "go/parser" "go/token" "log" "sort" ) func main() { source := ` package p import ( "fmt" // comment in import specs ) // S : type S struct { // Name : Name string // name // Value : Value int // value } // end of S // top level comment // F : func F(x, y int) int { if x > 0 { // x is must be positive fmt.Println("hmm") } if y > 0 { // y is must be positive fmt.Println("hmmmmm") } return x + y } ` fset := token.NewFileSet() f, err := parser.ParseFile(fset, "", source, parser.ParseComments) if err != nil { log.Fatal(err) } fmt.Println("*** all comments ***") for _, cg := range f.Comments { fmt.Printf("(%d, %d) %q\n", cg.Pos(), cg.End(), cg.Text()) } fmt.Println("----------------------------------------") var comments []*ast.CommentGroup ast.Inspect(f, func(node ast.Node) bool { if node != nil { if x, ok := node.(*ast.CommentGroup); ok { comments = append(comments, x) } } return true }) sort.Slice(comments, func(i, j int) bool { return comments[i].Pos() < comments[j].Pos() }) fmt.Println("*** comments from children ***") for _, cg := range comments { fmt.Printf("(%d, %d) %q\n", cg.Pos(), cg.End(), cg.Text()) } }
結果
*** all comments *** (29, 55) "comment in import specs\n" (59, 65) "S :\n" (83, 92) "Name :\n" (107, 114) "name\n" (116, 126) "Value :\n" (139, 147) "value\n" (150, 161) "end of S\n" (163, 183) "top level comment\n" (185, 191) "F :\n" (229, 253) "x is must be positive\n" (292, 316) "y is must be positive\n" ---------------------------------------- *** comments from children *** (29, 55) "comment in import specs\n" (59, 65) "S :\n" (83, 92) "Name :\n" (107, 114) "name\n" (116, 126) "Value :\n" (139, 147) "value\n" (150, 161) "end of S\n" (185, 191) "F :\n"