コメントに関連付けられたコードを取得する #golang

## はじめに

[`go/ast`](https://golang.org/pkg/go/ast)パッケージには、コメントとそれに関連付けられたAST上のノードを取得できる`CommentMap`という機能があります。
たとえば、以下のようなコードがあった場合に、`Hoge`という型に関連付けられたコメント（`Hoge です。`）を簡単に取得できます。

```go
// Hoge です。
type Hoge struct {
     N int
}
```

この記事では、Goのソースコードからコメントを取得する方法および、`CommentMap`の解説と活用方法について述べたいと思います。

## コメントを取得する

ソースコードからコメントを取得するには、ソースコードをAST（抽象構文木）にする必要があります。
ここではその方法については解説しませんが、「[ASTを取得する方法を調べる](http://qiita.com/tenntenn/items/13340f2845316532b55a)」という記事で解説していますので、よろしければそちらをご覧ください。

さて、以下のようなソースコードがあったとします。

```go
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`](https://golang.org/pkg/go/parser/#ParseFile)に食わせ、[`ast.File`](https://golang.org/pkg/go/ast/#File) を取得します。
このとき、`ParseFile`関数に、[`parser.Mode`](https://golang.org/pkg/go/parser/#Mode)として、[`parser.ParseComments`](https://golang.org/pkg/go/parser/#ParseComments)を渡して、コメントもASTに含めるようにします。

```go
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "sample.go", src, parser.ParseComments)
if err != nil {
	log.Fatalln("Error:", err)
}
```

また、`ast.File`は以下のように定義されているため、`Comments`フィールドからコメントを取得することができます。

```go
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`](https://golang.org/pkg/go/ast/#CommentGroup)型はコメントをまとめたものです。
`ast.CommentGroup`は構造体で、以下のように定義されています。

```
type CommentGroup struct {
        List []*Comment // len(List) > 0
}
```

`List`フィールドから1つずつコメントを取り出すこともできますが、[`Text`](https://golang.org/pkg/go/ast/#CommentGroup.Text)メソッドからまとめて文字列として取り出すことができます。
たとえば、`ast.File`構造体の`Comments`フィールドから以下のように標準出力に表示することができます（[Playground](https://play.golang.org/p/Qxyuq25ynN)で動かす）。

```go
for _, cg := range f.Comments {
	fmt.Println(cg.Text())
}
```

`ast.CommentGroup`構造体は、前述のとおりコメント、つまり[`ast.Comment`](https://golang.org/pkg/go/ast/#Comment)をまとめたものです。
これは`/*`と`*/`に囲まれたブロックコメントだけではなく、`//`で始まるラインコメントも他のトークンや空行で挟んでいない複数のコメントをグループとしてまとめています。

実際に[上述のコード](https://play.golang.org/p/Qxyuq25ynN)を実行してみると、以下のような結果が表示されるかと思います。

```
comment for hoge

comment for main

line comment 1/2
line comment 2/2

		block comment1
			block comment2
```

1つの`ast.CommentGroup`ごとに空行を挟んで出力しているので、複数行に渡るラインコメントが1つのグループとしてまとめられているのが分かるでしょう。

またこのとき、`*CommentGroup`型の[`Text`](https://golang.org/pkg/go/ast/#CommentGroup.Text)メソッドは、以下のような処理をしたコメントを返します。

* `//`、`/*`、`*/`を取り除く
* ラインコメントの最初の空白を取り除く
* 最初の方につづいている空行と後ろの方に続いてる空行は取り除く
* 途中の複数行に渡る空行は1つにまとめられる
* 1行ごとの後ろに続く空白は取り除く
* 最後は改行で終わる

## コメントに関連付けられたコードを取得する

`ast.File`構造体の`Comments`フィールドからコメントは取得できることは分かりました。
しかし、コメントを取得する用途としては、単にコメントがほしいだけではなく、以下のような用途が考えられます。

* 特定文言のコメントを付けた構造体にメソッドを自動生成する
* コメントを付けたパッケージ変数へ再代入が行われていないかチェックする
* 使われていない変数の自動削除とそれに付けられたコメントの削除

これらを実現するには、コメントとコメントが付けられたコードに対応するAST上のノードを関連付ける必要があります。
この関連付けを行う機能が`CommentMap`というもので、関連付けられたコメントとノードは[`ast.CommentMap`](https://golang.org/pkg/go/ast/#CommentMap)として表現されます。

`ast.CommentMap`は以下のように定義されています。

```go
type CommentMap map[Node][]*CommentGroup
```

文字通り、[`ast.Node`](https://golang.org/pkg/go/ast/#Node)と`[]*ast.CommentGroup`のマップとなっています。
`ast.CommentMap`は、[`ast.NewCommentMap`](https://golang.org/pkg/go/ast/#NewCommentMap)によって取得することができます。
なお、この関数は以下のようなシグニチャを持ちます。

```go
func NewCommentMap(fset *token.FileSet, node Node, comments []*CommentGroup) CommentMap
```

第1引数の`fset`は`ast.ParseFile`などでパースする際に渡した`*token.FileSet`型の値を渡します。
`fset`からファイル中の何行目のどの位置にノードが存在するか[`Position`](https://golang.org/pkg/go/token/#FileSet.Position)メソッドで取得できるため、コメントと対象ノードの関連付けのために用いられます。

第2引数の`node`は、対象となるコードを表すASTのルートノードを示します。
また、第3引数の`comments`は関連付けを行うコメントで、`ast.File`構造体の`Comments`などから取得した`*ast.ComentGroup`型のスライスを用います。

`ast.NewCommentMap`は、以下のルールに従ってノード`n`とコメントグループ`g`を関連付けています。

* `g`が`n`と同じ行で後ろについている場合
* `g`が`n`の直後行にあり、すくなくとも`g`の後のノードの間に空行があること
* `g`が`n`の前にあり、上記の他のルールによって別のノードに関連付けられていない場合

また、上記の処理は、できるだけ大きな範囲のノード`n`に対して関連付けが行われるようになっています。

少しルールが分かりづらいですが、実際に[Playgroundで試してみる](https://play.golang.org/p/5FU6wmPJLP)と分かるかと思います。
なお、1つのノードに対して、複数のコメントグループが関連付けられる可能性があるため、`ast.CommentMap`は`ast.Node`と`[]*CommentGroup`のマップになっています。

## コードともに関連付けられたコメントを取り除く

`ast.CommentMap`は、[`Filter`](https://golang.org/pkg/go/ast/#CommentMap.Filter)というメソッドを持っています。
これは以下のように、指定したノードより下にあるノードだけから構成される新しい`ast.CommentMap`を生成します。

```go
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
}
```

用途は、[ドキュメントにあるサンプル](https://golang.org/pkg/go/ast/#example_CommentMap)にあるように、ASTから特定のノードを削除した場合に、そのノードに関連付けられているコメントも削除するために用いられるようです。

## おわりに

この記事では、Goのソースコードからコメントを取得する方法と、コメントに関連付けられたAST上のノードを取得する方法について説明しました。
`ast.CommentMap`を使うことで、構造体の定義にコメントとして注釈をいれ、コードの自動生成に利用したり、代入文に対する自作のlintを作ったりとさまざまな用途に用いることができます。
ぜひ、`ast.CommentMap`を使って自作の開発ツールを作ってみて下さい。