はじめに
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
を使って自作の開発ツールを作ってみて下さい。