[インデックス 18049] ファイルの概要
このコミットは、Go言語のgo/ast
パッケージにおけるCommentMap
の利用例を追加するものです。具体的には、Goプログラムの抽象構文木(AST)から変数宣言を削除する際に、関連するコメントの関連付けを正しく維持する方法を示しています。
コミット
commit 108d35bd8eae996325f4387e85b52b1af1d6ba73
Author: Robert Griesemer <gri@golang.org>
Date: Wed Dec 18 10:10:40 2013 -0800
go/ast: added example illustrating CommentMap use.
R=bradfitz
CC=golang-dev
https://golang.org/cl/43930043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/108d35bd8eae996325f4387e85b52b1af1d6ba73
元コミット内容
go/ast: added example illustrating CommentMap use.
このコミットは、go/ast
パッケージにCommentMap
の使用方法を示す例を追加します。
変更の背景
Go言語のツール(goimports
、gofmt
など)は、Goのソースコードを解析し、抽象構文木(AST)を操作することで機能します。ASTを操作する際、コードの構造だけでなく、コメントも適切に扱うことが重要です。コメントはコードの可読性や意図を伝える上で不可欠な要素であり、ASTの変更によってコメントが失われたり、誤った場所に移動したりすると、コードの品質が著しく低下します。
go/ast
パッケージには、ASTノードとコメントの関連付けを管理するためのCommentMap
という構造体が存在します。しかし、その具体的な使用方法、特にASTの変更に伴うコメントのフィルタリングや再構築の方法は、ドキュメントだけでは理解しにくい場合があります。
このコミットは、CommentMap
の具体的な利用シナリオ、すなわちASTから特定のノード(この場合は変数宣言)を削除する際に、そのノードに関連付けられていたコメントを適切に処理し、残りのコメントを維持する方法を実例として示すことで、開発者がCommentMap
をより効果的に活用できるようにすることを目的としています。これにより、AST操作を行うツールの開発者が、コメントの整合性を保ちながらより堅牢なコード変換ロジックを実装できるようになります。
前提知識の解説
抽象構文木 (AST)
抽象構文木(Abstract Syntax Tree, AST)は、ソースコードの抽象的な構文構造を木構造で表現したものです。コンパイラやインタープリタがソースコードを解析する際に中間表現として生成します。Go言語では、go/parser
パッケージがソースコードを解析してASTを生成し、go/ast
パッケージがASTの構造を定義しています。ASTは、プログラムの構造(関数、変数、式など)を階層的に表現するため、プログラムの静的解析、コード生成、リファクタリングツールなどの開発に利用されます。
go/token
パッケージ
go/token
パッケージは、Go言語のソースコードにおけるトークン(キーワード、識別子、演算子など)と、それらのトークンがソースコード内のどこに位置するかを示す位置情報(Pos
)を定義します。FileSet
は、複数のファイルにわたる位置情報を管理するための構造体で、ASTノードがソースコードのどの部分に対応するかを正確に特定するために使用されます。
go/ast
パッケージ
go/ast
パッケージは、Go言語のASTのデータ構造を定義します。ast.File
はGoの単一のソースファイルを表すASTのルートノードです。ast.Decl
は宣言(変数宣言、関数宣言など)を表すインターフェースで、ast.GenDecl
はvar
, const
, type
などの一般的な宣言を表します。
ast.Comment
とast.CommentGroup
ast.Comment
は単一のコメント(例: // comment
または /* comment */
)を表し、ast.CommentGroup
は連続するコメントのブロック(例: // comment1\n// comment2
)を表します。ast.File
には、ファイル全体から抽出されたすべてのコメントグループのリストが含まれています。
ast.CommentMap
ast.CommentMap
は、ASTノードとコメントグループの関連付けを管理するためのデータ構造です。parser.ParseComments
オプションを使用してソースコードを解析すると、ast.File
のComments
フィールドにすべてのコメントグループが格納されます。しかし、これらのコメントグループはASTノードと直接的な関連付けを持っていません。ast.NewCommentMap
関数は、FileSet
、ast.File
、およびコメントグループのリストを受け取り、ASTノードとコメントグループの間の「最も近い」関連付けを推測してCommentMap
を構築します。これにより、特定のASTノードが削除されたり移動されたりした場合でも、そのノードに関連するコメントを識別し、適切に処理することが可能になります。
CommentMap
の主な機能は以下の通りです。
- 関連付けの構築: ASTノードとコメントグループの間の論理的な関連付けを確立します。
- フィルタリング: ASTの変更後、もはや関連性のないコメントを
Filter
メソッドを使って除去し、新しいコメントリストを生成します。
go/format
パッケージ
go/format
パッケージは、Goのソースコードを標準的なGoのフォーマット規則に従って整形するために使用されます。ASTを操作した後、format.Node
関数を使って整形されたソースコードをバイト列として出力することができます。
技術的詳細
このコミットで追加されたExampleCommentMap
関数は、以下のステップでast.CommentMap
の利用方法をデモンストレーションしています。
-
ソースコードの準備: 操作対象となるGoのソースコード文字列を定義します。このソースコードには、パッケージコメント、定数宣言、変数宣言、関数宣言が含まれ、それぞれにコメントが付与されています。特に、削除対象となる変数宣言
var foo = hello
には、その前後にコメントが関連付けられています。 -
ASTの生成:
go/parser.ParseFile
関数を使用して、準備したソースコードからASTを生成します。この際、parser.ParseComments
オプションを渡すことで、コメントもASTの一部として解析され、ast.File
のComments
フィールドに格納されます。 -
ast.CommentMap
の作成:ast.NewCommentMap
関数を呼び出し、生成されたast.File
からCommentMap
を作成します。このCommentMap
は、ASTノードとコメントグループの間の関連付けを内部的に保持します。 -
ASTの操作(変数宣言の削除):
removeFirstVarDecl
ヘルパー関数を呼び出し、ASTの宣言リスト(f.Decls
)から最初の変数宣言を削除します。この関数は、ast.Decl
のリストを走査し、ast.GenDecl
型でtoken.VAR
トークンを持つ宣言(つまり変数宣言)を見つけて削除します。 -
コメントのフィルタリング: ASTの変更後、
CommentMap
のFilter
メソッドを使用して、もはやASTのどのノードとも関連付けられていないコメントをフィルタリングします。cmap.Filter(f).Comments()
は、変更後のAST (f
) に基づいて、関連性のないコメントを除外し、新しいコメントグループのリストを返します。この新しいリストがf.Comments
に再割り当てされます。これにより、削除された変数宣言に関連するコメントがASTから適切に除去されます。 -
整形されたASTの出力:
go/format.Node
関数を使用して、変更され、コメントがフィルタリングされたASTを整形し、bytes.Buffer
に書き込みます。最終的に、その内容が標準出力に出力されます。
この一連のプロセスにより、var foo = hello
とその関連コメントが削除され、残りのコードとコメントが正しく整形されて出力されることが示されます。これは、AST操作においてコメントの整合性を維持するためのCommentMap
の重要な役割を明確に示しています。
コアとなるコードの変更箇所
変更はsrc/pkg/go/ast/example_test.go
ファイルに集中しており、ExampleCommentMap
関数とそのヘルパー関数removeFirstVarDecl
が追加されています。
--- a/src/pkg/go/ast/example_test.go
+++ b/src/pkg/go/ast/example_test.go
@@ -5,8 +5,10 @@
package ast_test
import (
+ "bytes"
"fmt"
"go/ast"
+ "go/format"
"go/parser"
"go/token"
)
@@ -134,3 +136,75 @@ func main() {
// 57 . }
// 58 }\n
}\n
++
+// This example illustrates how to remove a variable declaration
+// in a Go program while maintaining correct comment association
+// using an ast.CommentMap.
+func ExampleCommentMap() {
+ // src is the input for which we create the AST that we
+ // are going to manipulate.
+ src := `
+// This is the package comment.
+package main
+
+// This comment is associated with the hello constant.
+const hello = "Hello, World!" // line comment 1
+
+// This comment is associated with the foo variable.
+var foo = hello // line comment 2
+
+// This comment is associated with the main function.
+func main() {
+ fmt.Println(hello) // line comment 3
+}
+`
+
+ // Create the AST by parsing src.
+ fset := token.NewFileSet() // positions are relative to fset
+ f, err := parser.ParseFile(fset, "src.go", src, parser.ParseComments)
+ if err != nil {
+ panic(err)
+ }
+
+ // Create an ast.CommentMap from the ast.File's comments.
+ // This helps keeping the association between comments
+ // and AST nodes.
+ cmap := ast.NewCommentMap(fset, f, f.Comments)
+
+ // Remove the first variable declaration from the list of declarations.
+ f.Decls = removeFirstVarDecl(f.Decls)
+
+ // Use the comment map to filter comments that don't belong anymore
+ // (the comments associated with the variable declaration), and create
+ // the new comments list.
+ f.Comments = cmap.Filter(f).Comments()
+
+ // Print the modified AST.
+ var buf bytes.Buffer
+ if err := format.Node(&buf, fset, f); err != nil {
+ panic(err)
+ }
+ fmt.Printf("%s", buf.Bytes())
+
+ // output:\n
+ // // This is the package comment.\n
+ // package main\n
+ //\n
+ // // This comment is associated with the hello constant.\n
+ // const hello = "Hello, World!" // line comment 1\n
+ //\n
+ // // This comment is associated with the main function.\n
+ // // \tfmt.Println(hello) // line comment 3\n
+ // // }\n
+ // \tfmt.Println(hello) // line comment 3\n
+ // }\n
+}\n
+\n
+func removeFirstVarDecl(list []ast.Decl) []ast.Decl {
+ for i, decl := range list {
+ if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.VAR {
+ copy(list[i:], list[i+1:])
+ return list[:len(list)-1]
+ }
+ }
+ panic("variable declaration not found")
+}
コアとなるコードの解説
ExampleCommentMap
関数
この関数は、ast.CommentMap
の利用方法をエンドツーエンドで示しています。
-
src
文字列: 操作対象のGoソースコードを定義しています。このコードには、パッケージコメント、定数宣言、変数宣言、関数宣言が含まれ、それぞれにコメントが付与されています。特に、var foo = hello // line comment 2
という行が削除対象となります。 -
ASTのパース:
fset := token.NewFileSet() f, err := parser.ParseFile(fset, "src.go", src, parser.ParseComments)
parser.ParseFile
は、src
文字列からASTを構築します。parser.ParseComments
フラグにより、コメントも解析され、f.Comments
に格納されます。fset
は、ソースコード内の位置情報を管理するために必要です。 -
CommentMap
の作成:cmap := ast.NewCommentMap(fset, f, f.Comments)
ここで、
ast.NewCommentMap
が呼び出され、fset
、パースされたast.File
(f
)、およびファイルから抽出されたコメントのリスト (f.Comments
) を基にCommentMap
が作成されます。このマップは、ASTノードとコメントの関連付けを内部的に構築します。 -
変数宣言の削除:
f.Decls = removeFirstVarDecl(f.Decls)
f.Decls
は、ファイル内のトップレベル宣言のリストです。removeFirstVarDecl
関数を呼び出すことで、このリストから最初の変数宣言が削除されます。 -
コメントのフィルタリング:
f.Comments = cmap.Filter(f).Comments()
これが
CommentMap
の最も重要な利用箇所です。cmap.Filter(f)
は、変更後のAST (f
) を受け取り、もはやASTのどのノードとも関連付けられていないコメント(この場合は削除された変数宣言に関連するコメント)を除外した新しいCommentMap
を返します。.Comments()
メソッドはその新しいCommentMap
からコメントグループのリストを取得し、それをf.Comments
に再割り当てすることで、ASTから不要なコメントを削除します。 -
整形と出力:
var buf bytes.Buffer if err := format.Node(&buf, fset, f); err != nil { panic(err) } fmt.Printf("%s", buf.Bytes())
変更されたAST (
f
) は、go/format
パッケージのformat.Node
関数によって標準的なGoのフォーマット規則に従って整形され、bytes.Buffer
に書き込まれます。最終的に、その内容が標準出力に表示されます。出力結果は、var foo = hello
とその関連コメントが削除され、残りのコードが正しく整形されていることを示しています。
removeFirstVarDecl
関数
func removeFirstVarDecl(list []ast.Decl) []ast.Decl {
for i, decl := range list {
if gen, ok := decl.(*ast.GenDecl); ok && gen.Tok == token.VAR {
copy(list[i:], list[i+1:])
return list[:len(list)-1]
}
}
panic("variable declaration not found")
}
このヘルパー関数は、ast.Decl
のスライスを受け取り、その中から最初の変数宣言(ast.GenDecl
型でtoken.VAR
トークンを持つもの)を見つけて削除します。スライス操作によって要素を削除し、新しいスライスを返します。もし変数宣言が見つからない場合はパニックします。
関連リンク
- Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/parser
パッケージのドキュメント: https://pkg.go.dev/go/parser - Go言語の
go/token
パッケージのドキュメント: https://pkg.go.dev/go/token - Go言語の
go/format
パッケージのドキュメント: https://pkg.go.dev/go/format
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のAST操作に関する一般的な情報源(例: Go AST Examples, Go AST Walk)
go/ast
パッケージのソースコード- Go言語のコードフォーマッタ
gofmt
の内部実装(AST操作の典型例) - Go言語の静的解析ツールに関する記事やチュートリアル