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"