はじめに
go/astパッケージには、コメントとそれに関連付けられたAST上のノードを取得できるCommentMapという機能があります。
たとえば、以下のようなコードがあった場合に、Hogeという型に関連付けられたコメント(Hoge です。)を簡単に取得できます。
// Hoge です。
type Hoge struct {
N int
}
この記事では、Goのソースコードからコメントを取得する方法および、CommentMapの解説と活用方法について述べたいと思います。
コメントを取得する
ソースコードからコメントを取得するには、ソースコードをAST(抽象構文木)にする必要があります。
ここではその方法については解説しませんが、「ASTを取得する方法を調べる」という記事で解説していますので、よろしければそちらをご覧ください。
さて、以下のようなソースコードがあったとします。
package main
// comment for hoge
var hoge int
// comment for main
func main() {
// line comment 1/2
// line comment 2/2
/*
block comment1
block comment2
*/
}
このコード中のコメント部分を取り出してみましょう。
手順としては以下のように、ソースコードをparser.ParseFileに食わせ、ast.File を取得します。
このとき、ParseFile関数に、parser.Modeとして、parser.ParseCommentsを渡して、コメントもASTに含めるようにします。
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "sample.go", src, parser.ParseComments)
if err != nil {
log.Fatalln("Error:", err)
}
また、ast.Fileは以下のように定義されているため、Commentsフィールドからコメントを取得することができます。
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フィールドから取得できるのは、*CommentGroup型のスライスで、ast.CommentGroup型はコメントをまとめたものです。
ast.CommentGroupは構造体で、以下のように定義されています。
type CommentGroup struct {
List []*Comment // len(List) > 0
}
Listフィールドから1つずつコメントを取り出すこともできますが、Textメソッドからまとめて文字列として取り出すことができます。
たとえば、ast.File構造体のCommentsフィールドから以下のように標準出力に表示することができます(Playgroundで動かす)。
for _, cg := range f.Comments {
fmt.Println(cg.Text())
}
ast.CommentGroup構造体は、前述のとおりコメント、つまりast.Commentをまとめたものです。
これは/*と*/に囲まれたブロックコメントだけではなく、//で始まるラインコメントも他のトークンや空行で挟んでいない複数のコメントをグループとしてまとめています。
実際に上述のコードを実行してみると、以下のような結果が表示されるかと思います。
comment for hoge
comment for main
line comment 1/2
line comment 2/2
block comment1
block comment2
1つのast.CommentGroupごとに空行を挟んで出力しているので、複数行に渡るラインコメントが1つのグループとしてまとめられているのが分かるでしょう。
またこのとき、*CommentGroup型のTextメソッドは、以下のような処理をしたコメントを返します。
-
//、/*、*/を取り除く - ラインコメントの最初の空白を取り除く
- 最初の方につづいている空行と後ろの方に続いてる空行は取り除く
- 途中の複数行に渡る空行は1つにまとめられる
- 1行ごとの後ろに続く空白は取り除く
- 最後は改行で終わる
コメントに関連付けられたコードを取得する
ast.File構造体のCommentsフィールドからコメントは取得できることは分かりました。
しかし、コメントを取得する用途としては、単にコメントがほしいだけではなく、以下のような用途が考えられます。
- 特定文言のコメントを付けた構造体にメソッドを自動生成する
- コメントを付けたパッケージ変数へ再代入が行われていないかチェックする
- 使われていない変数の自動削除とそれに付けられたコメントの削除
これらを実現するには、コメントとコメントが付けられたコードに対応するAST上のノードを関連付ける必要があります。
この関連付けを行う機能がCommentMapというもので、関連付けられたコメントとノードはast.CommentMapとして表現されます。
ast.CommentMapは以下のように定義されています。
type CommentMap map[Node][]*CommentGroup
文字通り、ast.Nodeと[]*ast.CommentGroupのマップとなっています。
ast.CommentMapは、ast.NewCommentMapによって取得することができます。
なお、この関数は以下のようなシグニチャを持ちます。
func NewCommentMap(fset *token.FileSet, node Node, comments []*CommentGroup) CommentMap
第1引数のfsetはast.ParseFileなどでパースする際に渡した*token.FileSet型の値を渡します。
fsetからファイル中の何行目のどの位置にノードが存在するかPositionメソッドで取得できるため、コメントと対象ノードの関連付けのために用いられます。
第2引数のnodeは、対象となるコードを表すASTのルートノードを示します。
また、第3引数のcommentsは関連付けを行うコメントで、ast.File構造体のCommentsなどから取得した*ast.ComentGroup型のスライスを用います。
ast.NewCommentMapは、以下のルールに従ってノードnとコメントグループgを関連付けています。
-
gがnと同じ行で後ろについている場合 -
gがnの直後行にあり、すくなくともgの後のノードの間に空行があること -
gがnの前にあり、上記の他のルールによって別のノードに関連付けられていない場合
また、上記の処理は、できるだけ大きな範囲のノードnに対して関連付けが行われるようになっています。
少しルールが分かりづらいですが、実際にPlaygroundで試してみると分かるかと思います。
なお、1つのノードに対して、複数のコメントグループが関連付けられる可能性があるため、ast.CommentMapはast.Nodeと[]*CommentGroupのマップになっています。
コードともに関連付けられたコメントを取り除く
ast.CommentMapは、Filterというメソッドを持っています。
これは以下のように、指定したノードより下にあるノードだけから構成される新しいast.CommentMapを生成します。
func (cmap CommentMap) Filter(node Node) CommentMap {
umap := make(CommentMap)
Inspect(node, func(n Node) bool {
if g := cmap[n]; len(g) > 0 {
umap[n] = g
}
return true
})
return umap
}
用途は、ドキュメントにあるサンプルにあるように、ASTから特定のノードを削除した場合に、そのノードに関連付けられているコメントも削除するために用いられるようです。
おわりに
この記事では、Goのソースコードからコメントを取得する方法と、コメントに関連付けられたAST上のノードを取得する方法について説明しました。
ast.CommentMapを使うことで、構造体の定義にコメントとして注釈をいれ、コードの自動生成に利用したり、代入文に対する自作のlintを作ったりとさまざまな用途に用いることができます。
ぜひ、ast.CommentMapを使って自作の開発ツールを作ってみて下さい。