[インデックス 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/