[インデックス 13392] ファイルの概要
このコミットは、gofmt ツールにおけるコードの書き換え(リライト)処理中にコメントが正しく扱われるようにするための修正を含んでいます。具体的には、以下のファイルが変更されました。
src/cmd/gofmt/gofmt_test.go:gofmtのテストスイートに新しいテストケースが追加されました。このテストケースは、リライト時にコメントが適切に処理されることを検証します。src/cmd/gofmt/rewrite.go:gofmtのリライトロジックのコア部分が修正されました。ast.CommentMapを使用して、リライトによって削除されるコードに関連付けられたコメントも適切に削除されるように変更されています。src/cmd/gofmt/testdata/rewrite5.golden: 新しいテストケースの期待される出力(ゴールデンファイル)です。src/cmd/gofmt/testdata/rewrite5.input: 新しいテストケースの入力ファイルです。
コミット
- コミットハッシュ:
96a609c2d7fcb8e1dc370e0cda9e4eb3bfb77412 - 作者: Robert Griesemer gri@golang.org
- 日付: Mon Jun 25 13:58:28 2012 -0700
- コミットメッセージ:
gofmt: handle comments correctly in rewrites R=rsc CC=golang-dev https://golang.org/cl/6294076
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/96a609c2d7fcb8e1dc370e0cda9e4eb3bfb77412
元コミット内容
gofmt: handle comments correctly in rewrites
R=rsc
CC=golang-dev
https://golang.org/cl/6294076
変更の背景
gofmt はGo言語のコードを整形するためのツールであり、コードの抽象構文木(AST)を操作して整形や書き換えを行います。このコミット以前の gofmt のリライト機能には、コードの書き換え時にコメントの扱いに関する問題がありました。具体的には、リライトによって特定のコード要素が削除された場合、その要素に付随していたコメントが適切に削除されずに残ってしまう可能性がありました。
この問題は、コードの整形結果が意図しないコメントを含んでしまい、コードの可読性や正確性を損なう原因となります。特に、gofmt -r コマンドでコードのパターンマッチングと置換を行う際に顕著でした。このコミットは、このような状況でコメントが正しく処理され、不要なコメントが確実に削除されるようにするために導入されました。
新しいテストケース rewrite5.input と rewrite5.golden は、この問題の具体的なシナリオを示しています。x + x という式を 2 * x に書き換える際に、元の式に付随するコメントがどのように扱われるべきかを定義し、リライトによって削除されるべきコメントが正しく消えることを検証しています。
前提知識の解説
gofmt
gofmt は、Go言語のソースコードを標準的なスタイルに自動的に整形するツールです。コードのインデント、スペース、改行などを統一し、Goコミュニティ全体で一貫したコードスタイルを維持するのに役立ちます。また、単なる整形だけでなく、ASTを操作してコードの構造的な変更(例: gofmt -r によるパターンベースのリライト)も行うことができます。
Go言語の抽象構文木 (AST)
Goコンパイラやツール(gofmt など)は、Goのソースコードを直接テキストとして扱うのではなく、まず抽象構文木(AST: Abstract Syntax Tree)と呼ばれるデータ構造に変換します。ASTは、プログラムの構造を木構造で表現したもので、各ノードがプログラムの要素(変数、関数、式、文など)に対応します。
Goの go/ast パッケージは、このASTを扱うための型と関数を提供します。例えば、ast.File はGoのソースファイル全体を表すASTのルートノードです。ast.Expr は式を表すインターフェースで、ast.BinaryExpr(二項演算式)や ast.Ident(識別子)などがその具体的な型です。
コメントとAST
GoのASTでは、コメントはコードとは別に管理されますが、特定のASTノードに関連付けられることがあります。ast.File 構造体には Comments フィールドがあり、ファイル内のすべてのコメントグループ(ast.CommentGroup)のリストを保持します。ast.CommentGroup は、複数の連続するコメント(ast.Comment)をまとめたものです。
gofmt がコードをリライトする際、ASTノードが追加、削除、または変更されると、それに伴って関連するコメントも適切に処理される必要があります。特に、ノードが削除された場合、そのノードにのみ関連付けられていたコメントも削除されるべきです。
ast.CommentMap
ast.CommentMap は、go/ast パッケージで提供されるユーティリティで、ASTノードとコメントグループの関連付けを管理するために使用されます。これは、ASTを変換する際にコメントの配置を維持したり、不要なコメントをフィルタリングしたりするのに非常に役立ちます。
ast.NewCommentMap(fileset, node, comments) は、指定されたASTノードとコメントのリストから CommentMap を構築します。このマップは、各ASTノードがどのコメントグループに関連付けられているかを効率的に検索できるようにします。
cmap.Filter(node) メソッドは、新しいASTノード(または変換後のAST)を受け取り、元の CommentMap に基づいて、新しいASTに属するコメントのみを抽出します。これにより、リライトによって削除されたASTノードに関連付けられていたコメントは自動的に除外され、残ったASTノードに関連するコメントのみが保持されます。
技術的詳細
このコミットの技術的な核心は、gofmt のリライト処理において ast.CommentMap を導入し、コメントのライフサイクルをASTの変換と同期させる点にあります。
rewriteFile 関数は、与えられたパターンと置換式に基づいて、Goのソースファイル全体(ASTとして表現される *ast.File)を書き換えます。
-
ast.NewCommentMapの作成: リライト処理の開始時に、元のAST (p) とそのコメント (p.Comments) を基にast.CommentMap(cmap) が作成されます。このcmapは、元のコードにおけるASTノードとコメントの関連付けのスナップショットを保持します。これにより、どのコメントがどのコード要素に属していたかを追跡できます。cmap := ast.NewCommentMap(fileSet, p, p.Comments)ここで
fileSetは、ソースファイルの位置情報(行番号、列番号など)を管理するtoken.FileSetオブジェクトです。 -
ASTの変換:
apply(f, reflect.ValueOf(p)).Interface().(*ast.File)の呼び出しによって、実際のリライト処理が行われ、元のAST (p) が新しいAST (r) に変換されます。このapply関数は、リフレクションを使用してASTノードを再帰的に走査し、パターンに一致するノードを置換します。この段階では、コメント自体は直接操作されません。 -
コメントのフィルタリング: 変換された新しいAST (
r) が得られた後、cmap.Filter(r).Comments()が呼び出されます。cmap.Filter(r)は、元のCommentMap(cmap) を使用して、新しいAST (r) に含まれるノードに関連付けられているコメントグループのみを抽出します。つまり、リライトによって削除されたASTノードに付随していたコメントは、このフィルタリングプロセスで自動的に除外されます。.Comments()メソッドは、フィルタリングされたコメントグループのリストを返します。
-
新しいASTへのコメントの割り当て: 最後に、フィルタリングされたコメントのリストが、新しいAST (
r) のCommentsフィールドに割り当てられます。r.Comments = cmap.Filter(r).Comments() // recreate comments listこれにより、リライト後のASTは、その構造に合致する適切なコメントのみを持つことになります。
このメカニズムにより、gofmt はコードのリライト時に、削除されたコード要素に付随するコメントを自動的にクリーンアップし、整形後のコードの正確性と整合性を保つことができます。
コアとなるコードの変更箇所
src/cmd/gofmt/rewrite.go ファイルの rewriteFile 関数における変更点です。
--- a/src/cmd/gofmt/rewrite.go
+++ b/src/cmd/gofmt/rewrite.go
@@ -55,6 +55,7 @@ func dump(msg string, val reflect.Value) {
// rewriteFile applies the rewrite rule 'pattern -> replace' to an entire file.
func rewriteFile(pattern, replace ast.Expr, p *ast.File) *ast.File {
+ cmap := ast.NewCommentMap(fileSet, p, p.Comments)
m := make(map[string]reflect.Value)
pat := reflect.ValueOf(pattern)
repl := reflect.ValueOf(replace)
@@ -73,7 +74,9 @@ func rewriteFile(pattern, replace ast.Expr, p *ast.File) *ast.File {
}
return val
}\n- return apply(f, reflect.ValueOf(p)).Interface().(*ast.File)
+ r := apply(f, reflect.ValueOf(p)).Interface().(*ast.File)
+ r.Comments = cmap.Filter(r).Comments() // recreate comments list
+ return r
}
// setValue is a wrapper for x.SetValue(y); it protects
コアとなるコードの解説
変更された rewriteFile 関数は、以下の2つの主要な行を追加・変更しています。
-
cmap := ast.NewCommentMap(fileSet, p, p.Comments): この行は、リライト処理の開始時にast.CommentMapを初期化しています。fileSetはファイルのメタデータ(位置情報など)を提供し、pはリライト対象の元のASTファイル、p.Commentsは元のファイルに存在するすべてのコメントグループのリストです。このCommentMapは、元のASTノードとコメントの関連付けを記憶するためのものです。 -
r := apply(f, reflect.ValueOf(p)).Interface().(*ast.File): この行は、元のreturn文を分割し、apply関数によって返された新しいASTをrという変数に格納しています。apply関数は、定義されたリライトルール (pattern -> replace) を元のAST (p) に適用し、変換されたASTを返します。 -
r.Comments = cmap.Filter(r).Comments() // recreate comments list: この行が、コメントの正しい処理を保証する最も重要な部分です。cmap.Filter(r): 先ほど作成したCommentMap(cmap) を使用し、リライトによって生成された新しいAST (r) に含まれるノードに関連付けられているコメントのみを抽出します。これにより、リライトによって削除されたコード要素に付随していたコメントは、この段階で自動的にフィルタリングされ、結果のコメントリストから除外されます。.Comments(): フィルタリングされたコメントグループのリストを取得します。r.Comments = ...: フィルタリングされたコメントリストを、新しいAST (r) のCommentsフィールドに割り当てます。
この変更により、gofmt はリライト後のコードに不要なコメントが残ることを防ぎ、より正確でクリーンな整形結果を提供できるようになりました。
関連リンク
- Go CL 6294076: https://golang.org/cl/6294076
参考にした情報源リンク
- Go言語の
go/astパッケージドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/tokenパッケージドキュメント: https://pkg.go.dev/go/token gofmtの公式ドキュメント (Goコマンド): https://pkg.go.dev/cmd/gofmt- Go言語のASTに関するブログ記事やチュートリアル (一般的な情報源)