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

[インデックス 13344] ファイルの概要

このコミットは、Go言語の抽象構文木(AST)を扱う go/ast パッケージに、コメントとASTノードの関連付けを管理する CommentMap の実装を追加するものです。これにより、ASTが操作された際にコメントリストが正しく更新されるようになります。

コミット

commit f5f23e075e179ddbb518683e0762f27cd59018dd
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Jun 13 13:32:29 2012 -0700

    go/ast: comment map implementation
    
    A comment map associates comments with AST nodes
    and permits correct updating of the AST's comment
    list when the AST is manipulated.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/6281044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/f5f23e075e179ddbb518683e0762f27cd59018dd

元コミット内容

go/ast: comment map implementation

A comment map associates comments with AST nodes
and permits correct updating of the AST's comment
list when the AST is manipulated.

変更の背景

Go言語のコンパイラやツールは、ソースコードを解析して抽象構文木(AST)を生成します。ASTはプログラムの構造を木構造で表現したもので、コンパイラの最適化、コード生成、静的解析ツール、リファクタリングツールなど、様々な用途で利用されます。

しかし、ASTは通常、コメント情報を含みません。コメントはプログラムの実行には影響しないため、解析の段階で破棄されることが一般的です。しかし、コードフォーマッタ(gofmtなど)やリファクタリングツールのように、ソースコードを再生成したり、ASTを操作したりするツールにとっては、コメントの位置や内容を保持し、ASTの変更に合わせて適切にコメントも移動・更新されることが非常に重要になります。

このコミット以前は、ASTを操作する際にコメントの整合性を保つのが困難でした。ASTノードが追加、削除、移動されると、それに関連するコメントが孤立したり、誤った位置に配置されたりする可能性がありました。この問題に対処するため、ASTノードとコメントグループを関連付けるメカニズム、すなわち「コメントマップ」が必要とされました。これにより、ASTの操作とコメントの管理を同期させ、より堅牢なツール開発を可能にすることが変更の背景にあります。

前提知識の解説

抽象構文木 (Abstract Syntax Tree, AST)

ASTは、プログラミング言語のソースコードを抽象的な構文構造で表現した木構造のデータ構造です。コンパイラのフロントエンド(字句解析、構文解析)によって生成されます。ASTは、ソースコードの具体的な構文(括弧やセミコロンなど)を抽象化し、プログラムの意味的な構造に焦点を当てます。

例えば、a = b + c; というコードは、以下のようなASTで表現されることがあります。

  Assignment
  /        \
Identifier  Binary Expression
(a)         /    |    \
         Identifier  Operator  Identifier
         (b)       (+)       (c)

Go言語では、標準ライブラリの go/ast パッケージがASTの定義と操作を提供しています。

go/ast パッケージ

go/ast パッケージは、Goプログラムのソースコードの抽象構文木を表現するための型と関数を提供します。主要な型には以下のようなものがあります。

  • Node: ASTのすべてのノードが実装するインターフェース。
  • File: 単一のGoソースファイルを表すASTのルートノード。パッケージ宣言、インポート、トップレベルの宣言(関数、変数、型など)を含みます。
  • Decl: 宣言(変数宣言、関数宣言、型宣言など)を表すインターフェース。
  • Stmt: 文(代入文、if文、for文など)を表すインターフェース。
  • Expr: 式(リテラル、識別子、演算など)を表すインターフェース。
  • CommentGroup: 複数のコメント(// comment/* comment */)をまとめたグループ。

go/ast パッケージは、ASTの走査(ast.Inspect)や、ASTのノードの作成・変更を行うための機能を提供します。

go/token パッケージ

go/token パッケージは、Goソースコード内の位置情報(ファイル、行番号、列番号、オフセット)を扱うための型と関数を提供します。token.Pos はソースコード内の特定のオフセットを表し、token.FileSet は複数のファイルにわたる位置情報を管理します。ASTノードは通常、そのノードがソースコードのどこからどこまでを占めるかを示す Pos()End() メソッドを持ち、これらは token.Pos 型を返します。

コメントとASTの関連付けの課題

コメントは、プログラムの実行には影響しないため、ASTの直接的な一部として扱われないことが多いです。しかし、人間がコードを理解する上で非常に重要であり、コードの意図や設計を説明します。ASTを操作するツールがコメントを無視すると、生成されるコードが元のコメントを失ったり、コメントが不適切な位置に配置されたりする問題が発生します。

例えば、ある変数宣言に付随するコメントがあったとして、その変数宣言が別のスコープに移動された場合、コメントも一緒に移動されるべきです。コメントマップは、このようなASTの構造変更に伴うコメントの適切な管理を可能にするためのデータ構造です。

技術的詳細

このコミットで導入される CommentMap は、go/ast パッケージ内でASTノードとコメントグループを関連付けるための新しいデータ構造です。

CommentMap の定義

CommentMapmap[Node][]*CommentGroup として定義されています。これは、ASTの任意の Node をキーとして、そのノードに関連付けられた CommentGroup のスライスを値として保持します。

NewCommentMap 関数

NewCommentMap 関数は、*token.FileSet*ast.File を引数に取り、新しい CommentMap を生成します。この関数が CommentMap のコアロジックを実装しており、コメントグループを適切なASTノードに関連付けるための複雑なルールを含んでいます。

コメントグループ g がノード n に関連付けられる条件は以下の通りです。

  1. gn の終了行と同じ行で始まる場合。
  2. gn の直後の行で始まり、かつ g の後に次のノードの前に少なくとも1つの空行がある場合。
  3. gn の前に始まり、かつ上記のルールで前のノードに関連付けられていない場合。

NewCommentMap は、コメントグループを「可能な限り大きな」ノードに関連付けようとします。例えば、代入文の末尾にある行コメントは、代入文全体に関連付けられます。

この関連付けロジックは、以下のステップで実行されます。

  1. コメントのソート: f.Comments からコメントグループのコピーを作成し、sortComments 関数(byPos 型を使用してコメントグループの開始位置でソート)でソースコード順にソートします。
  2. ノードリストの作成: nodeList 関数(ast.Inspect を使用してASTを走査し、コメントグループ以外のすべてのノードをソースコード順に収集)で、AST f 内のすべてのノードをソースコード順にリスト化します。末尾にセンチネル(nil ノード)を追加し、すべてのコメントが処理されるようにします。
  3. コメントとノードの関連付け:
    • commentListReader を使用してコメントグループを順次読み込みます。
    • nodeStack を使用して、現在処理中のコメントを包含する可能性のある「重要な」ノード(File, Field, Decl, Spec, Stmt など)のスタックを管理します。nodeStack は、ノードの開始位置に基づいてスタックを操作し、現在のコメントの範囲外に出たノードをポップします。
    • 各コメントグループについて、以下の優先順位で関連付けを行うノードを決定します。
      • ノードグループへの関連付け: まず、現在のコメントを包含する可能性のある最も最近の「ノードグループ」(pg)に対して関連付けを試みます。これは、コメントがノードグループの終了行と同じ行にあるか、または直後の行にあり、かつその後に空行がある場合に適用されます。
      • 直前のノードへの関連付け: ノードグループに関連付けられなかった場合、直前のノード(p)に対して同様のルールで関連付けを試みます。
      • 現在のノードへの関連付け: 上記のいずれにも当てはまらない場合、現在のノード(q)に関連付けられます。

この複雑なロジックにより、コメントがその意味的な文脈に最も近いASTノードに正確に関連付けられるようになります。

その他のメソッド

  • CommentMap.addComment(n Node, c *CommentGroup): 指定されたノード n にコメントグループ c を追加します。
  • CommentMap.Filter(nodes ...Node) CommentMap: 既存の CommentMap から、引数で与えられたノードツリー内に存在するノードに関連付けられたコメントのみを含む新しい CommentMap を返します。これは、ASTの一部を抽出したり、特定のサブツリーに関連するコメントのみを扱いたい場合に有用です。
  • CommentMap.Comments() []*CommentGroup: CommentMap 内のすべてのコメントグループをソースコード順にソートして返します。

テスト (commentmap_test.go)

commentmap_test.go は、NewCommentMap の動作を検証するための包括的なテストケースを含んでいます。src という定数に実際のGoソースコードの文字列が定義されており、res というマップには、各ASTノード(行番号と型で識別)に期待されるコメントテキストが定義されています。

TestCommentMap 関数は、以下のことを行います。

  1. parser.ParseFile を使用して src からASTをパースし、コメントも同時にパースします。
  2. NewCommentMap を呼び出して CommentMap を生成します。
  3. 生成された CommentMap をイテレートし、各ノードに関連付けられたコメントが res マップの期待値と一致するかを検証します。
  4. CommentMap 内のコメントグループの総数が、元のファイルからパースされたコメントグループの総数と一致するかを検証し、コメントが失われていないことを確認します。

このテストは、CommentMap の関連付けロジックが複雑なGoコードに対しても正しく機能することを確認するための重要な役割を果たします。

コアとなるコードの変更箇所

このコミットでは、以下の2つの新しいファイルが追加されています。

  • src/pkg/go/ast/commentmap.go: CommentMap の主要な実装が含まれています。
  • src/pkg/go/ast/commentmap_test.go: CommentMap の機能と正確性を検証するためのテストコードが含まれています。

これらのファイルは、既存の go/ast パッケージに新しい機能を追加するものであり、既存のコードの変更は含まれていません。

コアとなるコードの解説

src/pkg/go/ast/commentmap.go

このファイルには、CommentMap の型定義と、その操作に関連する関数が実装されています。

  • byPos: []*CommentGroup のエイリアスで、sort.Interface を実装し、コメントグループをその開始位置 (Pos()) でソートするために使用されます。
  • sortComments(list []*CommentGroup): コメントグループのスライスをソースコード順にソートします。
  • CommentMap: map[Node][]*CommentGroup として定義され、ASTノードとコメントグループの関連付けを保持します。
  • addComment(n Node, c *CommentGroup): CommentMap にノードとコメントグループの関連付けを追加するヘルパーメソッド。
  • byInterval: []Node のエイリアスで、sort.Interface を実装し、ノードをその開始位置 (Pos()) でソートし、開始位置が同じ場合は終了位置 (End()) で逆順にソートするために使用されます。これは、nodeList でノードをソートする際に考慮される可能性がありますが、現在の実装では Inspect の走査順序に依存しています。
  • nodeList(n Node) []Node: 与えられたAST n から、コメントグループ以外のすべてのノードをソースコード順に収集してリストとして返します。
  • commentListReader: コメントグループのリストを効率的にイテレートするためのヘルパー構造体。
  • nodeStack: ネストされたノードを追跡するためのスタック実装。コメントの関連付けロジックで、コメントを包含する「重要な」ノードを特定するために使用されます。
  • NewCommentMap(fset *token.FileSet, f *File) CommentMap: このファイルで最も重要な関数であり、コメントグループをASTノードにマッピングする主要なロジックを含んでいます。前述の「技術的詳細」セクションで説明した複雑な関連付けルールがここで実装されています。
  • Filter(nodes ...Node) CommentMap: 指定されたノードツリー内に存在するノードに関連付けられたコメントのみを含む新しい CommentMap を返します。
  • Comments() []*CommentGroup: CommentMap 内のすべてのコメントグループをソースコード順にソートして返します。

src/pkg/go/ast/commentmap_test.go

このファイルは、commentmap.go で実装された CommentMap の機能が正しく動作するかを検証するための単体テストを提供します。

  • src 定数: テストに使用されるGoソースコードの文字列。様々な種類のコメントとAST構造を含んでいます。
  • res 変数: src のASTをパースし、NewCommentMap で生成された CommentMap が期待するノードとコメントの関連付けを定義するマップ。キーは "行番号: *ast.ノード型" の形式で、値は関連付けられるコメントのテキストです。
  • ctext(list []*CommentGroup) string: CommentGroup のスライスから、それらのコメントテキストを連結した文字列を生成するヘルパー関数。
  • TestCommentMap(t *testing.T): メインのテスト関数。
    1. go/parser を使用して src をパースし、ASTとコメント情報を取得します。
    2. NewCommentMap を呼び出して CommentMap を生成します。
    3. 生成された CommentMap をイテレートし、各ノードと関連付けられたコメントが res マップの期待値と一致するかを検証します。
    4. cmap.Comments() の結果と元のコメントの数を比較し、すべてのコメントがマップに含まれていることを確認します。
    5. genMap フラグが true の場合、テスト結果を元に res マップを再生成するためのデバッグコードも含まれています。

このテストファイルは、CommentMap のロジックが意図した通りに機能し、Goソースコードの複雑なコメント配置にも対応できることを保証します。

関連リンク

参考にした情報源リンク