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

[インデックス 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.IDENTtoken.KEYWORDtoken.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.RBRACEtoken.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ファイルに集中しています。

  1. 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 booltok token.Tokenに変更されました。

  2. 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`の場合のみに変更されました。
    
    
  3. 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.goldensrc/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を指します。

参考にした情報源リンク