[インデックス 14494] ファイルの概要
このコミットは、Go言語の公式フォーマッタであるgo/printer
パッケージにおける内部的なクリーンアップとリファクタリングを目的としています。具体的には、以前に破棄された変更セット(Change List: CL)から導入されたクリーンアップを取り込み、既存のフォーマット動作に影響を与えないように、if
文とfor
文に関する追加のテストケースを導入しています。
コミット
commit 9a61c0412c119f5df05a7c3877985cd1dbc83931
Author: Robert Griesemer <gri@golang.org>
Date: Mon Nov 26 13:20:30 2012 -0800
go/printer: some internal cleanups
Cleanups introduced originally by now abandoned
https://golang.org/cl/6846078/ .
Includes additional test cases for 'if' and 'for'
statements.
No formatting changes.
R=rsc
CC=golang-dev
https://golang.org/cl/6855096
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9a61c0412c119f5df05a7c3877985cd1dbc83931
元コミット内容
このコミットは、go/printer
パッケージの内部的なクリーンアップを目的としています。具体的には、以前に破棄されたChange List https://golang.org/cl/6846078/
で導入されたクリーンアップ作業を再導入しています。このクリーンアップは、コードの可読性や保守性を向上させるためのものであり、既存のGoコードのフォーマット結果には影響を与えません。また、if
文とfor
文に関する追加のテストケースが含まれており、これらの変更がフォーマットに予期せぬ影響を与えないことを保証しています。
変更の背景
このコミットの背景には、Go言語のコードフォーマッタであるgo/printer
パッケージの継続的な改善があります。go/printer
は、Goの抽象構文木(AST)を受け取り、標準的なGoのフォーマット規則に従ってソースコードを生成する役割を担っています。
コミットメッセージに記載されているように、この変更は「以前に破棄されたhttps://golang.org/cl/6846078/
によって元々導入されたクリーンアップ」を再導入するものです。これは、開発プロセスにおいて一度は取り下げられたものの、その後の検討でやはり取り込むべきだと判断された内部的な改善があったことを示唆しています。破棄されたCLの内容を再評価し、その中の有用なクリーンアップを現在のコードベースに統合することで、go/printer
の内部構造をより堅牢で理解しやすいものにすることが目的です。
「No formatting changes.」という記述は非常に重要です。これは、このコミットがGoコードの見た目のフォーマットルールを変更するものではなく、あくまでgo/printer
の内部実装の改善に焦点を当てていることを明確にしています。ユーザーがgo fmt
コマンドを実行した際の出力結果は、このコミットによって変わることはありません。
また、「Includes additional test cases for 'if' and 'for' statements.」という点は、内部的な変更が既存のフォーマットロジックに悪影響を与えないことを保証するためのものです。特にif
文やfor
文のような制御構造は、コードの可読性に大きく影響するため、これらのフォーマットが正しく維持されることを確認するためのテストが追加されています。
前提知識の解説
このコミットを理解するためには、以下のGo言語および関連ツールの基本的な知識が必要です。
-
Go言語の
go/printer
パッケージ:go/printer
は、Go言語のソースコードを整形(フォーマット)するためのパッケージです。Goのコンパイラが生成する抽象構文木(AST: Abstract Syntax Tree)を入力として受け取り、Goの公式なスタイルガイド(go fmt
が適用するルール)に従って整形されたソースコードを出力します。- このパッケージは、
go fmt
コマンドの基盤となっており、Goコミュニティ全体で一貫したコードスタイルを維持するために不可欠なツールです。 - 内部的には、ASTノードを走査し、各ノードの種類に応じて適切な空白、改行、インデントなどを挿入することで、整形されたコードを生成します。
-
抽象構文木(AST: Abstract Syntax Tree):
- ASTは、ソースコードの構造を木構造で表現したものです。Go言語では、
go/ast
パッケージがASTの定義を提供しています。 - コンパイラはソースコードを解析(パース)してASTを構築し、その後のセマンティック解析やコード生成に利用します。
go/printer
は、このASTを操作してコードを整形します。例えば、ast.FuncLit
は関数リテラル、ast.BlockStmt
はブロック文({ ... }
)、ast.FuncDecl
は関数宣言を表すASTノードです。
- ASTは、ソースコードの構造を木構造で表現したものです。Go言語では、
-
token.Pos
とtoken.Position
:- Goのパーサーは、ソースコード内の各要素(トークン)が元のファイル内のどこに位置していたかを示す情報(行番号、列番号、ファイル名など)を保持します。
token.Pos
は、ソースコード内の位置を表す抽象的な型です。token.Position
は、token.Pos
を具体的なファイル名、行番号、列番号に変換した構造体です。go/printer
は、これらの位置情報を使用して、要素間の距離や改行の有無などを判断し、適切なフォーマットを適用します。
-
コードフォーマットの原則:
- Go言語のコードフォーマットは、可読性と一貫性を重視しています。特に、短いコードブロック(例:一行の
if
文やfor
文の本体)は、一行にまとめることで簡潔さを保ち、長いブロックは複数行に分割して可読性を高めるという原則があります。 - このコミットで変更されている
adjBlock
やbodySize
といった関数は、このような「一行にまとめるべきか、複数行に分割すべきか」という判断ロジックに関連しています。
- Go言語のコードフォーマットは、可読性と一貫性を重視しています。特に、短いコードブロック(例:一行の
-
GoのChange List (CL):
- Goプロジェクトでは、コード変更はGerritというコードレビューシステムを通じて提案されます。各変更は「Change List (CL)」として管理され、一意のID(例:
6846078
)が割り当てられます。 - CLはレビューを経て承認されると、Gitリポジトリにコミットされます。コミットメッセージにCLのURLが記載されるのは一般的な慣習です。
- Goプロジェクトでは、コード変更はGerritというコードレビューシステムを通じて提案されます。各変更は「Change List (CL)」として管理され、一意のID(例:
これらの知識があることで、コミットがgo/printer
の内部でどのように動作し、なぜ特定の変更が行われたのかをより深く理解できます。
技術的詳細
このコミットは、go/printer
パッケージ内のいくつかの関数をリファクタリングし、コードの構造を改善しています。主な変更点は以下の通りです。
-
funcBody
関数の廃止とadjBlock
関数の導入:- 以前は
funcBody
という関数が、関数リテラルや関数宣言のボディ(ブロック)の整形を担当していました。この関数は、ブロックが一行に収まるかどうかを判断し、それに応じて整形方法を切り替えていました。 - このコミットでは、
funcBody
が廃止され、より汎用的なadjBlock
("adjacent block"、隣接するブロック)関数が導入されました。adjBlock
は、for
ループやif
文のボディ、関数ボディなど、ヘッダー(例:for
の条件式、関数シグネチャ)に続くブロックの整形を扱います。 adjBlock
は、ヘッダーのサイズとブロックのサイズを考慮し、全体がmaxSize
(デフォルト100文字)に収まる場合に一行で出力し、そうでない場合は複数行で出力するロジックを持っています。これにより、コードの簡潔さと可読性のバランスを取っています。
- 以前は
-
isOneLineFunc
関数の廃止とbodySize
関数の導入:isOneLineFunc
は、関数ボディが一行に収まるべきかを判断するためのブール値を返す関数でした。- このコミットでは、
isOneLineFunc
が廃止され、bodySize
という関数が導入されました。bodySize
は、*ast.BlockStmt
(ブロック文)のサイズを計算し、そのブロックが一行に収まるかどうかを判断するための情報を提供します。 bodySize
は、ブロックの開始と終了のブレースが同じ行にあるか、ステートメントの数が多すぎないか、ブロック内にコメントがないかなどを考慮して、ブロックのサイズを推定します。これらの条件を満たさない場合は、infinity
(無限大)を返し、一行には収まらないことを示します。
-
distance
関数の廃止とdistanceFrom
関数の導入:distance
関数は、2つのtoken.Pos
間の列の差を計算していました。distanceFrom
関数は、from
の位置と現在の推定位置(p.pos
)との間の列の差を計算します。これにより、現在の出力位置からの相対的な距離をより正確に把握できるようになります。これは、adjBlock
でヘッダーサイズを計算する際に利用されます。
-
ast.FuncLit
とast.FuncDecl
の整形ロジックの変更:- 関数リテラル(
ast.FuncLit
)と関数宣言(ast.FuncDecl
)のボディの整形に、新しいadjBlock
関数が使用されるようになりました。これにより、これらの構造の整形ロジックがadjBlock
に集約され、コードの重複が減り、保守性が向上しています。
- 関数リテラル(
-
テストケースの追加:
src/pkg/go/printer/testdata/statements.input
とsrc/pkg/go/printer/testdata/statements.golden
に、if
文とfor
文に関する新しいテストケースが追加されました。これらのテストケースは、単一行および複数行のfor
ループ、if
文、go
ステートメント、defer
ステートメントなど、様々なシナリオをカバーしています。- これにより、内部的なクリーンアップが既存のフォーマットルールに影響を与えないことが保証されます。特に、
adjBlock
関数が導入されたことで、ブロックの整形ロジックが変更されたため、これらのテストは非常に重要です。
これらの変更は、go/printer
の内部構造をよりモジュール化し、将来的な拡張やメンテナンスを容易にすることを目的としています。既存のフォーマット出力に影響を与えないように細心の注意が払われており、追加されたテストケースがその保証を強化しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主にsrc/pkg/go/printer/nodes.go
ファイルに集中しています。
-
func (p *printer) expr1(expr ast.Expr, prec1, depth int)
内の変更:--- a/src/pkg/go/printer/nodes.go +++ b/src/pkg/go/printer/nodes.go @@ -730,7 +730,7 @@ func (p *printer) expr1(expr ast.Expr, prec1, depth int) { case *ast.FuncLit: \tp.expr(x.Type) -\t\tp.funcBody(x.Body, p.distance(x.Type.Pos(), p.pos), true) +\t\tp.adjBlock(p.distanceFrom(x.Type.Pos()), blank, x.Body) case *ast.ParenExpr: \tif _, hasParens := x.X.(*ast.ParenExpr); hasParens {
ast.FuncLit
(関数リテラル)のボディを整形する際に、funcBody
の代わりに新しく導入されたadjBlock
を呼び出すように変更されています。p.distance(x.Type.Pos(), p.pos)
がp.distanceFrom(x.Type.Pos())
に、true
がblank
に置き換えられています。
-
func (p *printer) block(b *ast.BlockStmt, nindent int)
の変更:--- a/src/pkg/go/printer/nodes.go +++ b/src/pkg/go/printer/nodes.go @@ -916,11 +916,11 @@ func (p *printer) stmtList(list []ast.Stmt, nindent int, nextIsRBrace bool) { }\n \n // block prints an *ast.BlockStmt; it always spans at least two lines.\n-func (p *printer) block(s *ast.BlockStmt, nindent int) {\n-\tp.print(s.Pos(), token.LBRACE)\n-\tp.stmtList(s.List, nindent, true)\n-\tp.linebreak(p.lineFor(s.Rbrace), 1, ignore, true)\n-\tp.print(s.Rbrace, token.RBRACE)\n+func (p *printer) block(b *ast.BlockStmt, nindent int) {\n+\tp.print(b.Lbrace, token.LBRACE)\n+\tp.stmtList(b.List, nindent, true)\n+\tp.linebreak(p.lineFor(b.Rbrace), 1, ignore, true)\n+\tp.print(b.Rbrace, token.RBRACE)\n }\n \n func isTypeName(x ast.Expr) bool {\
block
関数の引数名がs
からb
に変更され、それに伴い内部の参照もs.Pos()
からb.Lbrace
、s.Rbrace
からb.Rbrace
に変更されています。これは、より明確な命名と、ブロックの開始ブレースの位置を直接参照するための変更です。
-
isOneLineFunc
関数の削除とbodySize
関数の追加:--- a/src/pkg/go/printer/nodes.go +++ b/src/pkg/go/printer/nodes.go @@ -1425,19 +1425,19 @@ func (p *printer) nodeSize(n ast.Node, maxSize int) (size int) {\ \treturn\n }\n \n-func (p *printer) isOneLineFunc(b *ast.BlockStmt, headerSize int) bool {\ +// bodySize is like nodeSize but it is specialized for *ast.BlockStmt's.\n+func (p *printer) bodySize(b *ast.BlockStmt, maxSize int) int {\ \tpos1 := b.Pos()\n \tpos2 := b.Rbrace\n \tif pos1.IsValid() && pos2.IsValid() && p.lineFor(pos1) != p.lineFor(pos2) {\ \t\t// opening and closing brace are on different lines - don't make it a one-liner\n-\t\treturn false\n+\t\treturn infinity\n \t}\n \tif len(b.List) > 5 || p.commentBefore(p.posFor(pos2)) {\ \t\t// too many statements or there is a comment inside - don't make it a one-liner\n-\t\treturn false\n+\t\treturn infinity\n \t}\n \t// otherwise, estimate body size\n-\tconst maxSize = 100\n \tbodySize := 0\n \tfor i, s := range b.List {\ \t\tif i > 0 {\ @@ -1445,19 +1445,23 @@ func (p *printer) isOneLineFunc(b *ast.BlockStmt, headerSize int) bool {\ \t\t}\n \t\tbodySize += p.nodeSize(s, maxSize)\n \t}\n-\treturn headerSize+bodySize <= maxSize\n+\treturn bodySize }\n ``` * `isOneLineFunc`関数が削除され、代わりに`bodySize`関数が追加されています。`bodySize`は、ブロックが一行に収まるべきかを判断するためのサイズを返します。
-
funcBody
関数の削除とadjBlock
関数の追加:--- a/src/pkg/go/printer/nodes.go +++ b/src/pkg/go/printer/nodes.go @@ -1445,19 +1445,23 @@ func (p *printer) isOneLineFunc(b *ast.BlockStmt, headerSize int) bool {\ \t\t}\n \t\tbodySize += p.nodeSize(s, maxSize)\n \t}\n-\treturn headerSize+bodySize <= maxSize\n+\treturn bodySize }\n \n-func (p *printer) funcBody(b *ast.BlockStmt, headerSize int, isLit bool) {\ +// adjBlock prints an "adjacent" block (e.g., a for-loop or function body) following\n+// a header (e.g., a for-loop control clause or function signature) of given headerSize.\n+// If the header's and block's size are "small enough" and the block is "simple enough",\n+// the block is printed on the current line, without line breaks, spaced from the header\n+// by sep. Otherwise the block's opening "{" is printed on the current line, followed by\n+// lines for the block's statements and its closing "}".\n+//\n+func (p *printer) adjBlock(headerSize int, sep whiteSpace, b *ast.BlockStmt) {\ \tif b == nil {\n \t\treturn\n \t}\n \n-\tif p.isOneLineFunc(b, headerSize) {\ -\t\tsep := vtab\n-\t\tif isLit {\ -\t\t\tsep = blank\n-\t\t}\n+\tconst maxSize = 100\n+\tif headerSize+p.bodySize(b, maxSize) <= maxSize {\ \t\tp.print(sep, b.Lbrace, token.LBRACE)\n \t\tif len(b.List) > 0 {\ \t\t\tp.print(blank)\ @@ -1473,17 +1477,20 @@ func (p *printer) funcBody(b *ast.BlockStmt, headerSize int, isLit bool) {\ \t\treturn\n \t}\n \n-\tp.print(blank)\n+\tif sep != ignore {\n+\t\tp.print(blank) // always use blank\n+\t}\n \tp.block(b, 1)\n }\n ``` * `funcBody`関数が削除され、代わりに`adjBlock`関数が追加されています。この関数は、ヘッダーサイズとブロックサイズに基づいて、ブロックを一行で出力するか複数行で出力するかを決定します。
-
distance
関数の削除とdistanceFrom
関数の追加:--- a/src/pkg/go/printer/nodes.go +++ b/src/pkg/go/printer/nodes.go @@ -1497,7 +1504,7 @@ func (p *printer) funcDecl(d *ast.FuncDecl) {\ \t}\n \tp.expr(d.Name)\n \tp.signature(d.Type.Params, d.Type.Results)\n-\tp.funcBody(d.Body, p.distance(d.Pos(), p.pos), false)\n+\tp.adjBlock(p.distanceFrom(d.Pos()), vtab, d.Body)\ }\n \n func (p *printer) decl(decl ast.Decl) {\
distance
関数が削除され、代わりにdistanceFrom
関数が追加されています。この関数は、指定された位置から現在の推定位置までの列の差を計算します。ast.FuncDecl
(関数宣言)のボディを整形する際にも、funcBody
の代わりにadjBlock
が使用されるように変更されています。
-
テストデータの追加:
src/pkg/go/printer/testdata/statements.golden
とsrc/pkg/go/printer/testdata/statements.input
に、if
文とfor
文の新しいテストケースが追加されています。これにより、adjBlock
などの新しいロジックが既存のフォーマットルールに準拠していることが確認されます。
これらの変更は、go/printer
の内部的な構造を改善し、コードの再利用性を高め、より堅牢なフォーマットロジックを構築することを目的としています。
コアとなるコードの解説
このコミットのコアとなる変更は、go/printer
パッケージがGoコードのブロック({ ... }
)をどのように整形するか、特にそのブロックを一行にまとめるか、複数行に分割するかの判断ロジックにあります。
主要な変更点は以下の通りです。
-
adjBlock
関数:- この関数は、
for
ループ、if
文、関数リテラル、関数宣言など、ヘッダー(例:for
の条件式、関数シグネチャ)の後に続くコードブロックを整形するための汎用的なメカニズムを提供します。 adjBlock
の主な目的は、ヘッダーとブロックの合計サイズがmaxSize
(このコミットでは100
)以下である場合に、ブロックを一行で出力することです。これにより、短いコードブロックが簡潔に表現され、可読性が向上します。- もし合計サイズが
maxSize
を超えるか、ブロックが複雑(例: ステートメントが多い、内部にコメントがある、開始と終了のブレースが異なる行にある)である場合は、ブロックは複数行に分割されて出力されます。 sep
引数は、ヘッダーとブロックの間に挿入される空白の種類(例:blank
、vtab
、ignore
)を制御します。
- この関数は、
-
bodySize
関数:- この関数は、
*ast.BlockStmt
(ブロック文)の「サイズ」を推定します。この「サイズ」は、ブロックを一行で出力した場合の文字数に相当します。 bodySize
は、以下の条件をチェックして、ブロックが一行に収まるべきかを判断します。- ブレースの位置: ブロックの開始ブレースと終了ブレースが同じ行にあるか。異なる行にある場合は、一行には収まらないと判断し、
infinity
を返します。 - ステートメントの数: ブロック内のステートメントの数が5を超える場合、一行には収まらないと判断し、
infinity
を返します。これは、あまりにも多くのステートメントを一行に詰め込むと可読性が損なわれるためです。 - コメントの有無: 終了ブレースの前にコメントがある場合、一行には収まらないと判断し、
infinity
を返します。コメントは通常、コードの特定のセクションを説明するために使用されるため、一行にまとめるべきではないと見なされます。
- ブレースの位置: ブロックの開始ブレースと終了ブレースが同じ行にあるか。異なる行にある場合は、一行には収まらないと判断し、
- これらの条件を満たさない場合、
bodySize
はブロック内の各ステートメントのサイズを合計して、推定されるボディサイズを返します。
- この関数は、
-
distanceFrom
関数:- この関数は、指定された
token.Pos
(ソースコード内の位置)から、現在のプリンタの推定位置(p.pos
)までの列の差を計算します。 - これは、
adjBlock
関数でヘッダーのサイズを計算する際に使用されます。ヘッダーのサイズとボディのサイズを合計することで、ブロック全体が一行に収まるかどうかを正確に判断できます。
- この関数は、指定された
これらの関数は連携して動作し、Goコードの整形において、簡潔さと可読性のバランスを取りながら、一貫したスタイルを適用します。特に、adjBlock
とbodySize
の導入により、ブロックの整形ロジックがより明確に分離され、再利用性が高まっています。これにより、go/printer
の内部コードがよりクリーンで保守しやすくなっています。
関連リンク
- Go Change List 6855096: このコミットに対応するGerritのChange List。 https://golang.org/cl/6855096
- Go Change List 6846078: このコミットでクリーンアップが再導入された、以前に破棄されたGerritのChange List。 https://golang.org/cl/6846078
参考にした情報源リンク
- Go言語の
go/printer
パッケージのドキュメント: https://pkg.go.dev/go/printer - Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージのドキュメント: https://pkg.go.dev/go/token - Go Code Review Comments - Formatting: Go言語のコードフォーマットに関する公式ガイドライン。 https://go.dev/doc/effective_go#formatting
- Gerrit Code Review: Goプロジェクトが使用しているコードレビューシステム。 https://gerrit-review.googlesource.com/