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

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

このコミットは、Go言語のコードフォーマッタであるgofmtが、関数の呼び出しにおける可変引数(...エリプシス)の扱いを改善するためのものです。具体的には、gofmtrewrite機能が、関数の引数に...が付いている場合と付いていない場合を正しく区別して処理できるように修正されています。これにより、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(抽象構文木)ベースのコード書き換え機能を提供します。この機能は、特定のパターンにマッチするコードを別のパターンに変換する際に非常に強力です。

しかし、このコミット以前のgofmtrewrite機能には、関数の呼び出しにおいて可変引数(...)が使用されているかどうかを正確に区別できないという問題がありました。例えば、fun(x)fun(x...)はGo言語においては異なる意味を持ちます。前者は単一の引数xを渡す通常の関数呼び出しですが、後者はスライスxの要素を個別の引数として展開して渡す可変引数関数呼び出しです。

Issue #5059は、このgofmtrewrite機能がfun(x)fun(x...)を区別せずに同じパターンとして扱ってしまうというバグを報告していました。これにより、ユーザーが意図しないコードの書き換えが発生する可能性がありました。このコミットは、この問題を解決し、gofmtrewrite機能がより正確に動作するようにするためのものです。

前提知識の解説

  • gofmt: Go言語の公式なコードフォーマッタです。Goのソースコードを自動的に整形し、Goコミュニティ全体で一貫したコードスタイルを維持するのに役立ちます。-rオプションを使用すると、ASTベースのコード書き換え(リライト)を行うことができます。
  • AST (Abstract Syntax Tree): 抽象構文木。ソースコードの構文構造を木構造で表現したものです。コンパイラやリンタ、コードフォーマッタなどがコードを解析・操作する際に内部的に使用します。gofmtrewrite機能は、この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フィールドが含まれています。

技術的詳細

このコミットの核心は、gofmtrewrite機能がASTを比較する際に、ast.CallExprノードのEllipsisフィールドを考慮に入れるようにした点です。

gofmtrewrite機能は、ユーザーが指定したパターン(例: fun(x))と、解析対象のコードのASTノードを再帰的に比較し、パターンにマッチした場合に書き換えを行います。これまでの実装では、ast.CallExprの比較において、Ellipsisフィールドが適切に比較されていませんでした。

変更点を見ると、src/cmd/gofmt/rewrite.gomatch関数に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.CallExprEllipsisフィールドは、可変引数呼び出し(例: fun(x...))の場合に...のトークンの位置を保持し、通常の呼び出し(例: fun(x))の場合は無効な位置(token.NoPos)を保持します。

したがって、p.Ellipsis.IsValid() != v.Ellipsis.IsValid()という条件は、パターンと値のast.CallExprが、一方が可変引数呼び出しで他方が通常の呼び出しである場合にtrueとなり、match関数はfalseを返します。これにより、gofmtfun(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.gomatch関数は、gofmtのAST書き換え機能の中核をなす部分です。この関数は、パターンASTノードと実際のコードのASTノードを比較し、両者が一致するかどうかを判断します。

追加されたcase callExprType:ブロックは、比較対象のノードが関数呼び出し(ast.CallExpr)である場合に特化した処理です。

  1. p := pattern.Interface().(*ast.CallExpr): パターン側のast.CallExprを取得します。
  2. v := val.Interface().(*ast.CallExpr): 実際のコード側のast.CallExprを取得します。
  3. 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は可変引数の有無を正確に区別して書き換えを行うことができるようになりました。

この変更は、gofmtrewrite機能の堅牢性と正確性を向上させ、ユーザーがより信頼性の高いコード変換を行えるようにするために不可欠でした。

関連リンク

  • Go言語のgofmtコマンドに関する公式ドキュメントやブログ記事
  • Go言語の可変引数関数に関する公式ドキュメント
  • Goのgo/astパッケージのドキュメント

参考にした情報源リンク