[インデックス 16053] ファイルの概要
このコミットは、Go言語のコードフォーマッタであるgofmt
が、関数の呼び出しにおける可変引数(...
エリプシス)の扱いを改善するためのものです。具体的には、gofmt
のrewrite
機能が、関数の引数に...
が付いている場合と付いていない場合を正しく区別して処理できるように修正されています。これにより、gofmt -r
コマンドを用いたコードの書き換え時に、意図しない変更が適用されることを防ぎます。
コミット
commit 9115e411f596df35c0b1ba5a2335bd4bbfbdc1fa
Author: Robert Griesemer <gri@golang.org>
Date: Tue Apr 2 13:18:32 2013 -0700
cmd/gofmt: handle ... in rewrite of calls
Fixes #5059.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/8284043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9115e411f596df35c0b1ba5a2335bd4bbfbdc1fa
元コミット内容
cmd/gofmt: handle ... in rewrite of calls
Fixes #5059.
このコミットメッセージは、gofmt
コマンドが関数の呼び出しを書き換える際に、可変引数(...
)の扱いを改善したことを示しています。また、GoのIssue #5059を修正したことも明記されています。
変更の背景
Go言語のgofmt
は、Goのソースコードを標準的なスタイルに自動的にフォーマットするツールです。また、-r
オプションを使用することで、AST(抽象構文木)ベースのコード書き換え機能を提供します。この機能は、特定のパターンにマッチするコードを別のパターンに変換する際に非常に強力です。
しかし、このコミット以前のgofmt
のrewrite
機能には、関数の呼び出しにおいて可変引数(...
)が使用されているかどうかを正確に区別できないという問題がありました。例えば、fun(x)
とfun(x...)
はGo言語においては異なる意味を持ちます。前者は単一の引数x
を渡す通常の関数呼び出しですが、後者はスライスx
の要素を個別の引数として展開して渡す可変引数関数呼び出しです。
Issue #5059は、このgofmt
のrewrite
機能がfun(x)
とfun(x...)
を区別せずに同じパターンとして扱ってしまうというバグを報告していました。これにより、ユーザーが意図しないコードの書き換えが発生する可能性がありました。このコミットは、この問題を解決し、gofmt
のrewrite
機能がより正確に動作するようにするためのものです。
前提知識の解説
gofmt
: Go言語の公式なコードフォーマッタです。Goのソースコードを自動的に整形し、Goコミュニティ全体で一貫したコードスタイルを維持するのに役立ちます。-r
オプションを使用すると、ASTベースのコード書き換え(リライト)を行うことができます。- AST (Abstract Syntax Tree): 抽象構文木。ソースコードの構文構造を木構造で表現したものです。コンパイラやリンタ、コードフォーマッタなどがコードを解析・操作する際に内部的に使用します。
gofmt
のrewrite
機能は、このASTを操作することでコードを書き換えます。 - 可変引数(Variadic Functions)とエリプシス(
...
): Go言語では、関数の最後のパラメータに...
を付けることで、その関数が任意の数の引数を受け取れるように定義できます。これを可変引数関数と呼びます。- 例:
func sum(nums ...int) int
- 可変引数関数を呼び出す際には、通常通り複数の引数を渡すことができます:
sum(1, 2, 3)
- また、スライスを可変引数関数に渡す場合、スライスの後ろに
...
を付けることで、スライスの要素を個別の引数として展開して渡すことができます:nums := []int{1, 2, 3}; sum(nums...)
- この
...
は「エリプシス」と呼ばれ、可変引数関数の定義と呼び出しの両方で使用されますが、それぞれ異なる意味を持ちます。
- 例:
ast.CallExpr
: Goのgo/ast
パッケージで定義されているASTノードの一つで、関数呼び出しを表します。この構造体には、呼び出される関数(Fun
フィールド)と引数(Args
フィールド)の他に、可変引数呼び出しの場合に...
の位置を示すEllipsis
フィールドが含まれています。
技術的詳細
このコミットの核心は、gofmt
のrewrite
機能がASTを比較する際に、ast.CallExpr
ノードのEllipsis
フィールドを考慮に入れるようにした点です。
gofmt
のrewrite
機能は、ユーザーが指定したパターン(例: fun(x)
)と、解析対象のコードのASTノードを再帰的に比較し、パターンにマッチした場合に書き換えを行います。これまでの実装では、ast.CallExpr
の比較において、Ellipsis
フィールドが適切に比較されていませんでした。
変更点を見ると、src/cmd/gofmt/rewrite.go
のmatch
関数にcallExprType
(*ast.CallExpr
の型)のケースが追加されています。
case callExprType:
// For calls, the Ellipsis fields (token.Position) must
// match since that is how f(x) and f(x...) are different.
// Check them here but fall through for the remaining fields.
p := pattern.Interface().(*ast.CallExpr)
v := val.Interface().(*ast.CallExpr)
if p.Ellipsis.IsValid() != v.Ellipsis.IsValid() {
return false
}
このコードスニペットは、パターン(p
)と値(v
)が両方ともast.CallExpr
型である場合に実行されます。ここで重要なのは、p.Ellipsis.IsValid() != v.Ellipsis.IsValid()
という条件です。
token.Position
型のIsValid()
メソッドは、その位置が有効なソースコード上の位置を示している場合にtrue
を返します。ast.CallExpr
のEllipsis
フィールドは、可変引数呼び出し(例:fun(x...)
)の場合に...
のトークンの位置を保持し、通常の呼び出し(例:fun(x)
)の場合は無効な位置(token.NoPos
)を保持します。
したがって、p.Ellipsis.IsValid() != v.Ellipsis.IsValid()
という条件は、パターンと値のast.CallExpr
が、一方が可変引数呼び出しで他方が通常の呼び出しである場合にtrue
となり、match
関数はfalse
を返します。これにより、gofmt
はfun(x)
とfun(x...)
を異なるものとして認識し、パターンマッチングの精度が向上します。
この変更により、gofmt -r="fun(x)->Fun(x)"
のような書き換えルールが適用される際に、fun(x)
はFun(x)
に書き換えられるが、fun(x...)
は書き換えられない、といった期待通りの動作が実現されます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/cmd/gofmt/rewrite.go
ファイル内のmatch
関数です。
--- a/src/cmd/gofmt/rewrite.go
+++ b/src/cmd/gofmt/rewrite.go
@@ -107,6 +107,7 @@ var (
identType = reflect.TypeOf((*ast.Ident)(nil))
objectPtrType = reflect.TypeOf((*ast.Object)(nil))
positionType = reflect.TypeOf(token.NoPos)
+ callExprType = reflect.TypeOf((*ast.CallExpr)(nil))
scopePtrType = reflect.TypeOf((*ast.Scope)(nil))
)
@@ -192,8 +193,17 @@ func match(m map[string]reflect.Value, pattern, val reflect.Value) bool {
tv := val.Interface().(*ast.Ident)
return tp == nil && tv == nil || tp != nil && tv != nil && tp.Name == tv.Name
case objectPtrType, positionType:
- // object pointers and token positions don't need to match
+ // object pointers and token positions always match
return true
+ case callExprType:
+ // For calls, the Ellipsis fields (token.Position) must
+ // match since that is how f(x) and f(x...) are different.
+ // Check them here but fall through for the remaining fields.
+ p := pattern.Interface().(*ast.CallExpr)
+ v := val.Interface().(*ast.CallExpr)
+ if p.Ellipsis.IsValid() != v.Ellipsis.IsValid() {
+ return false
+ }
}
tp := reflect.Indirect(pattern)
また、この変更を検証するためのテストケースがsrc/cmd/gofmt/gofmt_test.go
に追加され、新しいテストデータファイルsrc/cmd/gofmt/testdata/rewrite6.input
, rewrite6.golden
, rewrite7.input
, rewrite7.golden
が作成されています。
コアとなるコードの解説
rewrite.go
のmatch
関数は、gofmt
のAST書き換え機能の中核をなす部分です。この関数は、パターンASTノードと実際のコードのASTノードを比較し、両者が一致するかどうかを判断します。
追加されたcase callExprType:
ブロックは、比較対象のノードが関数呼び出し(ast.CallExpr
)である場合に特化した処理です。
p := pattern.Interface().(*ast.CallExpr)
: パターン側のast.CallExpr
を取得します。v := val.Interface().(*ast.CallExpr)
: 実際のコード側のast.CallExpr
を取得します。if p.Ellipsis.IsValid() != v.Ellipsis.IsValid() { return false }
: ここが最も重要な変更点です。p.Ellipsis.IsValid()
は、パターンが可変引数呼び出し(fun(x...)
)である場合にtrue
を返します。v.Ellipsis.IsValid()
は、実際のコードが可変引数呼び出しである場合にtrue
を返します。- この条件は、パターンと実際のコードのどちらか一方だけが可変引数呼び出しである場合に
true
となり、match
関数はfalse
を返します。つまり、fun(x)
とfun(x...)
は異なるものとして扱われ、パターンマッチが失敗します。 - これにより、
gofmt
は可変引数の有無を正確に区別して書き換えを行うことができるようになりました。
この変更は、gofmt
のrewrite
機能の堅牢性と正確性を向上させ、ユーザーがより信頼性の高いコード変換を行えるようにするために不可欠でした。
関連リンク
- Go言語の
gofmt
コマンドに関する公式ドキュメントやブログ記事 - Go言語の可変引数関数に関する公式ドキュメント
- Goの
go/ast
パッケージのドキュメント
参考にした情報源リンク
- Go Issue #5059: cmd/gofmt: rewrite doesn't distinguish between f(x) and f(x...)
- Gerrit Code Review for Go: cmd/gofmt: handle ... in rewrite of calls (これはコミットメッセージに記載されているリンクと同じです)
- Go言語の可変引数について (Effective Goより)
- Go言語の
go/ast
パッケージ - Go言語の
go/token
パッケージ