Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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の柔軟性を高め、FilterCommentsといった他のメソッドとの一貫性を持たせることを目指しています。

また、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変更に集約されます。

  1. 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がどのコメントを考慮すべきかを呼び出し側が制御できるようになります。
  2. Updateメソッドの追加:

    • func (cmap CommentMap) Update(old, new Node) Node
    • このメソッドは、ASTの変更(例えば、oldノードをnewノードに置き換える場合)に対応してCommentMapを更新するために導入されました。
    • oldノードに関連付けられていたコメントグループをnewノードに移動させ、oldノードのエントリをマップから削除します。これにより、ASTの構造が変更されても、コメントとコード要素の関連付けを維持することが可能になります。これは、リファクタリングツールやコード生成ツールにおいて非常に重要な機能です。
  3. Stringメソッドの追加:

    • func (cmap CommentMap) String() string
    • このメソッドは、CommentMapの内容を人間が読める形式の文字列として返すために追加されました。
    • デバッグ目的で設計されており、CommentMapがどのノードにどのコメントをマッピングしているかを簡単に確認できます。各ノードのポインタアドレス、識別子名(*ast.Identの場合)またはノードの型、および関連するコメントの要約(最初の40文字程度)が出力されます。これにより、開発者はCommentMapの動作を視覚的に確認し、予期せぬマッピングの問題を特定しやすくなります。
  4. 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)に対して定義されています。
  • oldnewという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言語のソースコード(特にgo/astパッケージ)
  • コミットメッセージに記載されているGo CL (Change List) のリンク: https://golang.org/cl/6303086 (ただし、このリンクは古い形式であり、現在はアクセスできない可能性があります。GoのCLは通常、go.googlesource.com/go/+show/master/...のような形式で参照されます。)
  • 一般的なプログラミングにおけるASTの概念に関する情報源