[インデックス 12540] ファイルの概要
このコミットは、Go言語のコードフォーマッタであるgofmt
と、その基盤となるgo/printer
パッケージにおける、複数行にわたるreturn
文のフォーマット改善に関するものです。特に、複数の戻り値が複数行にわたる場合に、より見やすく整形されるように変更が加えられました。
コミット
commit 49d2d986978f3b3654ff284fbcbeb4c32ec55fad
Author: Robert Griesemer <gri@golang.org>
Date: Fri Mar 9 11:05:50 2012 -0800
go/printer, gofmt: nicer formatting of multi-line returns
This affects corner (test) cases only; gofmt -w src misc
doesn't cause any changes.
- added additional test cases
- removed doIndent parameter from printer.valueSpec
(was always false)
- gofmt -w src misc causes no changes
Fixes #1207.
R=dsymonds, rsc
CC=golang-dev
https://golang.org/cl/5786060
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/49d2d986978f3b3654ff284fbcbeb4c32ec55fad
元コミット内容
このコミットの目的は、go/printer
パッケージとgofmt
ツールにおいて、複数行にわたるreturn
文のフォーマットを改善することです。具体的には、複数の戻り値が複数行にまたがるような特殊なケース(主にテストケース)において、より適切なインデントが適用されるように修正されました。
変更点としては、以下の点が挙げられています。
- 追加のテストケースが追加された。
printer.valueSpec
関数からdoIndent
パラメータが削除された(常にfalse
であったため)。gofmt -w src misc
を実行しても、既存のコードベースに大きな変更は発生しないことが確認された。
この変更は、Go言語のIssue 1207を修正するものです。
変更の背景
このコミットは、Go言語のIssue 1207「go/printer: odd layout of multiple return values that span lines」に対応するものです。このIssueは、gofmt
が複数行にわたるreturn
文の戻り値を整形する際に、期待されるインデントが適用されず、読みにくいレイアウトになるという問題点を指摘していました。
具体的には、以下のようなコードが問題とされていました。
func F() (*T, os.Error) {
return &T{
X: 1,
Y: 2,
},
nil
}
この場合、nil
が&T{...}
と同じインデントレベルになってしまい、視覚的に戻り値のリストがどこから始まるのか、どこで終わるのかが分かりにくいという問題がありました。このコミットは、このようなケースにおいて、より適切なインデントを適用し、コードの可読性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語のツールと概念に関する知識が必要です。
go/printer
パッケージ: Go言語のソースコードを整形(フォーマット)するための低レベルな機能を提供するパッケージです。抽象構文木(AST)を受け取り、整形されたコードを出力します。gofmt
はこのパッケージを利用して実装されています。gofmt
: Go言語の公式なコードフォーマッタです。Goのコードを標準的なスタイルに自動的に整形します。これにより、Goコミュニティ全体で一貫したコードスタイルが保たれ、コードの可読性が向上します。gofmt
は、コードの構文解析を行い、ASTを構築し、それをgo/printer
パッケージに渡して整形されたコードを生成します。- 抽象構文木 (AST: Abstract Syntax Tree): ソースコードの構造を木構造で表現したものです。コンパイラやリンタ、フォーマッタなどのツールは、ソースコードを直接操作するのではなく、一度ASTに変換してから処理を行います。
- インデント: コードの階層構造を視覚的に表現するために、行頭に空白(スペースやタブ)を入れることです。適切なインデントはコードの可読性を大きく向上させます。
return
文: 関数の実行を終了し、呼び出し元に値を返すためのGo言語のキーワードです。Go言語では、複数の値を返すことができます。
技術的詳細
このコミットの主要な変更は、go/printer
パッケージ内のstmt
メソッド(特にtoken.RETURN
の場合)と、新しく導入されたindentList
ヘルパー関数にあります。
indentList
関数の導入
indentList
関数は、与えられたast.Expr
のリスト(この場合はreturn
文の戻り値のリスト)が、全体的にインデントされるべきかどうかをヒューリスティックに基づいて判断します。
- 目的: 複数行にわたる戻り値のリストが、より見やすく整形されるべきかを判定する。
- ヒューリスティック:
- リストに2つ以上の要素がある場合。
- リストが複数行にわたる場合。
- リスト内の要素が、前の要素の終了行と同じ行から始まっていない場合(つまり、要素間に改行がある場合)。
- リスト内に複数の複数行要素がある場合。
これらの条件のいずれかが満たされる場合、indentList
はtrue
を返し、そのリスト全体をインデントする必要があることを示します。
stmt
メソッドにおけるreturn
文の処理変更
printer.stmt
メソッドは、Goの各ステートメント(文)を整形する役割を担っています。このコミットでは、return
文の処理が以下のように変更されました。
変更前:
case *ast.ReturnStmt:
p.print(token.RETURN)
if s.Results != nil {
p.print(blank)
p.exprList(s.Pos(), s.Results, 1, 0, token.NoPos)
}
変更後:
case *ast.ReturnStmt:
p.print(token.RETURN)
if s.Results != nil {
p.print(blank)
// Use indentList heuristic to make corner cases look
// better (issue 1207). A more systematic approach would
// always indent, but this would cause significant
// reformatting of the code base and not necessarily
// lead to more nicely formatted code in general.
if p.indentList(s.Results) {
p.print(indent)
p.exprList(s.Pos(), s.Results, 1, noIndent, token.NoPos)
p.print(unindent)
} else {
p.exprList(s.Pos(), s.Results, 1, 0, token.NoPos)
}
}
この変更により、return
文の戻り値リストを整形する際に、まずindentList
関数が呼び出されます。
indentList
がtrue
を返した場合、p.print(indent)
でインデントを開始し、p.exprList
で戻り値リストを整形した後、p.print(unindent)
でインデントを解除します。これにより、戻り値リスト全体が一段階インデントされて出力されます。indentList
がfalse
を返した場合、以前と同様にインデントなしでp.exprList
が呼び出されます。
この条件付きインデントの導入により、Issue 1207で指摘されたような、複数行にわたる戻り値のレイアウト問題が解決されました。コミットメッセージにもあるように、より体系的なアプローチとして常にインデントすることも考えられますが、それは既存のコードベースに大規模な再フォーマットを引き起こすため、ヒューリスティックに基づいたアプローチが採用されました。
valueSpec
関数の変更
printer.valueSpec
関数からdoIndent
パラメータが削除されました。これは、このパラメータが常にfalse
として渡されていたため、冗長であったためです。この変更は、コードのクリーンアップと簡素化を目的としています。
変更前:
func (p *printer) valueSpec(s *ast.ValueSpec, keepType, doIndent bool) {
p.setComment(s.Doc)
p.identList(s.Names, doIndent) // always present
変更後:
func (p *printer) valueSpec(s *ast.ValueSpec, keepType bool) {
p.setComment(s.Doc)
p.identList(s.Names, false) // always present
そして、genDecl
関数での呼び出しも変更されています。
変更前:
p.valueSpec(s.(*ast.ValueSpec), keepType[i], false)
変更後:
p.valueSpec(s.(*ast.ValueSpec), keepType[i])
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は、主に以下の2つのファイルに集中しています。
-
src/pkg/go/printer/nodes.go
:indentList
関数の新規追加。stmt
メソッド内の*ast.ReturnStmt
ケースにおける、indentList
を用いた条件付きインデントロジックの追加。valueSpec
関数のシグネチャ変更と、それに伴うidentList
呼び出しの修正。genDecl
関数内のvalueSpec
呼び出しの修正。
-
src/pkg/go/printer/testdata/statements.golden
およびsrc/pkg/go/printer/testdata/statements.input
:return
文の新しいフォーマットを検証するためのテストケースの追加と、それに対応する期待される出力(.golden
ファイル)の更新。
コアとなるコードの解説
src/pkg/go/printer/nodes.go
indentList
関数
func (p *printer) indentList(list []ast.Expr) bool {
// Heuristic: indentList returns true if there are more than one multi-
// line element in the list, or if there is any element that is not
// starting on the same line as the previous one ends.
if len(list) >= 2 {
var b = p.lineFor(list[0].Pos())
var e = p.lineFor(list[len(list)-1].End())
if 0 < b && b < e {
// list spans multiple lines
n := 0 // multi-line element count
line := b
for _, x := range list {
xb := p.lineFor(x.Pos())
xe := p.lineFor(x.End())
if line < xb {
// x is not starting on the same
// line as the previous one ended
return true
}
if xb < xe {
// x is a multi-line element
n++
}
line = xe
}
return n > 1
}
}
return false
}
この関数は、ast.Expr
のスライス(list
)を受け取り、そのリストが複数行にわたる場合に、全体をインデントすべきかどうかを判断します。
- リストの要素が2つ以上あり、かつリスト全体が複数行にわたる場合に処理を進めます。
- 各要素について、開始行と終了行を取得し、前の要素の終了行と比較します。
- もし要素が前の要素の終了行と同じ行から始まっていない場合(つまり、要素間に改行がある場合)、即座に
true
を返します。 - また、複数行にわたる要素の数をカウントし、それが1より大きい場合(つまり、2つ以上の複数行要素がある場合)も
true
を返します。 - これらの条件に合致しない場合は
false
を返します。
stmt
メソッド内のreturn
文処理
case *ast.ReturnStmt:
p.print(token.RETURN)
if s.Results != nil {
p.print(blank)
// Use indentList heuristic to make corner cases look
// better (issue 1207).
if p.indentList(s.Results) {
p.print(indent)
p.exprList(s.Pos(), s.Results, 1, noIndent, token.NoPos)
p.print(unindent)
} else {
p.exprList(s.Pos(), s.Results, 1, 0, token.NoPos)
}
}
この部分が、return
文の戻り値の整形ロジックの核心です。
return
キーワードを出力した後、戻り値がある場合に処理を進めます。p.indentList(s.Results)
を呼び出し、戻り値のリストをインデントすべきかを判断します。indentList
がtrue
を返した場合、p.print(indent)
で現在のインデントレベルを一段階増やし、p.exprList
で戻り値の式リストを整形します。整形後、p.print(unindent)
でインデントレベルを元に戻します。これにより、戻り値全体がインデントされたブロックとして出力されます。indentList
がfalse
を返した場合は、従来のインデントなしのp.exprList
呼び出しが行われます。
src/pkg/go/printer/testdata/statements.golden
および src/pkg/go/printer/testdata/statements.input
これらのファイルは、gofmt
のテストスイートの一部です。
.input
ファイルは、gofmt
に与えられる整形前のコードスニペットを含みます。.golden
ファイルは、.input
ファイルがgofmt
によって整形された後の期待される出力を含みます。
このコミットでは、Issue 1207で問題とされた複数行のreturn
文のケースが追加され、それらが適切にインデントされるように.golden
ファイルが更新されています。これにより、gofmt
が意図した通りに動作することを保証しています。
関連リンク
- Go言語のIssue 1207: https://github.com/golang/go/issues/1207
- Go言語の
go/printer
パッケージのドキュメント: https://pkg.go.dev/go/printer - Go言語の
gofmt
コマンドのドキュメント: https://go.dev/blog/gofmt
参考にした情報源リンク
- GitHub: golang/go repository (特にコミット履歴とIssueトラッカー)
- Go言語公式ドキュメント
- Go言語のソースコード