[インデックス 13388] ファイルの概要
このコミットは、Go言語のgo/ast
パッケージにおけるコメントマップ(CommentMap
)APIの軽微な変更に関するものです。具体的には、NewCommentMap
関数のシグネチャが変更され、Update
メソッドとString
メソッドがCommentMap
型に追加されました。これにより、AST(抽象構文木)の操作とデバッグがより柔軟かつ容易になります。
コミット
commit 277e7e57cadc08a2e82885b423308627e9e5c786
Author: Robert Griesemer <gri@golang.org>
Date: Mon Jun 25 11:27:54 2012 -0700
go/ast: minor comment maps API change
This is a new, not yet committed API.
- Changed NewCommentMap to be independent of
*File nodes and more symmetric with the
Filter and Comments methods.
- Implemented Update method for use in
AST modifications.
- Implemented String method for debugging
R=rsc
CC=golang-dev
https://golang.org/cl/6303086
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/277e7e57cadc08a2e82885b423308627e9e5c786
元コミット内容
このコミットは、go/ast
パッケージ内のCommentMap
という、まだコミットされていない新しいAPIに対する変更です。主な変更点は以下の通りです。
NewCommentMap
関数の変更:*File
ノードへの依存をなくし、Filter
メソッドやComments
メソッドとの対称性を高めました。Update
メソッドの実装:ASTの変更時に使用するためのUpdate
メソッドが追加されました。String
メソッドの実装:デバッグ目的でString
メソッドが追加されました。
変更の背景
この変更は、Go言語のgo/ast
パッケージにおけるコメントマップAPIの設計改善を目的としています。CommentMap
は、Goのソースコードから生成される抽象構文木(AST)内のノードと、それに関連するコメントグループをマッピングするためのデータ構造です。
元のNewCommentMap
関数は、特定の*ast.File
ノードに強く依存していました。しかし、ASTを操作する際には、*ast.File
全体ではなく、より汎用的なast.Node
に対してコメントマップを生成したり、既存のコメントマップを更新したりするニーズがあります。このコミットは、NewCommentMap
をより汎用的なast.Node
を受け入れるように変更することで、APIの柔軟性を高め、Filter
やComments
といった他のメソッドとの一貫性を持たせることを目指しています。
また、ASTの変更(例えば、ノードの置換や削除)を行う際に、それに伴ってコメントマップも適切に更新する必要が生じます。このニーズに応えるため、Update
メソッドが導入されました。これにより、ASTの構造が変更されても、コメントとノードの関連付けを維持できるようになります。
さらに、CommentMap
の内部状態をデバッグ時に確認しやすくするため、String
メソッドが追加されました。これは、開発者がCommentMap
の動作を理解し、問題を特定する上で非常に有用です。
要するに、この変更はgo/ast
パッケージのコメント処理機能をより堅牢で使いやすくするための、APIの洗練と機能拡張の一環です。
前提知識の解説
Go言語のgo/ast
パッケージ
go/ast
パッケージは、Go言語のソースコードを解析して抽象構文木(Abstract Syntax Tree, AST)を構築するための標準ライブラリです。ASTは、プログラムの構造を木構造で表現したもので、コンパイラ、リンター、コードフォーマッター、静的解析ツールなど、Goのコードをプログラム的に操作する様々なツールで利用されます。
ast.Node
: AST内のすべてのノードが実装するインターフェースです。関数宣言、変数宣言、式、ステートメントなど、コードのあらゆる要素がノードとして表現されます。ast.File
: 単一のGoソースファイル全体を表すASTノードです。パッケージ名、インポート、宣言、そしてファイル内のコメントグループを含みます。ast.CommentGroup
: ソースコード内の1つ以上の連続するコメント(// comment
や/* comment */
)を表す構造体です。
抽象構文木(AST)
ASTは、プログラミング言語のソースコードの抽象的な構文構造を、ツリー形式で表現したものです。各ノードはソースコード内の構成要素(例えば、演算子、変数、関数呼び出しなど)を表し、ノード間の関係はコードの構造を示します。ASTは、ソースコードの字句解析(トークン化)と構文解析(パース)の後に生成され、セマンティック解析やコード生成の前段階として利用されます。
CommentMap
の役割
Go言語では、コメントは単なるドキュメントとしてだけでなく、godoc
ツールによるドキュメント生成や、コード生成ツールなどにおいて、コードのセマンティクスの一部として扱われることがあります。go/ast
パッケージは、ソースコードをパースする際にコメントも抽出しますが、これらのコメントはASTノードとは直接的に関連付けられていません。
CommentMap
は、このASTノードとコメントグループの関連付けを管理するためのデータ構造です。具体的には、map[ast.Node][]*ast.CommentGroup
のような形式で、特定のASTノードにどのコメントグループが関連付けられているかをマッピングします。これにより、ツールはコードの構造とコメントの両方を考慮した処理を行うことができます。例えば、特定の関数宣言に付随するドキュメンテーションコメントを取得したり、コード変更時にコメントの位置を適切に維持したりする際にCommentMap
が利用されます。
CommentMap
は、コメントがコードのどの部分に属するかを判断するためのロジック(例えば、行末コメントは直前のステートメントに、ブロックコメントは次の宣言に、といったルール)に基づいて構築されます。
技術的詳細
このコミットの技術的詳細は、go/ast
パッケージ内のcommentmap.go
ファイルにおけるCommentMap
のAPI変更に集約されます。
-
NewCommentMap
関数のシグネチャ変更:- 変更前:
func NewCommentMap(fset *token.FileSet, f *File) CommentMap
- この関数は
*ast.File
型の引数f
を受け取っていました。これは、コメントマップの生成がファイル全体に限定されることを意味していました。
- この関数は
- 変更後:
func NewCommentMap(fset *token.FileSet, node Node, comments []*CommentGroup) CommentMap
- 新しいシグネチャでは、
*ast.File
の代わりに汎用的なast.Node
と、コメントグループのスライス[]*CommentGroup
を受け取るようになりました。これにより、コメントマップをファイル全体だけでなく、任意のASTサブツリー(例えば、特定の関数や構造体)に対して生成できるようになり、APIの柔軟性が大幅に向上しました。また、コメントのリストを明示的に渡すことで、NewCommentMap
がどのコメントを考慮すべきかを呼び出し側が制御できるようになります。
- 新しいシグネチャでは、
- 変更前:
-
Update
メソッドの追加:func (cmap CommentMap) Update(old, new Node) Node
- このメソッドは、ASTの変更(例えば、
old
ノードをnew
ノードに置き換える場合)に対応してCommentMap
を更新するために導入されました。 old
ノードに関連付けられていたコメントグループをnew
ノードに移動させ、old
ノードのエントリをマップから削除します。これにより、ASTの構造が変更されても、コメントとコード要素の関連付けを維持することが可能になります。これは、リファクタリングツールやコード生成ツールにおいて非常に重要な機能です。
-
String
メソッドの追加:func (cmap CommentMap) String() string
- このメソッドは、
CommentMap
の内容を人間が読める形式の文字列として返すために追加されました。 - デバッグ目的で設計されており、
CommentMap
がどのノードにどのコメントをマッピングしているかを簡単に確認できます。各ノードのポインタアドレス、識別子名(*ast.Ident
の場合)またはノードの型、および関連するコメントの要約(最初の40文字程度)が出力されます。これにより、開発者はCommentMap
の動作を視覚的に確認し、予期せぬマッピングの問題を特定しやすくなります。
-
Filter
メソッドのシグネチャ変更:- 変更前:
func (cmap CommentMap) Filter(nodes ...Node) CommentMap
- 可変長引数で複数のノードを受け取っていました。
- 変更後:
func (cmap CommentMap) Filter(node Node) CommentMap
- 単一の
ast.Node
を受け取るように変更されました。これにより、特定のASTサブツリーに含まれるノードに関連するコメントのみをフィルタリングする、より明確なセマンティクスが提供されます。
- 単一の
- 変更前:
これらの変更は、go/ast
パッケージが提供するAST操作の機能を強化し、より複雑なコード変換や解析タスクに対応できるようにするための基盤を築いています。
コアとなるコードの変更箇所
このコミットの主要な変更は、src/pkg/go/ast/commentmap.go
ファイルに集中しています。
src/pkg/go/ast/commentmap.go
--- a/src/pkg/go/ast/commentmap.go
+++ b/src/pkg/go/ast/commentmap.go
@@ -5,6 +5,8 @@
package ast
import (
+ "bytes"
+ "fmt"
"go/token"
"sort"
)
@@ -123,22 +125,22 @@ func (s *nodeStack) pop(pos token.Pos) (top Node) {
}
// NewCommentMap creates a new comment map by associating comment groups
-// to nodes. The nodes are the nodes of the given AST f and the comments
-// are taken from f.Comments.
+// of the comments list with the nodes of the AST specified by node.
//
// A comment group g is associated with a node n if:
//
@@ -139,22 +140,22 @@ func (s *nodeStack) pop(pos token.Pos) (top Node) {
// trailing an assignment, the comment is associated with the entire
// assignment rather than just the last operand in the assignment.
//
-func NewCommentMap(fset *token.FileSet, f *File) CommentMap {
- if len(f.Comments) == 0 {
+func NewCommentMap(fset *token.FileSet, node Node, comments []*CommentGroup) CommentMap {
+ if len(comments) == 0 {
return nil // no comments to map
}
cmap := make(CommentMap)
// set up comment reader r
- comments := make([]*CommentGroup, len(f.Comments))
- copy(comments, f.Comments) // don't change f.Comments
- sortComments(comments)
- r := commentListReader{fset: fset, list: comments} // !r.eol() because len(comments) > 0
+ tmp := make([]*CommentGroup, len(comments))
+ copy(tmp, comments) // don't change incomming comments
+ sortComments(tmp)
+ r := commentListReader{fset: fset, list: tmp} // !r.eol() because len(comments) > 0
r.next()
// create node list in lexical order
- nodes := nodeList(f)
+ nodes := nodeList(node)
nodes = append(nodes, nil) // append sentinel
// set up iteration variables
@@ -238,20 +239,30 @@ func NewCommentMap(fset *token.FileSet, f *File) CommentMap {
return cmap
}
+// Update replaces an old node in the comment map with the new node
+// and returns the new node. Comments that were associated with the
+// old node are associated with the new node.
+//
+func (cmap CommentMap) Update(old, new Node) Node {
+ if list := cmap[old]; len(list) > 0 {
+ delete(cmap, old)
+ cmap[new] = append(cmap[new], list...)
+ }
+ return new
+}
+
// Filter returns a new comment map consisting of only those
// entries of cmap for which a corresponding node exists in
-// any of the node trees provided.
+// the AST specified by node.
//
-func (cmap CommentMap) Filter(nodes ...Node) CommentMap {
+func (cmap CommentMap) Filter(node Node) CommentMap {
umap := make(CommentMap)
- for _, n := range nodes {
- Inspect(n, func(n Node) bool {\n-\t\t\tif g := cmap[n]; len(g) > 0 {\n-\t\t\t\tumap[n] = g\n-\t\t\t}\n-\t\t\treturn true\n-\t\t})\n-\t}\n+ Inspect(node, func(n Node) bool {
+ if g := cmap[n]; len(g) > 0 {
+ umap[n] = g
+ }
+ return true
+ })
return umap
}
@@ -266,3 +277,56 @@ func (cmap CommentMap) Comments() []*CommentGroup {\n \tsortComments(list)\n \treturn list\n }\n+\n+func summary(list []*CommentGroup) string {\n+\tconst maxLen = 40\n+\tvar buf bytes.Buffer\n+\n+\t// collect comments text\n+loop:\n+\tfor _, group := range list {\n+\t\t// Note: CommentGroup.Text() does too much work for what we\n+\t\t// need and would only replace this innermost loop.\n+\t\t// Just do it explicitly.\n+\t\tfor _, comment := range group.List {\n+\t\t\tif buf.Len() >= maxLen {\n+\t\t\t\tbreak loop\n+\t\t\t}\n+\t\t\tbuf.WriteString(comment.Text)\n+\t\t}\n+\t}\n+\n+\t// truncate if too long\n+\tif buf.Len() > maxLen {\n+\t\tbuf.Truncate(maxLen - 3)\n+\t\tbuf.WriteString(\"...\")\n+\t}\n+\n+\t// replace any invisibles with blanks\n+\tbytes := buf.Bytes()\n+\tfor i, b := range bytes {\n+\t\tswitch b {\n+\t\tcase \'\\t\', \'\\n\', \'\\r\':\n+\t\t\tbytes[i] = \' \'\n+\t\t}\n+\t}\n+\n+\treturn string(bytes)\n+}\n+\n+func (cmap CommentMap) String() string {\n+\tvar buf bytes.Buffer\n+\tfmt.Fprintln(&buf, \"CommentMap {\")\n+\tfor node, comment := range cmap {\n+\t\t// print name of identifiers; print node type for other nodes\n+\t\tvar s string\n+\t\tif ident, ok := node.(*Ident); ok {\n+\t\t\ts = ident.Name\n+\t\t} else {\n+\t\t\ts = fmt.Sprintf(\"%T\", node)\n+\t\t}\n+\t\tfmt.Fprintf(&buf, \"\\t%p %20s: %s\\n\", node, s, summary(comment))\n+\t}\n+\tfmt.Fprintln(&buf, \"}\")\n+\treturn buf.String()\n+}\n```
### `src/cmd/godoc/godoc.go` および `src/cmd/godoc/main.go`
これらのファイルでは、`NewCommentMap`の呼び出し箇所が新しいシグネチャに合わせて更新されています。
```diff
--- a/src/cmd/godoc/godoc.go
+++ b/src/cmd/godoc/godoc.go
@@ -873,7 +873,7 @@ func inList(name string, list []string) bool {
//
func packageExports(fset *token.FileSet, pkg *ast.Package) {
for _, src := range pkg.Files {
- cmap := ast.NewCommentMap(fset, src)
+ cmap := ast.NewCommentMap(fset, src, src.Comments)
ast.FileExports(src)
src.Comments = cmap.Filter(src).Comments()
}
--- a/src/cmd/godoc/main.go
+++ b/src/cmd/godoc/main.go
@@ -425,7 +425,7 @@ func main() {
filter := func(s string) bool { return rx.MatchString(s) }
switch {
case info.PAst != nil:
- cmap := ast.NewCommentMap(info.FSet, info.PAst)
+ cmap := ast.NewCommentMap(info.FSet, info.PAst, info.PAst.Comments)
ast.FilterFile(info.PAst, filter)
// Special case: Don't use templates for printing
// so we only get the filtered declarations without
src/pkg/go/ast/commentmap_test.go
テストファイルもNewCommentMap
のシグネチャ変更に合わせて更新されています。
--- a/src/pkg/go/ast/commentmap_test.go
+++ b/src/pkg/go/ast/commentmap_test.go
@@ -108,7 +108,7 @@ func TestCommentMap(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- cmap := NewCommentMap(fset, f)
+ cmap := NewCommentMap(fset, f, f.Comments)
// very correct association of comments
for n, list := range cmap {
コアとなるコードの解説
NewCommentMap
の変更
- 旧:
func NewCommentMap(fset *token.FileSet, f *File) CommentMap
*ast.File
型の引数f
を受け取っていました。これは、コメントマップが常にファイル全体を対象として構築されることを意味していました。- 内部で
f.Comments
からコメントグループを取得し、そのコピーを作成していました。
- 新:
func NewCommentMap(fset *token.FileSet, node Node, comments []*CommentGroup) CommentMap
ast.Node
型の引数node
と、[]*CommentGroup
型の引数comments
を受け取るようになりました。node
はコメントマップを構築する対象となるASTのルートノードを指定します。これにより、ファイル全体だけでなく、任意のASTサブツリーに対してコメントマップを生成できるようになります。comments
は、コメントマップの構築に使用するコメントグループのリストを明示的に指定します。これにより、呼び出し側がどのコメントを考慮に入れるかを制御できます。- 内部では、渡された
comments
スライスのコピーtmp
を作成し、それをソートして使用します。これは、元のcomments
スライスを変更しないための防御的なコピーです。 nodeList(node)
を呼び出すことで、指定されたnode
以下のASTノードを走査し、コメントマップの対象となるノードのリストを生成します。
この変更により、NewCommentMap
はより汎用的なAST操作に対応できるようになり、Filter
メソッドが単一のNode
を受け取るようになったことと合わせて、APIの一貫性が向上しました。
Update
メソッドの追加
func (cmap CommentMap) Update(old, new Node) Node {
if list := cmap[old]; len(list) > 0 {
delete(cmap, old)
cmap[new] = append(cmap[new], list...)
}
return new
}
- このメソッドは、
CommentMap
のレシーバー(cmap
)に対して定義されています。 old
とnew
という2つのast.Node
型の引数を受け取ります。cmap[old]
でold
ノードに関連付けられているコメントグループのリストを取得します。- もし
old
ノードにコメントが関連付けられていれば(len(list) > 0
)、以下の処理を行います。delete(cmap, old)
:old
ノードのエントリをマップから削除します。cmap[new] = append(cmap[new], list...)
:old
ノードに関連付けられていたコメントグループをnew
ノードに関連付けます。もしnew
ノードに既にコメントが存在する場合、それらのコメントにold
ノードのコメントが追加されます。
- 最終的に
new
ノードを返します。
このメソッドは、ASTの構造が変更された際に、コメントとノードの関連付けを効率的に更新するためのものです。例えば、AST変換ツールが特定のノードを別のノードに置き換える場合、このUpdate
メソッドを使用することで、コメントマップも自動的に更新され、コメントの整合性が保たれます。
String
メソッドの追加
func (cmap CommentMap) String() string {
var buf bytes.Buffer
fmt.Fprintln(&buf, "CommentMap {")
for node, comment := range cmap {
// print name of identifiers; print node type for other nodes
var s string
if ident, ok := node.(*Ident); ok {
s = ident.Name
} else {
s = fmt.Sprintf("%T", node)
}
fmt.Fprintf(&buf, "\t%p %20s: %s\\n", node, s, summary(comment))
}
fmt.Fprintln(&buf, "}")
return buf.String()
}
- このメソッドも
CommentMap
のレシーバーに対して定義されており、string
型の値を返します。 bytes.Buffer
を使用して、CommentMap
の内容を整形された文字列として構築します。- マップ内の各ノードとそれに関連するコメントグループをイテレートします。
- 各ノードについて、それが
*ast.Ident
(識別子)であればその名前を、そうでなければノードの型名(例:*ast.FuncDecl
)を文字列s
に格納します。 fmt.Fprintf
を使用して、ノードのポインタアドレス、整形されたノード名(または型名)、およびsummary(comment)
関数によって生成されたコメントの要約を1行に出力します。summary
関数は、コメントグループのリストを受け取り、そのテキスト内容を最大40文字に切り詰めて返します。タブ、改行、キャリッジリターンなどの不可視文字はスペースに置換されます。- 最終的に、構築された文字列バッファの内容を返します。
このString
メソッドは、CommentMap
のデバッグ表現を提供します。これにより、開発者はプログラムの実行中にCommentMap
の内容を簡単に確認し、コメントが正しくノードにマッピングされているかを検証できます。
関連リンク
- Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージのドキュメント: https://pkg.go.dev/go/token - 抽象構文木 (AST) についての一般的な情報: https://ja.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E6%A7%8B%E6%96%87%E6%9C%A8
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード(特に
go/ast
パッケージ) - コミットメッセージに記載されているGo CL (Change List) のリンク:
https://golang.org/cl/6303086
(ただし、このリンクは古い形式であり、現在はアクセスできない可能性があります。GoのCLは通常、go.googlesource.com/go/+show/master/...
のような形式で参照されます。) - 一般的なプログラミングにおけるASTの概念に関する情報源