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

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

このコミットは、Go言語の標準ライブラリである go/printer パッケージにおける、可変長引数(variadic arguments)を持つ関数呼び出しの整形に関するバグ修正です。具体的には、複数行にわたる可変長引数リストの末尾に ... がある場合に、正しくカンマが挿入されない問題を解決します。

コミット

eafe86c2df73c9d80d2eab17b0c50ed8514cba81 by Anthony Martin (ality@pbrane.org) on Mon Feb 27 13:56:43 2012 -0800

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

https://github.com/golang/go/commit/eafe86c2df73c9d80d2eab17b0c50ed8514cba81

元コミット内容

go/printer: fix printing of variadic function calls

Fixes #3130.

R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/5697062

変更の背景

この変更は、Go言語のコード整形ツールである go/printer が、可変長引数を持つ関数呼び出しを整形する際に発生していたバグ(Issue 3130)を修正するために行われました。

Go言語では、append(slice, elements...) のように、スライスを展開して可変長引数として関数に渡すことができます。この際、引数リストが複数行にわたる場合、go/printer... の後にカンマを適切に挿入しないことがありました。これにより、整形後のコードが期待通りのフォーマットにならず、可読性が損なわれたり、場合によってはコンパイルエラーを引き起こす可能性がありました。

具体的には、以下のようなコードが問題となっていました。

_ = append(s,
    a...)

このコードが go/printer によって整形されると、a... の後にカンマが欠落し、Goの慣習的なフォーマットに反していました。このコミットは、このようなケースで ... の後にカンマが正しく挿入されるように go/printer のロジックを調整することを目的としています。

前提知識の解説

Go言語の可変長引数(Variadic Functions)

Go言語では、関数の最後のパラメータに ... を付けることで、その関数が任意の数の引数を受け取れるように定義できます。これを可変長引数と呼びます。例えば、func sum(nums ...int) int のように定義された関数は、sum(1, 2, 3) のように複数の整数を引数として受け取ることができます。関数内部では、可変長引数はスライスとして扱われます。

また、既存のスライスを可変長引数として関数に渡す場合、スライスの後ろに ... を付けて展開します。例: append(slice, anotherSlice...)

go/printer パッケージ

go/printer はGo言語の標準ライブラリの一部であり、Goのソースコードを整形(pretty-print)するためのパッケージです。Goのコードは go fmt コマンドによって自動的に整形されますが、この go fmt の内部で go/printer パッケージが利用されています。

go/printer は、Goのソースコードを抽象構文木(AST: Abstract Syntax Tree)として解析し、そのASTを基に整形されたコードを生成します。これにより、Goのコードベース全体で一貫したコーディングスタイルが強制され、可読性が向上します。

抽象構文木(AST: Abstract Syntax Tree)と go/ast パッケージ

ASTは、プログラムのソースコードの抽象的な構文構造を木構造で表現したものです。Go言語では、go/ast パッケージがGoのソースコードを解析してASTを構築する機能を提供します。

go/printer は、この go/ast パッケージによって生成されたASTを受け取り、それを整形して出力します。ASTの各ノードは、関数呼び出し、変数宣言、式などの言語要素に対応しており、go/printer はこれらのノードをどのように整形するかを決定します。

token パッケージ

go/token パッケージは、Go言語の字句解析(lexical analysis)で使われるトークン(識別子、キーワード、演算子、区切り文字など)を定義しています。go/printer は、ASTを走査しながら、これらのトークンを適切な位置に配置してコードを再構築します。

このコミットで言及されている token.ELLIPSIS... トークンを、token.COMMA, トークンを指します。

技術的詳細

この修正は、src/pkg/go/printer/nodes.go ファイル内の printer 型の expr1 メソッドに焦点を当てています。expr1 メソッドは、Goの式(expression)を整形する主要なロジックを含んでいます。特に、関数呼び出し(ast.CallExpr)の処理を担当する部分が変更されました。

変更前のコードでは、関数呼び出しの引数リストを整形する際に、可変長引数であるかどうかにかかわらず、p.exprList(x.Lparen, x.Args, depth, commaSep|commaTerm, multiLine, x.Rparen) という単一の呼び出しで処理していました。ここで commaTerm は、引数リストの最後にカンマを付けるべきかどうかを制御するフラグです。

しかし、可変長引数(...)が使用されている場合、特に引数リストが複数行にわたる場合に問題が発生しました。... は引数リストの一部として扱われますが、その後にカンマが必要なケース(例えば、append(s, a...,) のように、... の後に閉じ括弧 ) が続くが、...) が異なる行にある場合)で、go/printer がカンマを挿入しないという問題がありました。

このコミットでは、x.Ellipsis.IsValid() を使って、現在の関数呼び出しが可変長引数を含んでいるかどうかをチェックする条件分岐が追加されました。

  1. 可変長引数がある場合 (x.Ellipsis.IsValid() が true):
    • まず、... の直前までの引数リストを p.exprList(x.Lparen, x.Args, depth, commaSep, multiLine, x.Ellipsis) で整形します。ここで commaTerm フラグは使用されず、... の直前でカンマが強制的に挿入されることはありません。
    • 次に、p.print(x.Ellipsis, token.ELLIPSIS)... トークンを印字します。
    • 重要な変更点: if x.Rparen.IsValid() && p.lineFor(x.Ellipsis) < p.lineFor(x.Rparen) という条件が追加されました。これは、... の後に閉じ括弧 ) が存在し、かつ ...) が異なる行にある場合にのみ、カンマと改行(formfeed)を挿入するというロジックです。これにより、複数行にわたる可変長引数リストの末尾に ... がある場合に、適切なカンマが追加されるようになります。
  2. 可変長引数がない場合 (x.Ellipsis.IsValid() が false):
    • 以前と同様に、p.exprList(x.Lparen, x.Args, depth, commaSep|commaTerm, multiLine, x.Rparen) を呼び出し、通常の関数呼び出しの引数リストを整形します。

この変更により、go/printer は可変長引数を持つ関数呼び出しの整形において、より正確なGoの慣習に従うようになりました。特に、テストデータ expressions.golden に追加された新しいテストケースは、この修正が様々な複数行の可変長引数呼び出しパターンを正しく処理できることを示しています。

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

src/pkg/go/printer/nodes.gofunc (p *printer) expr1(...) メソッド内の ast.CallExpr を処理する部分。

--- a/src/pkg/go/printer/nodes.go
+++ b/src/pkg/go/printer/nodes.go
@@ -804,9 +804,14 @@ func (p *printer) expr1(expr ast.Expr, prec1, depth int, multiLine *bool) {
 		}
 		p.expr1(x.Fun, token.HighestPrec, depth, multiLine)
 		p.print(x.Lparen, token.LPAREN)
-		p.exprList(x.Lparen, x.Args, depth, commaSep|commaTerm, multiLine, x.Rparen)
 		if x.Ellipsis.IsValid() {
+			p.exprList(x.Lparen, x.Args, depth, commaSep, multiLine, x.Ellipsis)
 			p.print(x.Ellipsis, token.ELLIPSIS)
+			if x.Rparen.IsValid() && p.lineFor(x.Ellipsis) < p.lineFor(x.Rparen) {
+				p.print(token.COMMA, formfeed)
+			}
+		} else {
+			p.exprList(x.Lparen, x.Args, depth, commaSep|commaTerm, multiLine, x.Rparen)
 		}
 		p.print(x.Rparen, token.RPAREN)

コアとなるコードの解説

変更されたコードブロックは、ast.CallExpr (関数呼び出しのASTノード) を処理する部分です。

  1. 変更前:

    p.exprList(x.Lparen, x.Args, depth, commaSep|commaTerm, multiLine, x.Rparen)
    if x.Ellipsis.IsValid() {
        p.print(x.Ellipsis, token.ELLIPSIS)
    }
    

    このロジックでは、まず引数リスト全体を整形し、その後に ... があればそれを印字していました。commaTerm フラグは引数リストの最後にカンマを付けるかどうかを制御しますが、... の後にカンマが必要な特定の複数行のケースを適切に処理できませんでした。

  2. 変更後:

    if x.Ellipsis.IsValid() { // 可変長引数がある場合
        p.exprList(x.Lparen, x.Args, depth, commaSep, multiLine, x.Ellipsis) // ... の直前までを整形
        p.print(x.Ellipsis, token.ELLIPSIS) // ... を印字
        if x.Rparen.IsValid() && p.lineFor(x.Ellipsis) < p.lineFor(x.Rparen) {
            p.print(token.COMMA, formfeed) // ... と ) が別行ならカンマと改行を挿入
        }
    } else { // 可変長引数がない場合
        p.exprList(x.Lparen, x.Args, depth, commaSep|commaTerm, multiLine, x.Rparen) // 通常の引数リスト整形
    }
    
    • if x.Ellipsis.IsValid(): 関数呼び出しが可変長引数を使用しているかどうかをチェックします。
    • p.exprList(x.Lparen, x.Args, depth, commaSep, multiLine, x.Ellipsis): 可変長引数がある場合、まず ... の直前までの引数(x.Args)を整形します。ここで commaSep のみを使用し、commaTerm を含めないことで、... の直前に余分なカンマが挿入されるのを防ぎます。x.Ellipsis は、exprList がどこまでを引数として扱うかの境界を示します。
    • p.print(x.Ellipsis, token.ELLIPSIS): ... トークンを印字します。
    • if x.Rparen.IsValid() && p.lineFor(x.Ellipsis) < p.lineFor(x.Rparen): この条件がこの修正の核心です。
      • x.Rparen.IsValid(): 閉じ括弧 ) が存在するかどうかを確認します。
      • p.lineFor(x.Ellipsis) < p.lineFor(x.Rparen): ... トークンと閉じ括弧 ) が異なる行にあるかどうかを確認します。
      • この両方の条件が満たされる場合(つまり、複数行にわたる可変長引数リストで ... の後に閉じ括弧が続く場合)、p.print(token.COMMA, formfeed) を実行してカンマと改行を挿入します。これにより、append(s, a...,) のような正しいフォーマットが生成されます。
    • else: 可変長引数がない場合は、以前のロジック(commaTerm を含む)で引数リストを整形します。

この変更により、go/printer は可変長引数を持つ関数呼び出しの整形において、より正確なGoの慣習に従うようになりました。特に、テストデータ expressions.golden に追加された新しいテストケースは、この修正が様々な複数行の可変長引数呼び出しパターンを正しく処理できることを示しています。

関連リンク

  • Go issue 3130: https://github.com/golang/go/issues/3130
  • Gerrit Change-Id: I2205f633f100644 (これはコミットハッシュの一部であり、Gerritの変更IDではありませんが、元のコミットメッセージに記載されている https://golang.org/cl/5697062 がGerritの変更リストへのリンクです。)
  • Gerrit Code Review: https://golang.org/cl/5697062

参考にした情報源リンク

  • Go言語の公式ドキュメント(go/printergo/astgo/token パッケージに関する情報)
  • Go言語の可変長引数に関する一般的な情報源
  • GitHubのGoリポジトリのIssue #3130
  • Gerrit Code Review の変更リスト 5697062