[インデックス 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
は// comment2
をF1
の行コメントの一部と見なしていたため、いずれにせよ表示されていました)。
バグ修正自体は非常に小さく(parser.go
)、変更の大部分は追加のテストケース(parser_test.go
)です。
この修正は、既存のテストの一つを通じてgo/printer
におけるキャッシュバグを露呈させたため、printer.go
にも変更が加えられました。
余談ですが、この修正により、特定のコード例(例: src/pkg/strings/example_test.go
のCount
例)において// 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
のみであるべきです。しかし、バグのあるパーサーでは、// comment2
がF1
のコメントグループの一部として誤って認識され、結果としてF1
の行コメントが// comment1// comment2
となってしまっていました。これは、// comment2
がF2
のリードコメント(ドキュメントコメント)として扱われるべきであるにもかかわらず、前の行のF1
のコメントに「吸い込まれて」しまっていたことを意味します。
このバグは、通常のコードのコンパイルや実行には直接的な影響を与えませんでしたが、godoc
のようなドキュメンテーションツールや、ASTを解析してコードの構造を理解するツールにとっては、誤った情報を提供することになり、問題でした。特に、エクスポートされていないフィールド(例のF2
)が続く場合、godoc
は// comment2
をF1
のコメントとして表示してしまうことがありました。
この問題は、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.Comment
やast.CommentGroup
としてASTの一部として扱われます。go/token
パッケージ: Goの字句解析器が使用するトークン(キーワード、識別子、演算子、コメントなど)の定義を提供するパッケージです。
2. Go言語におけるコメントの種類とASTへの格納
Go言語では、コメントは単なるコードの説明だけでなく、ドキュメンテーション生成ツール(godoc
)によって特別な意味を持つことがあります。ASTにおいては、コメントは主に以下の2つのカテゴリに分類され、ast.Field
やast.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
の例で// comment2
がF1
の行コメントに結合されるのを防ぎます。 - リードコメントの処理: 宣言の前に複数行にわたって記述されるコメント(リードコメント)を処理する際には、
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言語のツールチェインの基盤部分に深く関わるものであり、その正確性が言語のドキュメンテーションやコード解析の品質に直接影響を与えることを示しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。
-
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
-
src/pkg/go/parser/parser_test.go
:TestCommentGroups
関数が追加されました。これは、さまざまなコメントの配置パターンに対するコメントグループの正しいパースを検証します。TestLeadAndLineComments
関数が追加されました。これは、構造体のフィールドに対するリードコメントと行コメントが正しく抽出されるかを検証します。- 既存のテスト関数(
TestParse
,TestParseExpr
,TestColonEqualsScope
,TestVarScope
)内で、エラー報告にt.Errorf
の代わりにt.Fatalf
が使用されるように変更されました。これは、テストが失敗した場合に即座にテストを終了させることで、後続のテストの誤った実行を防ぐためです。
-
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 + }
-
src/pkg/strings/example_test.go
:ExampleCount
関数内の// Output:
コメントの前の空行が削除されました。- // Output: // 3 // 5
これらの変更は、Go言語のパーサーとプリンターのコアロジックに直接影響を与え、コメントの処理方法を根本的に改善しています。
コアとなるコードの解説
src/pkg/go/parser/parser.go
の変更
このファイルの変更は、Goパーサーがコメントグループをどのように認識し、終了させるかという核心的なロジックを修正しています。
-
consumeCommentGroup(n int)
の導入:- 以前の
consumeCommentGroup
関数は引数を持たず、コメントグループの継続条件が固定されていました。 - 新しい
n int
引数は、コメントグループが継続できる最大行数を制御します。n=0
の場合: コメントグループは現在の行で終了します。次の行にコメントがあっても、それは別のコメントグループとして扱われます。これは主に行コメント(コードと同じ行にあるコメント)の処理に適用されます。n=1
の場合: コメントグループは、最大で1行の空行を挟んで次の行に継続できます。これは主にリードコメント(宣言の前に複数行にわたって記述されるコメント)の処理に適用されます。リードコメントは、その性質上、複数行にわたることが多く、また慣習的に間に空行を挟むことも許容されるためです。
- 以前の
-
コメント継続条件の変更:
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
の内部キャッシュバグを修正するものです。
-
setComment
関数内のp.comments
のクリア:p.comments = p.comments[0:1]
という行が追加されました。これは、setComment
が呼び出された際に、p.comments
スライス(プリンターが処理を待っているコメントグループのリスト)が、最大で1つのコメントグループのみを保持するように強制します。- これにより、
setComment
が常に予測可能な状態(つまり、処理すべきコメントが最大1つしかない状態)で動作することが保証され、以前の処理で残っていた古いコメントグループが誤って再利用されるようなキャッシュの問題が解消されます。
-
p.nextComment()
の条件付き呼び出し:if p.commentOffset == infinity { p.nextComment() }
という条件が追加されました。p.commentOffset
は、プリンターが現在処理しているコメントのオフセットを示します。infinity
は、コメントキャッシュが空であることを意味します。- この変更により、
p.commentOffset
がinfinity
の場合にのみp.nextComment()
が呼び出され、新しいコメントがキャッシュにロードされます。 - これは、行コメントの直後にリードコメントが続くような特殊なケースで重要です。以前は、行コメントが処理された後も
p.comment
キャッシュにデータが残っている可能性があり、その直後にリードコメントが来ると、キャッシュが上書きされてしまう可能性がありました。この修正により、キャッシュが空の場合にのみ新しいコメントがロードされるため、このような競合が回避されます。
これらの変更は、Go言語のツールチェインがソースコードのコメントをより正確に解釈し、ASTに反映させるための重要な改善であり、godoc
などのドキュメンテーションツールや、コード分析ツールの信頼性を向上させます。
関連リンク
- Go Issue #3139: https://github.com/golang/go/issues/3139
- Gerrit Change-ID: https://golang.org/cl/6209080
参考にした情報源リンク
- Go言語のAST (go/astパッケージ):
- Go言語のパーサー (go/parserパッケージ):
- Go言語のプリンター (go/printerパッケージ):
- Go言語のトークン (go/tokenパッケージ):
- Effective Go - Comments:
- Go Code Review Comments - Commentary:
- GoDocの仕組みとコメントの書き方:
- GoDocの公式ドキュメントや、Go言語のドキュメンテーションに関する一般的な解説記事。 (具体的なURLは変動する可能性があるため、一般的な情報源として記載)
- Go言語のテスト:
- Gerrit Code Review:
- https://www.gerritcodereview.com/
- GoプロジェクトにおけるGerritの利用に関する情報。 (具体的なURLは変動する可能性があるため、一般的な情報源として記載)