[インデックス 10853] ファイルの概要
このコミットは、Go言語の公式フォーマッタであるgofmt
およびその基盤となるgo/printer
パッケージにおける、コードの行間(空白行)の整形ルールを微調整するものです。具体的には、空の構造体やインターフェース内の不要な空白行を削除し、トップレベルの宣言(const
, type
, var
, func
など)間の空白行の挿入ルールを改善しています。特に、異なる種類の宣言間、またはドキュメンテーションコメントが付随する宣言の前に空白行を挿入する新しいルールが導入されました。これにより、Goコードの可読性と一貫性が向上します。
コミット
commit 541b67d051fbd26f3727d4d13c6d2b025af8a775
Author: Robert Griesemer <gri@golang.org>
Date: Fri Dec 16 15:43:06 2011 -0800
go/printer, gofmt: fine tuning of line spacing
- no empty lines inside empty structs and interfaces
- top-level declarations are separated by a blank line if
a) they are of different kind (e.g. const vs type); or
b) there are documentation comments associated with a
declaration (this is new)
- applied gofmt -w misc src
The actual changes are in go/printer/nodes.go:397-400 (empty structs/interfaces),
and go/printer/printer.go:307-309 (extra line break). The remaining
changes are cleanups w/o changing the existing functionality.
Fixes issue 2570.
R=rsc
CC=golang-dev
https://golang.org/cl/5493057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/541b67d051fbd26f3727d4d13c6d2b025af8a775
元コミット内容
このコミットは、go/printer
パッケージとgofmt
ツールにおける行間(空白行)の整形ロジックを微調整することを目的としています。主な変更点は以下の通りです。
- 空の構造体およびインターフェース内の空白行の削除:
{}
のような空の宣言内に余分な空白行が挿入されないようにします。 - トップレベル宣言間の空白行の調整:
- 異なる種類のトップレベル宣言(例:
const
とtype
)の間には空白行を挿入します。 - 新規ルール: ドキュメンテーションコメントが付随する宣言の前には、空白行を挿入します。
- 異なる種類のトップレベル宣言(例:
gofmt -w misc src
コマンドを適用し、既存のGoソースコードベース全体にこれらの新しい整形ルールを適用しています。
この変更は、Issue 2570を修正するものです。
変更の背景
このコミットの背景には、Go言語のコード整形ツールgofmt
の出力の一貫性と可読性をさらに向上させるという目的があります。特に、Issue 2570で報告された問題に対処しています。
Issue 2570は、gofmt
がトップレベルの宣言(const
, type
, var
, func
など)の前にドキュメンテーションコメントがある場合に、そのコメントと前の宣言との間に適切な空白行を挿入しないことがあるという問題でした。これにより、生成されるコードの可読性が損なわれる可能性がありました。
Go言語では、gofmt
によってコードが自動的に整形されることで、プロジェクト全体で一貫したコーディングスタイルが保たれることが重視されています。しかし、既存のルールでは、特にドキュメンテーションコメントと宣言の間の空白行の扱いに不整合が生じることがありました。このコミットは、これらのエッジケースを修正し、より自然で読みやすいコードレイアウトを実現するために行われました。
また、空の構造体やインターフェース(例: struct{}
やinterface{}
)の内部に不要な空白行が挿入される問題も修正対象でした。これもまた、gofmt
の出力の「美しさ」と一貫性を高めるための微調整です。
前提知識の解説
このコミットを理解するためには、以下のGo言語のツールと概念に関する知識が必要です。
-
gofmt
:gofmt
はGo言語の公式なコード整形ツールです。Goのソースコードを解析し、Goコミュニティで推奨される標準的なスタイルに自動的に整形します。- その目的は、Goコードの可読性を高め、スタイルに関する議論を減らすことです。
gofmt
は、インデント、空白行、括弧の配置など、多くの整形ルールを自動的に適用します。 - このコミットは、
gofmt
が適用する整形ルールの一部、特に空白行の挿入ロジックを改善するものです。
-
go/printer
パッケージ:go/printer
は、Goの抽象構文木(AST)を整形し、Goソースコードとして出力するためのパッケージです。gofmt
はこのパッケージを内部的に利用しています。- このパッケージは、ASTノードをトラバースし、コメント、空白、インデントなどを考慮しながら、最終的なソースコード文字列を生成します。
- このコミットの主要な変更は、この
go/printer
パッケージ内のロジックにあります。
-
抽象構文木 (AST - Abstract Syntax Tree):
- Goコンパイラやツール(
gofmt
など)は、Goのソースコードを直接操作するのではなく、まずソースコードを解析してASTと呼ばれるツリー構造に変換します。 - ASTは、プログラムの構造を抽象的に表現したもので、各ノードがコードの要素(宣言、式、文など)に対応します。
go/printer
は、このASTを受け取り、それを整形して出力します。
- Goコンパイラやツール(
-
go/ast
パッケージ:- Goの標準ライブラリの一部で、GoソースコードのASTを定義しています。
ast.File
,ast.Decl
(宣言),ast.Expr
(式),ast.Stmt
(文) などの型が含まれます。 - このコミットでは、
ast.FieldList
(構造体やインターフェースのフィールドリスト) やast.Decl
(トップレベル宣言) などのASTノードの処理が変更されています。
- Goの標準ライブラリの一部で、GoソースコードのASTを定義しています。
-
go/token
パッケージ:- Goの標準ライブラリの一部で、Go言語の字句要素(トークン)とソースコード上の位置(
token.Pos
)を定義しています。 go/printer
は、トークンの位置情報を使用して、コメントの位置や空白行の挿入を決定します。
- Goの標準ライブラリの一部で、Go言語の字句要素(トークン)とソースコード上の位置(
-
ドキュメンテーションコメント:
- Goでは、パッケージ、関数、型、変数などの宣言の直前に書かれたコメントは、その宣言のドキュメンテーションとして扱われます。
- これらのコメントは
godoc
ツールによって抽出され、ドキュメントとして表示されます。 - このコミットでは、ドキュメンテーションコメントが付随する宣言の前に、適切な空白行を挿入するルールが追加されました。
技術的詳細
このコミットの技術的な変更は、主にsrc/pkg/go/printer/nodes.go
とsrc/pkg/go/printer/printer.go
の2つのファイルに集中しています。
1. 空の構造体およびインターフェース内の空白行の削除 (nodes.go
)
nodes.go
の(*printer).fieldList
メソッドは、構造体やインターフェースのフィールドリストを整形する役割を担っています。このコミットでは、空の構造体やインターフェース(例: struct{}
や interface{}
)が1行で記述されている場合に、{
と}
の間に不要な空白行が挿入されないようにロジックが変更されました。
変更前は、len(list) == 0
(フィールドがない)かつ!isIncomplete
(不完全な宣言ではない)の場合でも、p.print(blank, lbrace, token.LBRACE, indent, formfeed)
という行でformfeed
(改行とインデント)が強制的に挿入される可能性がありました。
変更後、hasComments
という新しいフラグが導入されました。これは、isIncomplete
であるか、または閉じ括弧}
の前にコメントがある場合にtrue
になります。
if !hasComments && srcIsOneLine
という条件が追加され、コメントがなく、かつソースコードが1行で書かれている場合に、特別な処理(// no blank between keyword and {} in this case
)が適用され、formfeed
が抑制されます。
これにより、type S struct{}
のような宣言が、type S struct{\n}
のように整形されるのを防ぎ、よりコンパクトな出力が実現されます。
2. トップレベル宣言間の空白行の調整 (nodes.go
とprinter.go
)
この変更は、nodes.go
の(*printer).file
メソッドとprinter.go
の(*printer).writeCommentPrefix
メソッドに関連しています。
nodes.go
の変更点:
(*printer).file
メソッドは、ファイルのトップレベル宣言を整形します。変更前は、異なる種類の宣言(例: const
からtype
へ)に切り替わる場合にのみ、宣言間に空白行(min = 2
)が挿入されていました。
変更後、以下の条件が追加されました。
if prev != tok || getDoc(d) != nil
これは、前の宣言の種類と現在の宣言の種類が異なる場合(prev != tok
)に加えて、**現在の宣言にドキュメンテーションコメントが付随している場合(getDoc(d) != nil
)**にも、宣言間に空白行を挿入する(min = 2
)ようにします。
getDoc(d)
は、与えられたASTノードd
に関連付けられたドキュメンテーションコメントグループを返します。これにより、ドキュメンテーションコメントを持つ宣言が、その前の宣言から視覚的に区切られるようになり、可読性が向上します。
printer.go
の変更点:
(*printer).writeCommentPrefix
メソッドは、コメントを書き出す前に必要な空白(特に改行)を処理します。このコミットでは、パッケージスコープ(p.indent == 0
)でドキュメンテーションコメントの前に空白行が適切に挿入されるようにロジックが調整されました。
特に重要なのは、droppedLinebreak
という新しいフラグと、以下のロジックです。
if p.indent == 0 && droppedLinebreak { n++ }
これは、writeCommentPrefix
が以前の空白行を「ドロップ」した場合(つまり、整形処理の過程で改行が失われた場合)に、パッケージスコープのコメントの前に追加の改行を挿入することで、Issue 2570で報告された問題を修正します。これにより、ドキュメンテーションコメントが前のコード行に密着しすぎるのを防ぎ、常に適切な空白行が確保されるようになります。
また、getDoc
関数が*ast.Field
のDoc
フィールドも参照するように拡張され、構造体フィールドのドキュメンテーションコメントも適切に扱えるようになりました。
これらの変更は、gofmt
が生成するコードの視覚的な一貫性と可読性を高めるための、細部にわたる調整です。
コアとなるコードの変更箇所
このコミットの主要な変更は以下のファイルと行範囲にあります。
src/pkg/go/printer/nodes.go
:fieldList
メソッド内の空の構造体/インターフェースの整形ロジック: 約397-400行目付近file
メソッド内のトップレベル宣言間の空白行挿入ロジック: 約1512-1517行目付近
src/pkg/go/printer/printer.go
:writeCommentPrefix
メソッド内のコメント前の空白行処理ロジック: 約289-309行目付近getDoc
関数への*ast.Field
ケースの追加: 約812-814行目付近
コアとなるコードの解説
src/pkg/go/printer/nodes.go
fieldList
メソッドの変更 (空の構造体/インターフェースの整形)
--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -364,9 +364,10 @@ func (p *printer) fieldList(fields *ast.FieldList, isStruct, isIncomplete bool)\
lbrace := fields.Opening
list := fields.List
rbrace := fields.Closing
+\thasComments := isIncomplete || p.commentBefore(p.fset.Position(rbrace))\
srcIsOneLine := lbrace.IsValid() && rbrace.IsValid() && p.fset.Position(lbrace).Line == p.fset.Position(rbrace).Line
-\tif !isIncomplete && !p.commentBefore(p.fset.Position(rbrace)) && srcIsOneLine {\
+\tif !hasComments && srcIsOneLine {\
// possibly a one-line struct/interface
if len(list) == 0 {\
// no blank between keyword and {} in this case
@@ -391,9 +392,13 @@ func (p *printer) fieldList(fields *ast.FieldList, isStruct, isIncomplete bool)\
return
}
}\
+\t// hasComments || !srcIsOneLine
+\
+\tp.print(blank, lbrace, token.LBRACE, indent)\
+\tif hasComments || len(list) > 0 {\
+\t\tp.print(formfeed)\
+\t}
\n-\t// at least one entry or incomplete
-\tp.print(blank, lbrace, token.LBRACE, indent, formfeed)\
\tif isStruct {
sep := vtab
hasComments
の導入:isIncomplete
であるか、または閉じ括弧rbrace
の前にコメントがある場合にtrue
となるhasComments
変数が追加されました。これは、空の構造体/インターフェースであっても、コメントが存在する場合は特別な整形ルールを適用するためのものです。- 条件式の変更: 以前は
!isIncomplete && !p.commentBefore(p.fset.Position(rbrace)) && srcIsOneLine
という条件で1行整形を試みていましたが、!hasComments && srcIsOneLine
に変更されました。これにより、コメントがある場合は1行整形を避けるようになります。 p.print
呼び出しの変更: 以前は無条件にformfeed
(改行とインデント)を伴って{
を出力していましたが、新しいロジックでは、hasComments
があるか、またはフィールドリストが空でない場合(len(list) > 0
)にのみformfeed
を挿入するようになりました。これにより、struct{}
のような空の宣言が1行で出力されるようになります。
file
メソッドの変更 (トップレベル宣言間の空白行)
--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -1512,9 +1517,14 @@ func (p *printer) file(src *ast.File) {\
prev := tok
tok = declToken(d)
// if the declaration token changed (e.g., from CONST to TYPE)
+\t\t\t// or the next declaration has documentation associated with it,
\t\t\t// print an empty line between top-level declarations
+\t\t\t// (because p.linebreak is called with the position of d, which
+\t\t\t// is past any documentation, the minimum requirement is satisfied
+\t\t\t// even w/o the extra getDoc(d) nil-check - leave it in case the
+\t\t\t// linebreak logic improves - there\'s already a TODO).\
min := 1
-\t\t\tif prev != tok {\
+\t\t\tif prev != tok || getDoc(d) != nil {\
min = 2
}
p.linebreak(p.fset.Position(d.Pos()).Line, min, ignore, false)\
- 空白行挿入条件の追加: トップレベル宣言間に空白行(
min = 2
)を挿入する条件に、getDoc(d) != nil
が追加されました。これは、現在の宣言d
にドキュメンテーションコメントが付随している場合に、前の宣言との間に空白行を強制的に挿入することを意味します。これにより、ドキュメンテーションコメントを持つ宣言がより明確に区切られます。
src/pkg/go/printer/printer.go
writeCommentPrefix
メソッドの変更 (コメント前の空白行処理)
--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -257,6 +257,7 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as\
} else {
// comment on a different line:
// separate with at least one line break
+\t\tdroppedLinebreak := false
if prev == nil {
// first comment of a comment group
j := 0
@@ -282,6 +283,7 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as\
case newline, formfeed:
// TODO(gri): may want to keep formfeed info in some cases
p.wsbuf[i] = ignore
+\t\t\t\t\tdroppedLinebreak = true
}
j = i
break
@@ -289,25 +291,41 @@ func (p *printer) writeCommentPrefix(pos, next token.Position, prev, comment *as\
p.writeWhitespace(j)
}
-\t\t// turn off indent if we\'re about to print a line directive.\
-\t\tindent := p.indent
-\t\tif strings.HasPrefix(comment.Text, linePrefix) {\
-\t\t\tp.indent = 0
+\t\t// determine number of linebreaks before the comment
+\t\tn := 0
+\t\tif pos.IsValid() && p.last.IsValid() {\
+\t\t\tn = pos.Line - p.last.Line
+\t\t\tif n < 0 { // should never happen
+\t\t\t\tn = 0
+\t\t\t}\
+\t\t}
+\n+\t\t// at the package scope level only (p.indent == 0),\
+\t\t// add an extra newline if we dropped one before:\
+\t\t// this preserves a blank line before documentation\
+\t\t// comments at the package scope level (issue 2570)\
+\t\tif p.indent == 0 && droppedLinebreak {\
+\t\t\tn++
\t\t}\
-\t\t// use formfeeds to break columns before a comment;\
-\t\t// this is analogous to using formfeeds to separate\
-\t\t// individual lines of /*-style comments - but make\n-\t\t// sure there is at least one line break if the previous\n-\t\t// comment was a line comment\n-\t\tn := pos.Line - p.last.Line // if !pos.IsValid(), pos.Line == 0, and n will be 0\n-\t\tif n <= 0 && prev != nil && prev.Text[1] == \'/\' {\
+\t\t// make sure there is at least one line break\n+\t\t// if the previous comment was a line comment\n+\t\tif n == 0 && prev != nil && prev.Text[1] == \'/\' {\
\t\t\tn = 1
\t\t}\
+\n \t\tif n > 0 {\
+\t\t\t// turn off indent if we\'re about to print a line directive\n+\t\t\tindent := p.indent\n+\t\t\tif strings.HasPrefix(comment.Text, linePrefix) {\
+\t\t\t\tp.indent = 0\n+\t\t\t}\n+\t\t\t// use formfeeds to break columns before a comment;\n+\t\t\t// this is analogous to using formfeeds to separate\n+\t\t\t// individual lines of /*-style comments\n \t\t\tp.writeByteN(\'\\f\', nlimit(n))\n+\t\t\t\tp.indent = indent // restore indent\n \t\t}\
-\t\tp.indent = indent\
\t}\
}\
droppedLinebreak
フラグ: コメントの前にあった改行が整形処理中に失われたかどうかを追跡するためのdroppedLinebreak
フラグが追加されました。- パッケージスコープでの追加改行:
p.indent == 0
(パッケージスコープ)かつdroppedLinebreak
がtrue
の場合に、n++
によって改行数を1つ増やしています。これは、Issue 2570で報告された、ドキュメンテーションコメントの前に空白行が不足する問題を修正するためのものです。これにより、パッケージレベルのドキュメンテーションコメントが常に適切な空白行で区切られるようになります。 - インデントの扱い: 行ディレクティブ(
//line
)をプリントする際に一時的にインデントをオフにし、その後復元するロジックが、n > 0
のブロック内に移動し、より適切に管理されるようになりました。
getDoc
関数の変更
--- a/src/pkg/go/printer/printer.go
+++ b/src/pkg/go/printer/printer.go
@@ -812,7 +830,8 @@ func (p *printer) flush(next token.Position, tok token.Token) (wroteNewline, dro\
// getNode returns the ast.CommentGroup associated with n, if any.\
func getDoc(n ast.Node) *ast.CommentGroup {\
switch n := n.(type) {\
-\t// *ast.Fields cannot be printed separately - ignore for now\
+\tcase *ast.Field:\
+\t\treturn n.Doc\
case *ast.ImportSpec:\
return n.Doc
case *ast.ValueSpec:\
*ast.Field
のサポート:getDoc
関数が*ast.Field
型も処理できるようになりました。これにより、構造体やインターフェースのフィールドに付随するドキュメンテーションコメントもgo/printer
によって適切に認識され、整形ロジックに反映されるようになります。
これらの変更は、Goのコード整形における細かな視覚的品質と一貫性を向上させるための重要なステップです。
関連リンク
- Go Gerrit Change-ID: https://golang.org/cl/5493057
- Go Issue 2570: https://code.google.com/p/go/issues/detail?id=2570 (現在はGitHub Issuesに移行している可能性がありますが、当時のリンクです)
参考にした情報源リンク
- Go言語の公式ドキュメント (
go/printer
,go/ast
,go/token
パッケージ) gofmt
の設計思想に関するGoブログ記事やトーク- Go言語のIssueトラッカー (Issue 2570の詳細)
- Go言語のソースコード (特に
src/cmd/gofmt
とsrc/pkg/go/printer
ディレクトリ) - https://go.dev/blog/gofmt (gofmtに関する公式ブログ記事)
- https://pkg.go.dev/go/printer (go/printerパッケージのドキュメント)
- https://pkg.go.dev/go/ast (go/astパッケージのドキュメント)
- https://pkg.go.dev/go/token (go/tokenパッケージのドキュメント)