[インデックス 12293] ファイルの概要
このコミットは、Go言語のコードフォーマッタであるgofmt
およびその基盤となるgo/printer
パッケージにおけるコメント配置の改善を目的としています。特に、複数行にわたるコード構造の後に続くコメントのインデントと配置のロジックが修正され、より自然で読みやすいフォーマットが実現されています。
コミット
- Author: Robert Griesemer gri@golang.org
- Date: Wed Feb 29 17:25:15 2012 -0800
- Commit Message:
go/printer, gofmt: improved comment placement Applied gofmt -w src misc (no changes). Fixes #3147. R=r, rsc CC=golang-dev https://golang.org/cl/5710046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fd5718ce82c9dec47ad6243acf3b3cc237df4efa
元コミット内容
go/printer, gofmt: improved comment placement
Applied gofmt -w src misc (no changes).
Fixes #3147.
R=r, rsc
CC=golang-dev
https://golang.org/cl/5710046
変更の背景
この変更は、Go言語の公式フォーマッタであるgofmt
が、特定の状況下でコメントを不適切に配置するという問題(Issue 3147)を修正するために行われました。具体的には、複数行にわたる式やステートメントの後に続くコメントが、期待されるインデントレベルで配置されないことがありました。これにより、コードの可読性が損なわれたり、開発者が手動でコメントのインデントを修正する必要が生じたりしていました。
gofmt
はGo言語のコードスタイルを統一し、可読性を高める上で非常に重要なツールです。そのため、コメントの配置のような細かな点であっても、その正確性と一貫性はGoコミュニティにとって重要視されます。このコミットは、そのようなフォーマットの品質を向上させるためのものです。
前提知識の解説
go/printer
パッケージ
go/printer
パッケージは、Go言語の抽象構文木(AST)を整形してGoソースコードとして出力するためのパッケージです。gofmt
ツールはこのパッケージを利用してコードのフォーマットを行っています。このパッケージは、コードの構造、空白、改行、コメントの配置などを厳密に制御し、Goの標準的なコーディングスタイルに準拠した出力を生成します。
gofmt
ツール
gofmt
は、Go言語のソースコードを自動的にフォーマットするコマンドラインツールです。Go言語のプロジェクトでは、コードのスタイルを統一するために広く利用されています。gofmt
は、インデント、空白、改行、コメントの配置など、Goの公式スタイルガイドに沿ってコードを整形します。これにより、異なる開発者が書いたコードでも一貫した見た目を保ち、コードレビューや共同作業を容易にします。
抽象構文木(AST: Abstract Syntax Tree)
ASTは、ソースコードの構造を木構造で表現したものです。コンパイラやリンタ、フォーマッタなどのツールは、ソースコードを直接操作するのではなく、一度ASTに変換してから処理を行います。go/printer
は、このASTを受け取り、それを基に整形されたソースコードを生成します。コメントもASTの一部として扱われ、その位置情報が保持されます。
token
パッケージ
token
パッケージは、Go言語の字句解析器(lexer)によって生成されるトークン(識別子、キーワード、演算子、リテラルなど)を定義しています。各トークンには、その種類(例: token.IDENT
、token.KEYWORD
、token.ADD
)と、ソースコード内での位置情報(ファイル名、行番号、列番号)が含まれます。go/printer
は、これらのトークンの情報を使用して、コードの構造を正確に再構築し、コメントを適切な位置に配置します。
コメントの扱い
Go言語では、コメントはコードの一部として扱われ、AST内にその位置情報が保持されます。go/printer
は、コメントがコードのどの部分に関連しているかを判断し、その関連性に基づいて適切な位置にコメントを配置しようとします。特に、行コメント(//
)やブロックコメント(/* ... */
)が、その直前のコード行や次のコード行に対してどのようにインデントされるべきかが重要な考慮事項となります。
技術的詳細
このコミットの主要な変更点は、go/printer
パッケージ内のwriteCommentPrefix
関数のシグネチャと内部ロジックの修正です。この関数は、コメントを書き出す前に、そのコメントの前に挿入すべき空白や改行を決定する役割を担っています。
writeCommentPrefix
関数の変更
元のシグネチャ:
func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *ast.Comment, isKeyword bool)
変更後のシグネチャ:
func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *ast.Comment, tok token.Token)
この変更により、isKeyword
というブール値のフラグがtok token.Token
という具体的なトークン型に置き換えられました。これにより、コメントの直後に続く要素が単なるキーワードであるかどうかの情報だけでなく、その要素がどのような種類のトークンであるか(例: token.RBRACE
、token.IDENT
など)というより詳細な情報に基づいて、コメントの配置ロジックを決定できるようになりました。
コメント配置ロジックの改善
特に注目すべきは、コメントが異なる行に配置される場合の処理です。
変更前は、prev == nil
(コメントグループの最初のコメント)の場合にのみ、p.wsbuf
(空白バッファ)の処理が行われていました。
変更後は、この条件が削除され、常にp.wsbuf
の処理が行われるようになりました。
さらに、unindent
(インデント解除)の処理ロジックが改善されています。
元のコードでは、isKeyword
フラグとpos.Column == next.Column
(コメントの列と次の要素の列が一致するか)に基づいてunindent
を適用するかを判断していました。これは、コメントがキーワードに揃っている場合にのみインデント解除を行うというものでした。
変更後のコードでは、tok != token.RBRACE
(次のトークンが閉じ波括弧でない)かつpos.Column == next.Column
の場合にunindent
を適用するようになりました。
この変更の意図は、以下のシナリオに対応することです。
- 複数行にわたる式やステートメントの後に続くコメントが、その式やステートメントのインデントレベルに揃うようにする。
- 閉じ波括弧(
}
)の前にコメントがある場合、そのコメントがブロックの閉じに属していると見なし、インデント解除を適用しない。これは、case
ラベルの前にコメントがある場合など、コメントが次のcase
に適用されるべきで、現在のブロックの閉じとは関係ない場合に特に重要です。
また、droppedLinebreak
の記録方法も変更され、prev == nil
(コメントグループの最初のコメント)の場合にのみ記録されるようになりました。これは、コメントグループ内の後続のコメントでは、すでに改行が挿入されているため、再度記録する必要がないためと考えられます。
これらの変更により、gofmt
は、特に複数行の式や構造体の後に続くコメントのインデントをより正確に判断し、コードの論理的な構造に合わせた自然な配置を実現できるようになりました。
コアとなるコードの変更箇所
変更は主にsrc/pkg/go/printer/printer.go
ファイルに集中しています。
-
writeCommentPrefix
関数のシグネチャ変更:--- a/src/pkg/go/printer/printer.go +++ b/src/pkg/go/printer/printer.go @@ -277,10 +277,9 @@ func (p *printer) writeString(pos token.Position, s string, isLit bool) { // it as is likely to help position the comment nicely. // pos is the comment position, next the position of the item // after all pending comments, prev is the previous comment in -// a group of comments (or nil), and isKeyword indicates if the -// next item is a keyword. +// a group of comments (or nil), and tok is the next token. // -func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *ast.Comment, isKeyword bool) { +func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *ast.Comment, tok token.Token) { if len(p.output) == 0 { // the comment is the first item to be printed - don't write any whitespace return
isKeyword bool
がtok token.Token
に変更されました。 -
writeCommentPrefix
関数内のロジック変更:--- a/src/pkg/go/printer/printer.go +++ b/src/pkg/go/printer/printer.go @@ -335,38 +334,41 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as // comment on a different line: // separate with at least one line break droppedLinebreak := false -\t\tif prev == nil {\n-\t\t\t// first comment of a comment group\n-\t\t\tj := 0\n-\t\t\tfor i, ch := range p.wsbuf {\n-\t\t\t\tswitch ch {\n-\t\t\t\tcase blank, vtab:\n-\t\t\t\t\t// ignore any horizontal whitespace before line breaks\n-\t\t\t\t\tp.wsbuf[i] = ignore\n+\t\tj := 0\n+\t\tfor i, ch := range p.wsbuf {\n+\t\t\tswitch ch {\n+\t\t\tcase blank, vtab:\n+\t\t\t\t// ignore any horizontal whitespace before line breaks\n+\t\t\t\tp.wsbuf[i] = ignore\n+\t\t\t\tcontinue\n+\t\t\tcase indent:\n+\t\t\t\t// apply pending indentation\n+\t\t\t\tcontinue\n+\t\t\tcase unindent:\n+\t\t\t\t// if this is not the last unindent, apply it\n+\t\t\t\t// as it is (likely) belonging to the last\n+\t\t\t\t// construct (e.g., a multi-line expression list)\n+\t\t\t\t// and is not part of closing a block\n+\t\t\t\t// if the next token is not a closing }, apply the unindent\n+\t\t\t\t// if it appears that the comment is aligned with the\n+\t\t\t\t// token; otherwise assume the unindent is part of a\n+\t\t\t\t// closing block and stop (this scenario appears with\n+\t\t\t\t// comments before a case label where the comments\n+\t\t\t\t// apply to the next case instead of the current one)\n+\t\t\t\tif tok != token.RBRACE && pos.Column == next.Column {\n \t\t\t\t\tcontinue\n-\t\t\t\tcase indent:\n-\t\t\t\t\t// apply pending indentation\n+\t\t\t\t}\n+\t\t\tcase newline, formfeed:\n+\t\t\t\tp.wsbuf[i] = ignore\n+\t\t\t\tdroppedLinebreak = prev == nil // record only if first comment of a group\n \t\t\t\t}\n-\t\t\t\tj = i\n-\t\t\t\tbreak\n+\t\t\t\tj = i\n+\t\t\t\tbreak\n \t\t\t}\n-\t\t\tp.writeWhitespace(j)\n+\t\tp.writeWhitespace(j)\n ``` - `if prev == nil`のブロックが削除され、`p.wsbuf`の処理が常に実行されるようになりました。 - `unindent`の条件が`isKeyword`から`tok != token.RBRACE`に変更されました。 - `droppedLinebreak`の記録条件が`prev == nil`の場合のみに変更されました。
-
intersperseComments
関数内の呼び出し元変更:--- a/src/pkg/go/printer/printer.go +++ b/src/pkg/go/printer/printer.go @@ -675,7 +677,7 @@ func (p *printer) intersperseComments(next token.Position, tok token.Token) (wro var last *ast.Comment for p.commentBefore(next) { for _, c := range p.comment.List { -\t\t\tp.writeCommentPrefix(p.posFor(c.Pos()), next, last, c, tok.IsKeyword())\n+\t\t\tp.writeCommentPrefix(p.posFor(c.Pos()), next, last, c, tok)\n p.writeComment(c) last = c }
tok.IsKeyword()
の代わりにtok
が直接渡されるようになりました。
また、src/pkg/go/printer/testdata/comments.golden
とsrc/pkg/go/printer/testdata/comments.input
が更新され、新しいコメント配置ロジックが正しく機能することを示すテストケースが追加されています。これらのテストケースは、Issue 3147で報告された問題の具体的なシナリオをカバーしています。
コアとなるコードの解説
このコミットの核心は、go/printer
がコメントのインデントと配置を決定する方法の洗練にあります。
writeCommentPrefix
関数は、コメントの前に挿入される空白文字(スペース、タブ、改行)を制御します。この関数は、コメント自体の位置(pos
)、コメントの後に続くコード要素の位置(next
)、前のコメント(prev
)、そしてコメントの後に続くトークン(tok
)などの情報を受け取ります。
変更前は、コメントの後に続く要素が「キーワード」であるかどうかに基づいて、インデント解除(unindent
)を適用するかを判断していました。しかし、これは十分な情報ではありませんでした。例えば、複数行の式が閉じ括弧で終わる場合、その後に続くコメントは、閉じ括弧のインデントレベルに揃うべきですが、単に「キーワード」であるかどうかの情報だけでは、この複雑なケースを適切に処理できませんでした。
変更後、isKeyword
フラグがtok token.Token
に置き換えられたことで、writeCommentPrefix
関数は、コメントの後に続く具体的なトークンの種類(例: token.RBRACE
)を直接参照できるようになりました。これにより、よりきめ細やかな制御が可能になります。
特に重要なのは、unindent
の処理ロジックです。
if tok != token.RBRACE && pos.Column == next.Column
という条件は、以下のことを意味します。
tok != token.RBRACE
: コメントの後に続くトークンが閉じ波括弧(}
)ではない場合。これは、コメントがブロックの閉じに関連しているのではなく、その後の別のコード要素に関連している可能性が高いことを示唆します。pos.Column == next.Column
: コメントの開始列が、コメントの後に続くコード要素の開始列と一致する場合。これは、コメントがそのコード要素と論理的に揃っていることを示唆します。
この二つの条件が満たされる場合、unindent
が適用されます。これにより、複数行の式や構造体の後に続くコメントが、その構造体のインデントレベルに適切に揃えられるようになります。例えば、以下のようなコードで、gofmt
がコメントを正しくインデントできるようになります。
func _() {
s := 1 +
2
// should be indented like s
}
また、droppedLinebreak = prev == nil
という変更は、コメントグループの最初のコメントの場合にのみ改行が挿入されたことを記録するという意味です。これにより、不必要な改行の挿入を防ぎ、よりクリーンなフォーマットを実現します。
これらの変更は、gofmt
がGoコードを整形する際のコメント配置の精度と一貫性を大幅に向上させ、開発者にとってより予測可能で読みやすいコードを提供することに貢献しています。
関連リンク
- Go Change List: https://golang.org/cl/5710046
- Go Issue 3147 (推定): このコミットが修正しているIssue 3147は、Go言語の公式Issueトラッカー(Go Bug Tracker)に登録されていたコメント配置に関する問題であると推測されます。当時のGoのIssueトラッカーは現在とは異なるURL構造であった可能性がありますが、Goプロジェクトのコミットメッセージで参照されているIssue番号は通常、そのプロジェクトの公式Issueを指します。
参考にした情報源リンク
- GitHub Commit: https://github.com/golang/go/commit/fd5718ce82c9dec47ad6243acf3b3cc237df4efa
- Go Change List: https://golang.org/cl/5710046
go/printer
パッケージのドキュメント (Go公式ドキュメント): https://pkg.go.dev/go/printer (現在のバージョン)gofmt
ツールのドキュメント (Go公式ドキュメント): https://go.dev/blog/gofmt (現在のバージョン)go/ast
パッケージのドキュメント (Go公式ドキュメント): https://pkg.go.dev/go/ast (現在のバージョン)go/token
パッケージのドキュメント (Go公式ドキュメント): https://pkg.go.dev/go/token (現在のバージョン)