[インデックス 12271] ファイルの概要
このコミットは、Go言語の標準ライブラリであるgo/printer
パッケージにおけるコード整形ロジックの改善に関するものです。go/printer
パッケージは、Goの抽象構文木(AST: Abstract Syntax Tree)を受け取り、それを整形されたGoのソースコードとして出力する役割を担っています。これはgofmt
ツールの中核をなすコンポーネントであり、Goコードの標準的なフォーマットを維持するために不可欠です。
この変更の主な目的は、コードの複数行にわたる表現を検出するための既存のロジックを簡素化し、より効率的で保守しやすい実装に置き換えることです。具体的には、多くの関数で引数として渡されていたmultiLine *bool
というフラグを削除し、ASTノードの開始位置と終了位置から複数行かどうかを判断するように変更しています。
コミット
commit b1b0ed1e60af93bc83298da80d0293a2b23fcb5e
Author: Robert Griesemer <gri@golang.org>
Date: Wed Feb 29 08:38:31 2012 -0800
go/printer: replace multiline logic
This CL mostly deletes code.
Using existing position information is
just as good to determine if a new section
is needed; no need to track exact multi-
line information. Eliminates the need to
carry around a multiLine parameter with
practically every function.
Applied gofmt -w src misc resulting in only
a minor change to godoc.go. In return, a couple
of test cases are now formatted better.
Not Go1-required, but nice-to-have as it will
simplify fixes going forward.
R=rsc
CC=golang-dev
https://golang.org/cl/5706055
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/b1b0ed1e60af93bc83298da80d0293a2b23fcb5e
元コミット内容
go/printer: replace multiline logic
This CL mostly deletes code.
Using existing position information is
just as good to determine if a new section
is needed; no need to track exact multi-
line information. Eliminates the need to
carry around a multiLine parameter with
practically every function.
Applied gofmt -w src misc resulting in only
a minor change to godoc.go. In return, a couple
of test cases are now formatted better.
Not Go1-required, but nice-to-have as it will
simplify fixes going forward.
R=rsc
CC=golang-dev
https://golang.org/cl/5706055
変更の背景
この変更の背景には、go/printer
パッケージにおけるコード整形ロジックの複雑性の解消があります。以前の実装では、コードの要素(式、宣言、ステートメントなど)が複数行にわたるかどうかを判断するために、multiLine *bool
というポインタ型のブーリアンフラグが多くの関数間で引き回されていました。このフラグは、関数が処理するASTノードが複数行にわたる場合にtrue
に設定され、その情報に基づいて改行やインデントの調整が行われていました。
しかし、コミットメッセージが示唆するように、このmultiLine
パラメータは冗長であり、コードベース全体にわたって多くの関数シグネチャを複雑化させていました。GoのASTノードは、token.Pos
とtoken.End
という位置情報を持っており、これらを利用すれば、ノードがソースコードの何行目から何行目までを占めるかを正確に知ることができます。したがって、明示的なmultiLine
フラグを渡す代わりに、既存の位置情報から複数行の状態を推論することが可能であると判断されました。
この変更は、コードの削除と簡素化を主眼としており、将来的なバグ修正や機能追加の際に、よりクリーンで理解しやすいコードベースを提供することを目指しています。また、gofmt
の出力にもわずかな改善が見られ、一部のテストケースでより良いフォーマットが適用されるようになりました。
前提知識の解説
このコミットの変更内容を理解するためには、以下のGo言語の標準ライブラリと概念に関する知識が役立ちます。
-
go/ast
パッケージ (Abstract Syntax Tree):go/ast
パッケージは、Goのソースコードを解析して抽象構文木(AST)を構築するためのデータ構造と関数を提供します。ASTは、プログラムの構造を木構造で表現したもので、コンパイラやコード分析ツール、そしてgofmt
のようなコード整形ツールにとっての入力となります。- ASTの各ノードは、
ast.Node
インターフェースを実装しており、Pos()
メソッドとEnd()
メソッドを通じて、それがソースコードのどの位置(開始と終了)に対応するかを返します。
-
go/token
パッケージ (トークンと位置情報):go/token
パッケージは、Goのソースコードを構成する個々の要素(キーワード、識別子、演算子など)である「トークン」と、それらのトークンがソースコード内のどこに位置するかを示す「位置情報」を扱います。token.Pos
はソースコード内の絶対位置を表し、token.Position
はファイル名、行番号、列番号などの人間が読める形式の位置情報を提供します。token.FileSet
は、複数のファイルにわたる位置情報を管理するために使用されます。printer
パッケージは、これらの位置情報を使用して、整形されたコードの改行やインデントを決定します。
-
go/printer
パッケージの役割:go/printer
パッケージは、go/ast
パッケージによって生成されたASTを受け取り、それをGoの標準的なフォーマット規則に従って整形されたソースコード文字列に変換する役割を担います。gofmt
コマンドはこのパッケージを利用しています。- コード整形では、改行、インデント、空白の挿入などが重要になります。特に、式や宣言が複数行にわたる場合に、どのように整形するかは、コードの可読性に大きく影響します。
-
コードフォーマッタにおける「複数行」の概念と整形への影響:
- コードフォーマッタは、要素が単一行に収まるか、複数行にわたるかによって、異なる整形ルールを適用することがよくあります。例えば、関数呼び出しの引数リストが長い場合、各引数を新しい行に配置し、適切にインデントすることが一般的です。
- この「複数行」の判定は、整形ロジックの複雑さに直結します。以前は
multiLine
フラグで明示的に伝達していましたが、このコミットではASTノードの位置情報からこの状態を推論するように変更されました。
技術的詳細
このコミットの技術的な核心は、go/printer
パッケージ内の多くの関数からmultiLine *bool
パラメータを削除し、代わりにASTノードの開始位置と終了位置から複数行の情報を動的に判断する新しいアプローチを採用した点にあります。
multiLine *bool
パラメータの問題点
以前のgo/printer
の実装では、identList
, exprList
, parameters
, signature
, binaryExpr
, expr1
, stmt
, decl
, funcDecl
, funcBody
, valueSpec
, spec
, genDecl
, file
など、ASTノードを処理して整形するほぼすべての主要な関数がmultiLine *bool
というポインタ型のブーリアンパラメータを受け取っていました。
このパラメータの目的は、現在処理しているASTノード(またはその一部)がソースコード上で複数行にわたる場合に、呼び出し元にその情報を伝えることでした。例えば、exprList
が複数行にわたる式リストを整形した場合、multiLine
ポインタが指す値をtrue
に設定し、その情報がさらに上位の関数に伝播されることで、適切な改行やインデントが適用される仕組みでした。
しかし、このアプローチには以下の問題がありました。
- APIの複雑化: 多くの関数シグネチャに冗長な
multiLine *bool
パラメータが存在し、コードの可読性と保守性を低下させていました。 - 状態の引き回し:
multiLine
の状態を関数呼び出しスタックを通じて明示的に引き回す必要があり、ロジックが複雑になりがちでした。 - ポインタの利用: ブーリアン値をポインタで渡すことで、値渡しに比べてわずかなオーバーヘッドが生じる可能性がありました(ただし、これはパフォーマンス上の大きな問題ではなかったでしょう)。
新しいアプローチ:位置情報からの推論
コミットメッセージが述べているように、「既存の位置情報を使用するだけで、新しいセクションが必要かどうかを判断するのに十分」であるという考えに基づき、multiLine
パラメータは削除されました。
新しいアプローチでは、printer
構造体にisMultiLine
という新しいヘルパーメソッドが追加されました。このメソッドはast.Node
を受け取り、そのノードがソースコード上で複数行にわたるかどうかを、ノードの開始位置 (n.Pos()
) と終了位置 (n.End()
) の行番号を比較することで判断します。
func (p *printer) isMultiLine(n ast.Node) bool {
return p.lineFor(n.End())-p.lineFor(n.Pos()) > 1
}
p.lineFor(pos token.Pos)
: このprinter
メソッドは、与えられたtoken.Pos
に対応する行番号を返します。p.lineFor(n.End()) - p.lineFor(n.Pos()) > 1
: ノードの終了位置の行番号から開始位置の行番号を引いた結果が1より大きい場合、そのノードは複数行にわたると判断されます。例えば、開始が1行目、終了が2行目であれば差は1ですが、これは2行にわたることを意味します。差が0であれば単一行、差が1であれば2行にわたる、というように解釈できます。したがって、> 1
は3行以上、または開始行と終了行を含めて2行以上を意味します。正確には、p.lineFor(n.End()) > p.lineFor(n.Pos())
であれば複数行と判断できます。コミットのコードでは> 1
となっているため、厳密には3行以上の場合にtrue
を返すことになりますが、これはgo/printer
の内部的な整形ロジックの要件に合わせたものと考えられます。
このisMultiLine
メソッドの導入により、各関数はmultiLine
フラグを明示的に受け渡す必要がなくなり、必要に応じて自身の内部でisMultiLine
を呼び出すことで、現在のノードが複数行かどうかを判断できるようになりました。
ignoreMultiLine
変数の削除
src/pkg/go/printer/printer.go
ファイルには、ignoreMultiLine
というグローバル変数が存在していました。これは、multiLine
情報が不要な場合にダミーとして渡すための*bool
ポインタでした。multiLine
パラメータが削除されたことで、このダミー変数も不要となり、削除されました。
gofmt
への影響とテストケースの改善
この変更は、gofmt
の出力に直接的な影響を与えました。コミットメッセージには「Applied gofmt -w src misc resulting in only a minor change to godoc.go. In return, a couple of test cases are now formatted better.」とあります。これは、gofmt
を適用した結果、godoc.go
にわずかな変更があったものの、いくつかのテストケース(特にdeclarations.golden
のような整形結果を比較するテスト)では、より適切なフォーマットが適用されるようになったことを示しています。これは、新しい複数行検出ロジックが、より正確な整形を可能にした結果と言えます。
コアとなるコードの変更箇所
このコミットでは、主にsrc/pkg/go/printer/nodes.go
とsrc/pkg/go/printer/printer.go
の2つのファイルが大きく変更されています。
src/pkg/go/printer/nodes.go
-
isMultiLine
関数の追加:func (p *printer) isMultiLine(n ast.Node) bool { return p.lineFor(n.End())-p.lineFor(n.Pos()) > 1 }
この関数が、ASTノードが複数行にわたるかを判断する新しいロジックの中核です。
-
多数の関数シグネチャからの
multiLine *bool
パラメータの削除: 以下は変更された関数の一部です。multiLine *bool
引数が削除され、それに伴い関数内部での*multiLine = true
のような代入も削除されています。func (p *printer) identList(list []*ast.Ident, indent bool, multiLine *bool)
->func (p *printer) identList(list []*ast.Ident, indent bool)
func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exprListMode, multiLine *bool, next0 token.Pos)
->func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exprListMode, next0 token.Pos)
func (p *printer) parameters(fields *ast.FieldList, multiLine *bool)
->func (p *printer) parameters(fields *ast.FieldList)
func (p *printer) signature(params, result *ast.FieldList, multiLine *bool)
->func (p *printer) signature(params, result *ast.FieldList)
func (p *printer) binaryExpr(x *ast.BinaryExpr, prec1, cutoff, depth int, multiLine *bool)
->func (p *printer) binaryExpr(x *ast.BinaryExpr, prec1, cutoff, depth int)
func (p *printer) expr1(expr ast.Expr, prec1, depth int, multiLine *bool)
->func (p *printer) expr1(expr ast.Expr, prec1, depth int)
func (p *printer) expr0(x ast.Expr, depth int, multiLine *bool)
->func (p *printer) expr0(x ast.Expr, depth int)
func (p *printer) expr(x ast.Expr, multiLine *bool)
->func (p *printer) expr(x ast.Expr)
func (p *printer) stmt(stmt ast.Stmt, nextIsRBrace bool, multiLine *bool)
->func (p *printer) stmt(stmt ast.Stmt, nextIsRBrace bool)
func (p *printer) decl(decl ast.Decl, multiLine *bool)
->func (p *printer) decl(decl ast.Decl)
func (p *printer) funcDecl(d *ast.FuncDecl, multiLine *bool)
->func (p *printer) funcDecl(d *ast.FuncDecl)
func (p *printer) funcBody(b *ast.BlockStmt, headerSize int, isLit bool, multiLine *bool)
->func (p *printer) funcBody(b *ast.BlockStmt, headerSize int, isLit bool)
func (p *printer) valueSpec(s *ast.ValueSpec, keepType, doIndent bool, multiLine *bool)
->func (p *printer) valueSpec(s *ast.ValueSpec, keepType, doIndent bool)
func (p *printer) spec(spec ast.Spec, n int, doIndent bool, multiLine *bool)
->func (p *printer) spec(spec ast.Spec, n int, doIndent bool)
func (p *printer) genDecl(d *ast.GenDecl, multiLine *bool)
->func (p *printer) genDecl(d *ast.GenDecl)
func (p *printer) file(src *ast.File)
:src.Name
を処理するp.expr
呼び出しからignoreMultiLine
が削除。
-
stmtList
関数におけるmultiLine
の利用方法の変更:// 変更前 var multiLine bool for i, s := range list { p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || _indent == 0 || multiLine) multiLine = false p.stmt(s, nextIsRBrace && i == len(list)-1, &multiLine) } // 変更後 multiLine := false // multiLineはローカル変数として初期化 for i, s := range list { p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || _indent == 0 || multiLine) // multiLineは次のセクションの開始を決定するために使用される p.stmt(s, nextIsRBrace && i == len(list)-1) // multiLine引数が削除された multiLine = p.isMultiLine(s) // 新しいisMultiLineを使って更新 }
ここでは、
multiLine
がローカル変数として扱われ、各ステートメントの処理後にp.isMultiLine(s)
を呼び出して更新されることで、次のステートメントの整形に影響を与えるようになっています。
src/pkg/go/printer/printer.go
-
ignoreMultiLine
変数の削除:// 削除されたコード // Use ignoreMultiLine if the multiLine information is not important. // var ignoreMultiLine = new(bool)
この変数は、
multiLine *bool
パラメータが不要な場合に渡すためのダミーでしたが、パラメータ自体が削除されたため不要になりました。 -
printNode
関数におけるignoreMultiLine
の利用箇所の変更:p.expr(n, ignoreMultiLine)
->p.expr(n)
p.stmt(n, false, ignoreMultiLine)
->p.stmt(n, false)
p.decl(n, ignoreMultiLine)
->p.decl(n)
p.spec(n, 1, false, ignoreMultiLine)
->p.spec(n, 1, false)
src/cmd/godoc/godoc.go
- フラグの定義におけるインデントの微調整。これは
gofmt
の適用による副次的な変更であり、本質的なロジック変更ではありません。
src/pkg/go/printer/testdata/declarations.golden
- 整形結果のテストデータがわずかに変更されています。これは、新しい整形ロジックによって出力が改善されたことを示しています。例えば、
int
やfloat
の前のタブの数が変わっています。
コアとなるコードの解説
このコミットのコアとなる変更は、go/printer
パッケージがコードの複数行の状態をどのように扱うかという根本的なアプローチの変更です。
isMultiLine
関数の役割
新しく導入されたfunc (p *printer) isMultiLine(n ast.Node) bool
関数は、この変更の要です。この関数は、与えられたASTノードn
がソースコード上で複数行にわたるかどうかを、ノードの開始位置 (n.Pos()
) と終了位置 (n.End()
) を比較することで判断します。
具体的には、p.lineFor(pos token.Pos)
というヘルパーメソッドを使って、各位置の行番号を取得し、p.lineFor(n.End()) - p.lineFor(n.Pos()) > 1
という条件で複数行かどうかを判定します。このシンプルな比較により、明示的なmultiLine
フラグを関数間で引き回す必要がなくなりました。
multiLine
パラメータの削除による簡素化
以前は、multiLine *bool
というポインタが多くの関数シグネチャに存在し、その値を更新することで複数行の状態を伝播させていました。例えば、p.expr(x, multiLine)
のように呼び出し、expr
関数内で*multiLine = true
と設定していました。
この変更により、これらのパラメータが削除されたことで、関数シグネチャが大幅に簡素化されました。例えば、p.expr(x)
のように、よりクリーンな呼び出しが可能になりました。これにより、go/printer
パッケージの内部APIがより理解しやすくなり、将来的な開発やデバッグが容易になります。
stmtList
におけるmultiLine
の新しい使い方
stmtList
関数(ステートメントのリストを整形する関数)では、multiLine
というローカル変数が導入され、各ステートメントの整形後にp.isMultiLine(s)
を呼び出してその値が更新されるようになりました。
multiLine := false // multiLineはローカル変数として初期化
for i, s := range list {
// linebreakの第4引数 (newSection) に multiLine を渡す
p.linebreak(p.lineFor(s.Pos()), 1, ignore, i == 0 || _indent == 0 || multiLine)
p.stmt(s, nextIsRBrace && i == len(list)-1)
multiLine = p.isMultiLine(s) // 現在のステートメントが複数行なら multiLine を true に更新
}
ここでlinebreak
関数の第4引数newSection
は、新しいセクション(例えば、新しいステートメントや宣言)が開始される際に、強制的に改行を挿入するかどうかを制御します。以前はmultiLine
パラメータがこの情報を提供していましたが、新しい実装では、直前のステートメントが複数行にわたっていた場合にmultiLine
がtrue
となり、次のステートメントの開始時にnewSection
として機能するようになっています。これにより、コードの論理的な区切りがより適切に表現されるようになります。
この変更は、go/printer
の内部ロジックをより洗練させ、ASTノードの構造と位置情報から整形ルールをより自然に導き出すことを可能にしました。
関連リンク
- Go Gerrit Change-ID:
5706055
参考にした情報源リンク
- Go言語の公式ドキュメント:
go/ast
パッケージ: https://pkg.go.dev/go/astgo/token
パッケージ: https://pkg.go.dev/go/tokengo/printer
パッケージ: https://pkg.go.dev/go/printer
gofmt
の設計思想に関する情報(一般的なコードフォーマッタの原則理解のため)