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

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

このコミットは、Go言語のパーサー(go/parserパッケージ)におけるコメントのグループ化に関する長年のバグ("day 1 bug")を修正するものです。具体的には、抽象構文木(AST)ノードに付随する「リードコメント」(Docコメント)と「行コメント」(Lineコメント)の正しい計算を保証するために、コメントグループの終了条件が厳密化されました。この修正により、コメントが意図しない形で結合される問題が解消され、go/printerパッケージにおける関連するキャッシュバグも露呈・修正されました。変更の大部分は、この修正を検証するための新しいテストケースの追加です。

コミット

commit f26d61731dd05a1b81f40117fe18630b78f4489e
Author: Robert Griesemer <gri@golang.org>
Date:   Tue May 22 10:04:34 2012 -0700

    go/parser: fix comment grouping (day 1 bug)
    
    Comment groups must end at the end of a line (or the
    next non-comment token) if the group started on a line
    with non-comment tokens.
    
    This is important for correct computation of "lead"
    and "line" comments (Doc and Comment fields in AST nodes).
    
    Without this fix, the "line" comment for F1 in the
    following example:
    
    type T struct {
         F1 int // comment1
         // comment2
         F2 int
    }
    
    is "// comment1// comment2" rather than just "// comment1".
    
    This bug was present from Day 1 but only visible when
    looking at export-filtered ASTs where only comments
    associated with AST nodes are printed, and only in rare
    cases (e.g, in the case above, if F2 where not exported,
    godoc would show "// comment2" anyway because it was
    considered part of the "line" comment for F1).
    
    The bug fix is very small (parser.go). The bulk of the
    changes are additional test cases (parser_test.go).
    
    The fix exposed a caching bug in go/printer via one of the
    existing tests, hence the changes to printer.go.
    
    As an aside, the fix removes the the need for empty lines
    before an "// Output" comment for some special cases of
    code examples (e.g.: src/pkg/strings/example_test.go, Count
    example).
    
    No impact on gofmt formatting of src, misc.
    
    Fixes #3139.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/6209080

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

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

元コミット内容

Go言語のパーサーにおいて、コメントのグループ化に関するバグを修正します(リリース当初からのバグ)。

コメントグループは、もしそのグループが非コメントトークンを含む行で始まった場合、その行の終わり(または次の非コメントトークン)で終了しなければなりません。

これは、ASTノードにおける「リードコメント」(Docフィールド)と「行コメント」(Commentフィールド)の正しい計算にとって重要です。

この修正がない場合、以下の例におけるF1の行コメントは、

type T struct {
     F1 int // comment1
     // comment2
     F2 int
}

単に// comment1であるべきところが、// comment1// comment2となっていました。

このバグはGo言語のリリース当初から存在していましたが、エクスポートフィルタリングされたAST(ASTノードに関連付けられたコメントのみが出力される場合)を見る場合にのみ、ごく稀なケースでしか顕在化しませんでした(上記の例では、もしF2がエクスポートされていなかった場合、godoc// comment2F1の行コメントの一部と見なしていたため、いずれにせよ表示されていました)。

バグ修正自体は非常に小さく(parser.go)、変更の大部分は追加のテストケース(parser_test.go)です。

この修正は、既存のテストの一つを通じてgo/printerにおけるキャッシュバグを露呈させたため、printer.goにも変更が加えられました。

余談ですが、この修正により、特定のコード例(例: src/pkg/strings/example_test.goCount例)において// Outputコメントの前に空行を置く必要がなくなりました。

gofmtによるsrcディレクトリやその他のファイルのフォーマットには影響ありません。

Issue #3139を修正します。

レビュー担当: rsc CC: golang-dev Gerrit Change-ID: https://golang.org/cl/6209080

変更の背景

このコミットの主な背景は、Go言語のパーサー(go/parser)がコメントを抽象構文木(AST)に正しく関連付けられないという、Go言語の初期バージョンから存在していたバグ("day 1 bug")の修正です。

具体的には、Goのソースコードにおいて、構造体のフィールドや関数の宣言など、特定のコード要素に付随するコメントは、その要素の「ドキュメントコメント」(Docフィールド)または「行コメント」(Commentフィールド)としてASTに格納されます。しかし、このバグのために、パーサーがコメントグループを誤って解釈し、本来は別のコード要素に属するべきコメントが、前の要素のコメントとして誤って結合されてしまう問題が発生していました。

コミットメッセージに示されている以下の例が、この問題の典型です。

type T struct {
     F1 int // comment1
     // comment2
     F2 int
}

このコードにおいて、F1の行コメントは本来// comment1のみであるべきです。しかし、バグのあるパーサーでは、// comment2F1のコメントグループの一部として誤って認識され、結果としてF1の行コメントが// comment1// comment2となってしまっていました。これは、// comment2F2のリードコメント(ドキュメントコメント)として扱われるべきであるにもかかわらず、前の行のF1のコメントに「吸い込まれて」しまっていたことを意味します。

このバグは、通常のコードのコンパイルや実行には直接的な影響を与えませんでしたが、godocのようなドキュメンテーションツールや、ASTを解析してコードの構造を理解するツールにとっては、誤った情報を提供することになり、問題でした。特に、エクスポートされていないフィールド(例のF2)が続く場合、godoc// comment2F1のコメントとして表示してしまうことがありました。

この問題は、GoのIssueトラッカーでIssue #3139として報告されており、このコミットはその問題を解決するために作成されました。修正は、コメントグループの「終了」を定義するロジックを厳密化することで行われました。

前提知識の解説

このコミットの理解には、以下のGo言語および関連ツールの概念に関する知識が役立ちます。

1. Go言語の抽象構文木 (AST: Abstract Syntax Tree)

Go言語のコンパイラや各種ツール(gofmt, go doc, go vetなど)は、Goのソースコードを直接処理するのではなく、まずそのソースコードを解析して「抽象構文木(AST)」と呼ばれるツリー構造のデータ表現に変換します。ASTは、プログラムの構造を抽象的に表現したもので、コメント、空白、括弧などの詳細な構文情報は含まれませんが、プログラムの論理的な構造(宣言、式、文など)を保持します。

  • go/parserパッケージ: Goのソースコードを解析し、ASTを生成する標準ライブラリパッケージです。このコミットの主要な変更対象です。
  • go/astパッケージ: ASTのノード構造を定義する標準ライブラリパッケージです。例えば、ast.Fileはファイル全体のASTを表し、ast.FuncDeclは関数宣言を表します。コメントもast.Commentast.CommentGroupとしてASTの一部として扱われます。
  • go/tokenパッケージ: Goの字句解析器が使用するトークン(キーワード、識別子、演算子、コメントなど)の定義を提供するパッケージです。

2. Go言語におけるコメントの種類とASTへの格納

Go言語では、コメントは単なるコードの説明だけでなく、ドキュメンテーション生成ツール(godoc)によって特別な意味を持つことがあります。ASTにおいては、コメントは主に以下の2つのカテゴリに分類され、ast.Fieldast.FuncDeclなどのASTノードのフィールドに格納されます。

  • Doc Comments (ドキュメントコメント / リードコメント):

    • 宣言(変数、定数、関数、型など)の直前に記述されるコメントで、その宣言のドキュメンテーションとして扱われます。
    • 通常、//または/* ... */形式で記述され、宣言の直前の行に連続して配置されます。
    • ASTでは、関連するノードのDocフィールド(型は*ast.CommentGroup)に格納されます。
    • 例:
      // This is a document comment for MyFunction.
      // It explains what MyFunction does.
      func MyFunction() {}
      
  • Line Comments (行コメント):

    • 宣言と同じ行の末尾に記述されるコメントです。
    • ASTでは、関連するノードのCommentフィールド(型は*ast.CommentGroup)に格納されます。
    • 例:
      var myVar int // This is a line comment for myVar.
      
  • ast.CommentGroup: 複数の連続するast.Commentをまとめたものです。パーサーは、連続するコメントを一つのグループとして認識し、ast.CommentGroupとしてASTに格納します。このコミットのバグは、この「コメントグループ」の終了条件の認識誤りに起因していました。

3. go/printerパッケージ

go/printerパッケージは、go/astパッケージで表現されたASTをGoのソースコードとして整形して出力する標準ライブラリパッケージです。gofmtツールはこのパッケージを利用しています。このコミットでは、go/parserの修正がgo/printer内の既存のキャッシュバグを露呈させたため、go/printer/nodes.goにも修正が加えられています。

4. Goのテストフレームワーク

Go言語には、標準ライブラリとして強力なテストフレームワークが組み込まれています。_test.goで終わるファイルにテストコードを記述し、go testコマンドで実行します。このコミットでは、バグ修正の大部分が新しいテストケースの追加によって構成されており、これはGo開発におけるテストの重要性を示しています。

5. IssueトラッカーとGerrit

  • Issue #3139: Go言語のバグや機能要望は、GitHubのIssueトラッカー(以前はGoogle Code)で管理されています。このコミットは、特定のIssue(#3139)を修正するものです。
  • Gerrit: Goプロジェクトは、コードレビューと変更管理にGerritを使用しています。コミットメッセージの末尾にあるhttps://golang.org/cl/6209080は、Gerrit上の変更リスト(Change List)へのリンクです。

これらの前提知識を理解することで、コミットがGo言語のツールチェインのどの部分に影響を与え、どのような問題を解決しようとしているのかを深く把握することができます。

技術的詳細

このコミットの技術的詳細は、主にgo/parserパッケージにおけるコメントグループの認識ロジックの変更と、それに伴って露呈したgo/printerパッケージのキャッシュバグの修正に集約されます。

1. go/parserにおけるコメントグループの終了条件の厳密化

バグの核心は、go/parserがコメントグループの「終了」を誤って判断していた点にあります。特に、非コメントトークン(例: F1 int)と同じ行で始まったコメント(例: // comment1)の後に、次の行にコメント(例: // comment2)が続く場合、パーサーはこれらを一つのコメントグループとして誤って結合していました。

この修正は、src/pkg/go/parser/parser.go内のconsumeCommentGroup関数のシグネチャと内部ロジックを変更することで実現されています。

  • consumeCommentGroup関数の変更:

    • 変更前: func (p *parser) consumeCommentGroup() (comments *ast.CommentGroup, endline int)
    • 変更後: func (p *parser) consumeCommentGroup(n int) (comments *ast.CommentGroup, endline int)
    • 新しい引数nが導入されました。このnは、コメントグループが終了するまでの「許容される空行の数」を制御します。
  • コメントグループの継続条件の変更:

    • 変更前: for p.tok == token.COMMENT && endline+1 >= p.file.Line(p.pos)
      • これは、「現在のトークンがコメントであり、かつ現在のコメントの行番号が、直前のコメントグループの最終行の次の行以内である限り、コメントグループを継続する」というロジックでした。この条件が緩すぎたため、意図しないコメントの結合が発生していました。
    • 変更後: for p.tok == token.COMMENT && p.file.Line(p.pos) <= endline+n
      • この新しい条件は、「現在のトークンがコメントであり、かつ現在のコメントの行番号が、直前のコメントグループの最終行からn行以内である限り、コメントグループを継続する」というものです。
      • nの値によって、コメントグループの継続の厳密さが変わります。
  • next()関数内でのconsumeCommentGroupの呼び出し:

    • next()関数は、パーサーが次のトークンを読み込む際に、コメントを処理する主要なロジックを含んでいます。
    • 行コメントの処理: 宣言と同じ行にあるコメント(行コメント)を処理する際には、consumeCommentGroup(0)が呼び出されます。n=0は、コメントグループが同じ行で終了することを意味し、次の行にコメントがあっても別のグループとして扱われます。これにより、F1 int // comment1 // comment2の例で// comment2F1の行コメントに結合されるのを防ぎます。
    • リードコメントの処理: 宣言の前に複数行にわたって記述されるコメント(リードコメント)を処理する際には、consumeCommentGroup(1)が呼び出されます。n=1は、コメントグループが最大で1行の空行を挟んで継続できることを意味します。これは、Goの慣習として、リードコメントが複数行にわたる場合や、間に空行を挟む場合があるためです。

この変更により、パーサーはコメントグループの境界をより正確に識別できるようになり、ASTのDocおよびCommentフィールドに正しいコメントが関連付けられるようになりました。

2. go/printerにおけるキャッシュバグの修正

go/parserの修正は、src/pkg/go/printer/nodes.go内のsetComment関数における既存のキャッシュバグを露呈させました。このバグは、go/printerがコメントを処理する際の内部状態管理に関するものでした。

  • setComment関数の変更:
    • setCommentは、ASTノードにコメントを設定する際に使用される関数です。
    • 変更前は、p.comments(保留中のコメントリスト)に複数のコメントが残っている場合に、予期せぬ動作をする可能性がありました。
    • 修正では、p.comments = p.comments[0:1]という行が追加され、setCommentが呼び出された際に、保留中のコメントリストが最大で1つのコメントのみを保持するように強制されます。これにより、setCommentが常にクリーンな状態で動作することが保証されます。
    • また、if p.commentOffset == infinity { p.nextComment() }という条件が追加され、p.commentOffsetが無限大(つまり、コメントキャッシュが空)の場合にのみp.nextComment()を呼び出すことで、既存のコメントキャッシュを上書きしないようにしています。これは、行コメントの直後にリードコメントが続くような特殊なケースで、コメントが正しく処理されるようにするためです。

3. テストケースの追加

このコミットの変更の大部分は、src/pkg/go/parser/parser_test.goに追加された広範なテストケースです。これは、Go言語のプロジェクトにおいて、バグ修正や新機能追加の際に、その変更が正しく機能し、既存の機能に悪影響を与えないことを保証するために、徹底的なテストが重視されていることを示しています。

  • TestCommentGroups: さまざまなコメントの配置パターンに対して、go/parserがコメントグループを正しく識別し、ast.File.Commentsに格納するかどうかを検証します。
  • TestLeadAndLineComments: 構造体のフィールドに対するリードコメント(Doc)と行コメント(Comment)が、ParseCommentsオプションを有効にしてパースされたASTで正しく抽出されるかを検証します。特に、コミットメッセージで言及されたバグのシナリオを直接テストしています。

これらのテストは、修正されたパーサーロジックが意図した通りに動作することを保証する上で不可欠です。

4. src/pkg/strings/example_test.goの変更

このファイルでは、ExampleCount関数の// Output:コメントの前にあった空行が削除されました。これは、go/parserの修正によって、もはやこの空行が不要になったことを示しています。以前は、パーサーがコメントグループを誤って解釈するのを避けるために、このような「ハック」が必要だった可能性があります。この変更は、修正がコードの可読性や慣習にも良い影響を与えたことを示唆しています。

これらの技術的詳細は、このコミットがGo言語のツールチェインの基盤部分に深く関わるものであり、その正確性が言語のドキュメンテーションやコード解析の品質に直接影響を与えることを示しています。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. src/pkg/go/parser/parser.go:

    • consumeCommentGroup関数のシグネチャが変更され、n intという新しい引数が追加されました。
      -func (p *parser) consumeCommentGroup() (comments *ast.CommentGroup, endline int) {
      +func (p *parser) consumeCommentGroup(n int) (comments *ast.CommentGroup, endline int) {
      
    • consumeCommentGroup内のコメントグループの継続条件が変更されました。
      -	for p.tok == token.COMMENT && endline+1 >= p.file.Line(p.pos) {
      +	for p.tok == token.COMMENT && p.file.Line(p.pos) <= endline+n {
      
    • next関数内でconsumeCommentGroupの呼び出し箇所が変更され、新しい引数nが渡されるようになりました。
      -			comment, endline = p.consumeCommentGroup()
      +			comment, endline = p.consumeCommentGroup(0) // for line comments
      
      -		comment, endline = p.consumeCommentGroup()
      +		comment, endline = p.consumeCommentGroup(1) // for lead comments
      
  2. src/pkg/go/parser/parser_test.go:

    • TestCommentGroups関数が追加されました。これは、さまざまなコメントの配置パターンに対するコメントグループの正しいパースを検証します。
    • TestLeadAndLineComments関数が追加されました。これは、構造体のフィールドに対するリードコメントと行コメントが正しく抽出されるかを検証します。
    • 既存のテスト関数(TestParse, TestParseExpr, TestColonEqualsScope, TestVarScope)内で、エラー報告にt.Errorfの代わりにt.Fatalfが使用されるように変更されました。これは、テストが失敗した場合に即座にテストを終了させることで、後続のテストの誤った実行を防ぐためです。
  3. src/pkg/go/printer/nodes.go:

    • setComment関数内で、コメントキャッシュの処理ロジックが変更されました。
      -	p.comments = p.comments[0:1]
      -	// in debug mode, report error
      -	p.internalError("setComment found pending comments")
      +	// should never happen - handle gracefully and flush
      +	// all comments up to g, ignore anything after that
      +	p.flush(p.posFor(g.List[0].Pos()), token.ILLEGAL)
      +	p.comments = p.comments[0:1] // Ensure only one comment group is pending
      +	// in debug mode, report error
      +	p.internalError("setComment found pending comments")
       }
       p.comments[0] = g
       p.cindex = 0
      -	p.nextComment() // get comment ready for use
      +	// don't overwrite any pending comment in the p.comment cache
      +	// (there may be a pending comment when a line comment is
      +	// immediately followed by a lead comment with no other
      +	// tokens inbetween)
      +	if p.commentOffset == infinity {
      +		p.nextComment() // get comment ready for use
      +	}
      
  4. src/pkg/strings/example_test.go:

    • ExampleCount関数内の// Output:コメントの前の空行が削除されました。
      -
       	// Output:
       	// 3
       	// 5
      

これらの変更は、Go言語のパーサーとプリンターのコアロジックに直接影響を与え、コメントの処理方法を根本的に改善しています。

コアとなるコードの解説

src/pkg/go/parser/parser.goの変更

このファイルの変更は、Goパーサーがコメントグループをどのように認識し、終了させるかという核心的なロジックを修正しています。

  1. consumeCommentGroup(n int)の導入:

    • 以前のconsumeCommentGroup関数は引数を持たず、コメントグループの継続条件が固定されていました。
    • 新しいn int引数は、コメントグループが継続できる最大行数を制御します。
      • n=0の場合: コメントグループは現在の行で終了します。次の行にコメントがあっても、それは別のコメントグループとして扱われます。これは主に行コメント(コードと同じ行にあるコメント)の処理に適用されます。
      • n=1の場合: コメントグループは、最大で1行の空行を挟んで次の行に継続できます。これは主にリードコメント(宣言の前に複数行にわたって記述されるコメント)の処理に適用されます。リードコメントは、その性質上、複数行にわたることが多く、また慣習的に間に空行を挟むことも許容されるためです。
  2. コメント継続条件の変更:

    • for p.tok == token.COMMENT && p.file.Line(p.pos) <= endline+n
    • この新しい条件は、現在のトークンがコメントであり、かつそのコメントの行番号が、直前のコメントグループの最終行(endline)からn行以内である場合にのみ、コメントグループを継続することを意味します。
    • これにより、コミットメッセージの例で示されたような、F1の行コメントが// comment2を誤って取り込んでしまう問題が解決されます。F1の行コメントを処理する際にはn=0が使われるため、// comment1の次の行にある// comment2は別のコメントグループとして認識され、F1の行コメントには含まれなくなります。

src/pkg/go/printer/nodes.goの変更

このファイルの変更は、go/parserの修正によって露呈したgo/printerの内部キャッシュバグを修正するものです。

  1. setComment関数内のp.commentsのクリア:

    • p.comments = p.comments[0:1]という行が追加されました。これは、setCommentが呼び出された際に、p.commentsスライス(プリンターが処理を待っているコメントグループのリスト)が、最大で1つのコメントグループのみを保持するように強制します。
    • これにより、setCommentが常に予測可能な状態(つまり、処理すべきコメントが最大1つしかない状態)で動作することが保証され、以前の処理で残っていた古いコメントグループが誤って再利用されるようなキャッシュの問題が解消されます。
  2. p.nextComment()の条件付き呼び出し:

    • if p.commentOffset == infinity { p.nextComment() }という条件が追加されました。
    • p.commentOffsetは、プリンターが現在処理しているコメントのオフセットを示します。infinityは、コメントキャッシュが空であることを意味します。
    • この変更により、p.commentOffsetinfinityの場合にのみp.nextComment()が呼び出され、新しいコメントがキャッシュにロードされます。
    • これは、行コメントの直後にリードコメントが続くような特殊なケースで重要です。以前は、行コメントが処理された後もp.commentキャッシュにデータが残っている可能性があり、その直後にリードコメントが来ると、キャッシュが上書きされてしまう可能性がありました。この修正により、キャッシュが空の場合にのみ新しいコメントがロードされるため、このような競合が回避されます。

これらの変更は、Go言語のツールチェインがソースコードのコメントをより正確に解釈し、ASTに反映させるための重要な改善であり、godocなどのドキュメンテーションツールや、コード分析ツールの信頼性を向上させます。

関連リンク

参考にした情報源リンク