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

[インデックス 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言語のツール(goimportsgofmtなど)は、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.GenDeclvar, const, typeなどの一般的な宣言を表します。

ast.Commentast.CommentGroup

ast.Commentは単一のコメント(例: // comment または /* comment */)を表し、ast.CommentGroupは連続するコメントのブロック(例: // comment1\n// comment2)を表します。ast.Fileには、ファイル全体から抽出されたすべてのコメントグループのリストが含まれています。

ast.CommentMap

ast.CommentMapは、ASTノードとコメントグループの関連付けを管理するためのデータ構造です。parser.ParseCommentsオプションを使用してソースコードを解析すると、ast.FileCommentsフィールドにすべてのコメントグループが格納されます。しかし、これらのコメントグループはASTノードと直接的な関連付けを持っていません。ast.NewCommentMap関数は、FileSetast.File、およびコメントグループのリストを受け取り、ASTノードとコメントグループの間の「最も近い」関連付けを推測してCommentMapを構築します。これにより、特定のASTノードが削除されたり移動されたりした場合でも、そのノードに関連するコメントを識別し、適切に処理することが可能になります。

CommentMapの主な機能は以下の通りです。

  • 関連付けの構築: ASTノードとコメントグループの間の論理的な関連付けを確立します。
  • フィルタリング: ASTの変更後、もはや関連性のないコメントをFilterメソッドを使って除去し、新しいコメントリストを生成します。

go/formatパッケージ

go/formatパッケージは、Goのソースコードを標準的なGoのフォーマット規則に従って整形するために使用されます。ASTを操作した後、format.Node関数を使って整形されたソースコードをバイト列として出力することができます。

技術的詳細

このコミットで追加されたExampleCommentMap関数は、以下のステップでast.CommentMapの利用方法をデモンストレーションしています。

  1. ソースコードの準備: 操作対象となるGoのソースコード文字列を定義します。このソースコードには、パッケージコメント、定数宣言、変数宣言、関数宣言が含まれ、それぞれにコメントが付与されています。特に、削除対象となる変数宣言var foo = helloには、その前後にコメントが関連付けられています。

  2. ASTの生成: go/parser.ParseFile関数を使用して、準備したソースコードからASTを生成します。この際、parser.ParseCommentsオプションを渡すことで、コメントもASTの一部として解析され、ast.FileCommentsフィールドに格納されます。

  3. ast.CommentMapの作成: ast.NewCommentMap関数を呼び出し、生成されたast.FileからCommentMapを作成します。このCommentMapは、ASTノードとコメントグループの間の関連付けを内部的に保持します。

  4. ASTの操作(変数宣言の削除): removeFirstVarDeclヘルパー関数を呼び出し、ASTの宣言リスト(f.Decls)から最初の変数宣言を削除します。この関数は、ast.Declのリストを走査し、ast.GenDecl型でtoken.VARトークンを持つ宣言(つまり変数宣言)を見つけて削除します。

  5. コメントのフィルタリング: ASTの変更後、CommentMapFilterメソッドを使用して、もはやASTのどのノードとも関連付けられていないコメントをフィルタリングします。cmap.Filter(f).Comments()は、変更後のAST (f) に基づいて、関連性のないコメントを除外し、新しいコメントグループのリストを返します。この新しいリストがf.Commentsに再割り当てされます。これにより、削除された変数宣言に関連するコメントがASTから適切に除去されます。

  6. 整形された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の利用方法をエンドツーエンドで示しています。

  1. src文字列: 操作対象のGoソースコードを定義しています。このコードには、パッケージコメント、定数宣言、変数宣言、関数宣言が含まれ、それぞれにコメントが付与されています。特に、var foo = hello // line comment 2という行が削除対象となります。

  2. ASTのパース:

    fset := token.NewFileSet()
    f, err := parser.ParseFile(fset, "src.go", src, parser.ParseComments)
    

    parser.ParseFileは、src文字列からASTを構築します。parser.ParseCommentsフラグにより、コメントも解析され、f.Commentsに格納されます。fsetは、ソースコード内の位置情報を管理するために必要です。

  3. CommentMapの作成:

    cmap := ast.NewCommentMap(fset, f, f.Comments)
    

    ここで、ast.NewCommentMapが呼び出され、fset、パースされたast.File (f)、およびファイルから抽出されたコメントのリスト (f.Comments) を基にCommentMapが作成されます。このマップは、ASTノードとコメントの関連付けを内部的に構築します。

  4. 変数宣言の削除:

    f.Decls = removeFirstVarDecl(f.Decls)
    

    f.Declsは、ファイル内のトップレベル宣言のリストです。removeFirstVarDecl関数を呼び出すことで、このリストから最初の変数宣言が削除されます。

  5. コメントのフィルタリング:

    f.Comments = cmap.Filter(f).Comments()
    

    これがCommentMapの最も重要な利用箇所です。cmap.Filter(f)は、変更後のAST (f) を受け取り、もはやASTのどのノードとも関連付けられていないコメント(この場合は削除された変数宣言に関連するコメント)を除外した新しいCommentMapを返します。.Comments()メソッドはその新しいCommentMapからコメントグループのリストを取得し、それをf.Commentsに再割り当てすることで、ASTから不要なコメントを削除します。

  6. 整形と出力:

    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操作に関する一般的な情報源(例: Go AST Examples, Go AST Walk)
  • go/astパッケージのソースコード
  • Go言語のコードフォーマッタgofmtの内部実装(AST操作の典型例)
  • Go言語の静的解析ツールに関する記事やチュートリアル