Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 11381] ファイルの概要

このコミットは、Go言語の公式フォーマッタであるgofmtと、その基盤となるgo/printerパッケージの挙動を改善するものです。具体的には、関数シグネチャ(引数リスト)内の既存の改行を尊重するように変更が加えられました。これにより、開発者が意図的にシグネチャ内で改行を入れている場合に、gofmtがそれらを削除して一行に整形してしまう問題が解消されます。

コミット

commit d665ea98f37ce556690f14a58b2f90032bd3a9d0
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Jan 25 10:21:13 2012 -0800

    go/printer, gofmt: respect line breaks in signatures
    
    No changes when applying gofmt to src, misc.
    
    Fixes #2597.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/5564056
---
 src/pkg/go/printer/nodes.go                     | 44 ++++++++++++++-------
 src/pkg/go/printer/testdata/declarations.golden | 50 ++++++++++++++++--------
 src/pkg/go/printer/testdata/linebreaks.golden   | 52 +++++++++++++++++++++++++
 src/pkg/go/printer/testdata/linebreaks.input    | 48 +++++++++++++++++++++++
 4 files changed, 164 insertions(+), 30 deletions(-)

diff --git a/src/pkg/go/printer/nodes.go b/src/pkg/go/printer/nodes.go
index 6817cc42ad..0f4e72b5f1 100644
--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -272,23 +272,32 @@ func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exp
 func (p *printer) parameters(fields *ast.FieldList, multiLine *bool) {
 	p.print(fields.Opening, token.LPAREN)
 	if len(fields.List) > 0 {
+		prevLine := p.fset.Position(fields.Opening).Line
 		ws := indent
-		var prevLine, line int
 		for i, par := range fields.List {
+			// determine par begin and end line (may be different
+			// if there are multiple parameter names for this par
+			// or the type is on a separate line)
+			var parLineBeg int
+			var parLineEnd = p.fset.Position(par.Type.Pos()).Line
+			if len(par.Names) > 0 {
+				parLineBeg = p.fset.Position(par.Names[0].Pos()).Line
+			} else {
+				parLineBeg = parLineEnd
+			}
+			// separating "," if needed
 			if i > 0 {
 				p.print(token.COMMA)
-				if len(par.Names) > 0 {
-					line = p.fset.Position(par.Names[0].Pos()).Line
-				} else {
-					line = p.fset.Position(par.Type.Pos()).Line
-				}
-				if 0 < prevLine && prevLine < line && p.linebreak(line, 0, ws, true) {
-					ws = ignore
-					*multiLine = true
-				} else {
-					p.print(blank)
-				}
 			}
+			// separator if needed (linebreak or blank)
+			if 0 < prevLine && prevLine < parLineBeg && p.linebreak(parLineBeg, 0, ws, true) {
+				// break line if the opening "(" or previous parameter ended on a different line
+				ws = ignore
+				*multiLine = true
+			} else if i > 0 {
+				p.print(blank)
+			}
+			// parameter names
 			if len(par.Names) > 0 {
 				// Very subtle: If we indented before (ws == ignore), identList
 				// won't indent again. If we didn't (ws == indent), identList will
@@ -299,11 +308,18 @@ func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exp
 				p.identList(par.Names, ws == indent, multiLine)
 				p.print(blank)
 			}
+			// parameter type
 			p.expr(par.Type, multiLine)
-			prevLine = p.fset.Position(par.Type.Pos()).Line
+			prevLine = parLineEnd
 		}
+		// if the closing ")" is on a separate line from the last parameter,
+		// print an additional "," and line break
+		if closing := p.fset.Position(fields.Closing).Line; 0 < prevLine && prevLine < closing {
+			p.print(",")
+			p.linebreak(closing, 0, ignore, true)
+		}
+		// unindent if we indented
 		if ws == ignore {
-			// unindent if we indented
 			p.print(unindent)
 		}
 	}
diff --git a/src/pkg/go/printer/testdata/declarations.golden b/src/pkg/go/printer/testdata/declarations.golden
index 239ba89030..928b8ce0a9 100644
--- a/src/pkg/go/printer/testdata/declarations.golden
+++ b/src/pkg/go/printer/testdata/declarations.golden
@@ -773,30 +773,39 @@ func ManageStatus(in <-chan *Status, req <-chan Request,
 	TargetHistorySize int) {
 }
 
-func MultiLineSignature0(a, b, c int) {
+func MultiLineSignature0(
+	a, b, c int,
+) {
 }
 
-func MultiLineSignature1(a, b, c int,
-	u, v, w float) {
+func MultiLineSignature1(
+	a, b, c int,
+	u, v, w float,
+) {
 }
 
-func MultiLineSignature2(a, b,
-	c int) {
+func MultiLineSignature2(
+	a, b,
+	c int,
+) {
 }
 
-func MultiLineSignature3(a, b,
+func MultiLineSignature3(
+	a, b,
 	c int, u, v,
 	w float,
 	x ...int) {
 }
 
-func MultiLineSignature4(a, b, c int,
+func MultiLineSignature4(
+	a, b, c int,
 	u, v,
 	w float,
 	x ...int) {
 }
 
-func MultiLineSignature5(a, b, c int,
+func MultiLineSignature5(
+	a, b, c int,
 	u, v, w float,
 	p, q,
 	r string,
@@ -805,25 +814,34 @@ func MultiLineSignature5(a, b, c int,
 
 // make sure it also works for methods in interfaces
 type _ interface {
-	MultiLineSignature0(a, b, c int)
+	MultiLineSignature0(
+		a, b, c int,
+	)
 
-	MultiLineSignature1(a, b, c int,
-		u, v, w float)
+	MultiLineSignature1(
+		a, b, c int,
+		u, v, w float,
+	)
 
-	MultiLineSignature2(a, b,
-		c int)
+	MultiLineSignature2(
+		a, b,
+		c int,
+	)
 
-	MultiLineSignature3(a, b,
+	MultiLineSignature3(
+		a, b,
 		c int, u, v,
 		w float,
 		x ...int)
 
-	MultiLineSignature4(a, b, c int,
+	MultiLineSignature4(
+		a, b, c int,
 		u, v,
 		w float,
 		x ...int)
 
-	MultiLineSignature5(a, b, c int,
+	MultiLineSignature5(
+		a, b, c int,
 		u, v, w float,
 		p, q,
 		r string,
diff --git a/src/pkg/go/printer/testdata/linebreaks.golden b/src/pkg/go/printer/testdata/linebreaks.golden
index be780da677..006cf17184 100644
--- a/src/pkg/go/printer/testdata/linebreaks.golden
+++ b/src/pkg/go/printer/testdata/linebreaks.golden
@@ -220,4 +220,56 @@ testLoop:\n 	}\n }\n \n+// Respect line breaks in function calls.\n+func _() {\n+\tf(x)\n+\tf(x,\n+\t\tx)\n+\tf(x,\n+\t\tx,\n+\t)\n+\tf(\n+\t\tx,\n+\t\tx)\n+\tf(\n+\t\tx,\n+\t\tx,\n+\t)\n+}\n+\n+// Respect line breaks in function declarations.\n+func _(x T)\t{}\n+func _(x T,\n+\ty T) {\n+}\n+func _(x T,\n+\ty T,\n+) {\n+}\n+func _(\n+\tx T,\n+\ty T) {\n+}\n+func _(\n+\tx T,\n+\ty T,\n+) {\n+}\n+\n+// Example from issue 2597.\n+func ManageStatus0(\n+\tin <-chan *Status,\n+\treq <-chan Request,\n+\tstat chan<- *TargetInfo,\n+\tTargetHistorySize int) {\n+}\n+\n+func ManageStatus1(\n+\tin <-chan *Status,\n+\treq <-chan Request,\n+\tstat chan<- *TargetInfo,\n+\tTargetHistorySize int,\n+) {\n+}\n+\n // There should be exactly one linebreak after this comment.\ndiff --git a/src/pkg/go/printer/testdata/linebreaks.input b/src/pkg/go/printer/testdata/linebreaks.input
index 457b491e6d..e782bb0444 100644
--- a/src/pkg/go/printer/testdata/linebreaks.input
+++ b/src/pkg/go/printer/testdata/linebreaks.input
@@ -220,4 +220,52 @@ testLoop:\n 	}\n }\n \n+// Respect line breaks in function calls.\n+func _() {\n+\tf(x)\n+\tf(x,\n+\t  x)\n+\tf(x,\n+\t  x,\n+\t)\n+\tf(\n+\t  x,\n+\t  x)\n+\tf(\n+\t  x,\n+\t  x,\n+\t)\n+}\n+\n+// Respect line breaks in function declarations.\n+func _(x T) {}\n+func _(x T,\n+       y T) {}\n+func _(x T,\n+       y T,\n+) {}\n+func _(\n+       x T,\n+       y T) {}\n+func _(\n+       x T,\n+       y T,\n+) {}\n+\n+// Example from issue 2597.\n+func ManageStatus0(\n+\tin <-chan *Status,\n+\treq <-chan Request,\n+\tstat chan<- *TargetInfo,\n+\tTargetHistorySize int) {\n+}\n+    \n+func ManageStatus1(\n+\tin <-chan *Status,\n+\treq <-chan Request,\n+\tstat chan<- *TargetInfo,\n+\tTargetHistorySize int,\n+) {\n+}\n+    \n // There should be exactly one linebreak after this comment.\n```

## GitHub上でのコミットページへのリンク

[https://github.com/golang/go/commit/d665ea98f37ce556690f14a58b2f90032bd3a9d0](https://github.com/golang/go/commit/d665ea98b37ce556690f14a58b2f90032bd3a9d0)

## 元コミット内容

go/printer, gofmt: respect line breaks in signatures

No changes when applying gofmt to src, misc.

Fixes #2597.

R=r CC=golang-dev https://golang.org/cl/5564056


## 変更の背景

このコミットは、Go言語のコードフォーマッタである`gofmt`が、関数やメソッドのシグネチャ(引数や戻り値のリスト)内で開発者が明示的に挿入した改行を無視し、自動的に整形してしまう問題を解決するために導入されました。

Goの`gofmt`は、コードのスタイルを統一し、可読性を高めるための非常に強力なツールです。しかし、その厳格な整形ルールが、特定の状況下で開発者の意図と異なる結果を生むことがありました。特に、引数の数が多い関数や、引数の型が長い場合など、可読性を向上させるために手動で改行を入れることがあります。以前の`gofmt`は、このような改行を「不要な空白」とみなし、整形時に削除して一行にまとめてしまう傾向がありました。

コミットメッセージにある`Fixes #2597`は、この問題がGoのIssueトラッカーで報告されていたことを示しています。開発者は、`gofmt`がコードのセマンティクスを変更しないだけでなく、開発者の意図したレイアウト(特に改行による視覚的な区切り)も可能な限り尊重することを期待していました。このコミットは、その期待に応えるための改善です。

## 前提知識の解説

### `gofmt`
`gofmt`は、Go言語のソースコードを自動的に整形するためのコマンドラインツールです。Go言語の標準的なコーディングスタイルを強制することで、Goコミュニティ全体で一貫したコードベースを維持し、可読性を向上させることを目的としています。インデント、空白、コメントの配置などを自動的に調整します。

### `go/printer`パッケージ
`go/printer`パッケージは、Goの標準ライブラリの一部であり、Goの抽象構文木(AST: Abstract Syntax Tree)を「整形(pretty-print)」してGoのソースコードとして出力する機能を提供します。`gofmt`ツールは、この`go/printer`パッケージを内部的に利用してコードの整形を行っています。つまり、`gofmt`の整形ロジックの大部分は`go/printer`パッケージに実装されています。

### 抽象構文木(AST: Abstract Syntax Tree)
ASTは、ソースコードの構造を木構造で表現したものです。Goコンパイラは、ソースコードを解析する際にまずASTを構築します。`go/printer`パッケージは、このASTを受け取り、それを基に整形されたソースコードを生成します。

### 関数シグネチャ
Go言語における関数シグネチャは、関数の名前、引数(パラメータ)のリスト、および戻り値のリストで構成されます。例えば、`func (a int, b string) (bool, error)`という関数シグネチャでは、`a int, b string`が引数リスト、`bool, error`が戻り値リストです。このコミットは、特に引数リスト内の改行の扱いに焦点を当てています。

## 技術的詳細

このコミットの主要な変更は、`src/pkg/go/printer/nodes.go`ファイル内の`parameters`関数にあります。この関数は、GoのASTノードから関数やメソッドのパラメータリストを整形して出力する役割を担っています。

変更の核心は、パラメータ間の改行を検出・尊重するためのロジックの追加と修正です。

1.  **`prevLine`の初期化の変更**:
    *   変更前は、`prevLine`はループ内で初期化されていましたが、変更後は`p.fset.Position(fields.Opening).Line`、つまりパラメータリストの開始括弧`(`の行で初期化されるようになりました。これにより、最初のパラメータの前に改行がある場合も検出できるようになります。

2.  **パラメータの開始行と終了行の正確な特定**:
    *   `parLineBeg`と`parLineEnd`という新しい変数が導入されました。
    *   `parLineBeg`は、現在のパラメータの最初の名前(`par.Names[0].Pos()`)または型(`par.Type.Pos()`)の開始行を正確に取得します。これは、複数のパラメータ名がある場合や、型が別の行にある場合に重要です。
    *   `parLineEnd`は、パラメータの型の終了行(`par.Type.Pos().Line`)を特定します。
    *   これにより、単一のパラメータ宣言が複数行にまたがる場合でも、そのパラメータ全体の開始と終了の行を正確に把握できるようになりました。

3.  **改行と空白の挿入ロジックの改善**:
    *   各パラメータの前にカンマを挿入するロジックはそのままですが、その後の空白または改行の挿入ロジックが大きく変更されました。
    *   `if 0 < prevLine && prevLine < parLineBeg && p.linebreak(parLineBeg, 0, ws, true)`:
        *   この条件は、`prevLine`(直前の要素の行)と`parLineBeg`(現在のパラメータの開始行)が異なる場合に真となります。つまり、直前の要素と現在のパラメータの間に改行が存在することを示します。
        *   `p.linebreak`関数を呼び出し、強制的に改行を挿入します。
        *   `ws = ignore`を設定することで、`go/printer`が自動的に挿入するデフォルトの空白を抑制し、明示的な改行が優先されるようにします。
        *   `*multiLine = true`を設定することで、パラメータリストが複数行にわたることを示します。
    *   `else if i > 0`: 上記の条件が偽で、かつ最初のパラメータでない場合(つまり、同じ行に続くパラメータの場合)、通常の空白を挿入します。

4.  **閉じ括弧前の改行の尊重**:
    *   パラメータリストのループが終了した後、新しいロジックが追加されました。
    *   `if closing := p.fset.Position(fields.Closing).Line; 0 < prevLine && prevLine < closing`:
        *   この条件は、最後のパラメータの終了行(`prevLine`)と、パラメータリストの閉じ括弧`)`の行(`closing`)が異なる場合に真となります。
        *   これは、最後のパラメータの後に改行があり、閉じ括弧が新しい行にある場合に該当します。
        *   この場合、追加のカンマ(Goでは最後の要素の後にカンマを置くことが許容され、複数行リストでは推奨される)と強制的な改行が挿入されます。これにより、以下のような整形が可能になります。
            ```go
            func foo(
                param1 Type1,
                param2 Type2, // 最後のカンマ
            ) { // 閉じ括弧が新しい行
            }
            ```

これらの変更により、`go/printer`は、開発者が意図的に挿入した関数シグネチャ内の改行を「意味のあるもの」として認識し、整形時にそれらを維持するようになりました。

## コアとなるコードの変更箇所

`src/pkg/go/printer/nodes.go`ファイルの`parameters`関数における変更がコアとなります。

```diff
--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -272,23 +272,32 @@ func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exp
 func (p *printer) parameters(fields *ast.FieldList, multiLine *bool) {
 	p.print(fields.Opening, token.LPAREN)
 	if len(fields.List) > 0 {
+		prevLine := p.fset.Position(fields.Opening).Line // 変更点1: prevLineの初期化
 		ws := indent
-		var prevLine, line int // 削除
 		for i, par := range fields.List {
+			// determine par begin and end line (may be different
+			// if there are multiple parameter names for this par
+			// or the type is on a separate line)
+			var parLineBeg int // 変更点2: parLineBegの導入
+			var parLineEnd = p.fset.Position(par.Type.Pos()).Line // 変更点3: parLineEndの導入
+			if len(par.Names) > 0 {
+				parLineBeg = p.fset.Position(par.Names[0].Pos()).Line
+			} else {
+				parLineBeg = parLineEnd
+			}
+			// separating "," if needed
 			if i > 0 {
 				p.print(token.COMMA)
-				if len(par.Names) > 0 { // 削除
-					line = p.fset.Position(par.Names[0].Pos()).Line // 削除
-				} else { // 削除
-					line = p.fset.Position(par.Type.Pos()).Line // 削除
-				} // 削除
-				if 0 < prevLine && prevLine < line && p.linebreak(line, 0, ws, true) { // 削除
-					ws = ignore // 削除
-					*multiLine = true // 削除
-				} else { // 削除
-					p.print(blank) // 削除
-				} // 削除
 			}
+			// separator if needed (linebreak or blank)
+			if 0 < prevLine && prevLine < parLineBeg && p.linebreak(parLineBeg, 0, ws, true) { // 変更点4: 改行検出ロジックの修正
+				// break line if the opening "(" or previous parameter ended on a different line
+				ws = ignore
+				*multiLine = true
+			} else if i > 0 {
+				p.print(blank)
+			}
+			// parameter names
 			if len(par.Names) > 0 {
 				// Very subtle: If we indented before (ws == ignore), identList
 				// won't indent again. If we didn't (ws == indent), identList will
@@ -299,11 +315,18 @@ func (p *printer) exprList(prev0 token.Pos, list []ast.Expr, depth int, mode exp
 				p.identList(par.Names, ws == indent, multiLine)
 				p.print(blank)
 			}
+			// parameter type
 			p.expr(par.Type, multiLine)
-			prevLine = p.fset.Position(par.Type.Pos()).Line // 変更点5: prevLineの更新をparLineEndに変更
+			prevLine = parLineEnd // 変更点5: prevLineの更新をparLineEndに変更
 		}
+		// if the closing ")" is on a separate line from the last parameter,
+		// print an additional "," and line break
+		if closing := p.fset.Position(fields.Closing).Line; 0 < prevLine && prevLine < closing { // 変更点6: 閉じ括弧前の改行処理
+			p.print(",")
+			p.linebreak(closing, 0, ignore, true)
+		}
+		// unindent if we indented
 		if ws == ignore {
-			// unindent if we indented // 削除
 			p.print(unindent)
 		}
 	}

コアとなるコードの解説

parameters関数は、ast.FieldList(GoのASTにおけるパラメータリストの表現)を受け取り、それを整形して出力します。

  1. prevLine := p.fset.Position(fields.Opening).Line:

    • prevLineは、直前に出力された要素の行番号を追跡するために使用されます。この変更により、パラメータリストの開始括弧(の行で初期化されるため、最初のパラメータが開始括弧の直後に改行されている場合でも、その改行を検出できるようになります。
  2. parLineBegparLineEnd:

    • 各パラメータparについて、その宣言が始まる行(parLineBeg)と終わる行(parLineEnd)を計算します。
    • parLineBegは、パラメータ名がある場合は最初のパラメータ名の行、ない場合は型の行になります。
    • parLineEndは、常にパラメータの型の行になります。
    • これにより、a, b intのように複数の名前を持つパラメータや、型が別の行に書かれているパラメータでも、その全体が占める行範囲を正確に把握できます。
  3. 改行検出ロジック:

    • if 0 < prevLine && prevLine < parLineBeg && p.linebreak(parLineBeg, 0, ws, true):
      • この条件は、prevLine(直前の要素の行)がparLineBeg(現在のパラメータの開始行)よりも小さい場合に真となります。これは、直前の要素と現在のパラメータの間に改行が存在することを示します。
      • p.linebreak(parLineBeg, 0, ws, true)は、parLineBegで指定された行に改行を挿入しようと試みます。wsindent(デフォルトのインデント)の場合、p.linebreakは通常の整形ルールに従いますが、ここでtrueが渡されているため、強制的に改行を挿入します。
      • ws = ignoreは、go/printerが通常挿入する空白を無視するように設定し、明示的な改行が優先されるようにします。
      • *multiLine = trueは、このパラメータリストが複数行にわたることを呼び出し元に伝えます。
  4. 閉じ括弧前の改行処理:

    • if closing := p.fset.Position(fields.Closing).Line; 0 < prevLine && prevLine < closing:
      • このブロックは、すべてのパラメータが処理された後に実行されます。
      • closingは、パラメータリストの閉じ括弧)の行番号です。
      • prevLine(最後のパラメータの終了行)がclosingよりも小さい場合、つまり最後のパラメータと閉じ括弧の間に改行がある場合に真となります。
      • この場合、p.print(",")でカンマを挿入し、p.linebreak(closing, 0, ignore, true)で強制的に改行を挿入します。これにより、複数行のパラメータリストの最後にカンマを付け、閉じ括弧を新しい行に配置するスタイルが維持されます。

これらの変更により、go/printerは、開発者が手動で挿入した関数シグネチャ内の改行を「整形上の意図」として解釈し、それを維持するようになりました。これにより、gofmtの利便性を損なうことなく、より柔軟なコードレイアウトが可能になります。

関連リンク

参考にした情報源リンク