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

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

このコミットは、Go言語のcmd/apiツールにおけるバグ修正です。具体的には、func(x, y, z int)のように、複数の引数や戻り値が同じ型を持つ場合に、それらの関数シグネチャの出力が正しく行われない問題を修正しています。この修正により、cmd/apiが生成するAPIシグネチャが、Goの言語仕様に沿って正確に表現されるようになりました。

コミット

commit 1ad5f87635ee35b8b0053a1a4d2a05d0e892e4d0
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Wed Sep 19 07:04:12 2012 +0900

    cmd/api: fix signatures like func(x, y, z int)
    
    Fixes writing of function parameter, result lists which
    consist of multiple named or unnamed items with same type.
    
    Fixes #4011.
    
    R=golang-dev, bsiegert, bradfitz, rsc
    CC=golang-dev
    https://golang.org/cl/6475062

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

https://github.com/golang/go/commit/1ad5f87635ee35b8b0053a1a4d2a05d0e892e4d0

元コミット内容

cmd/api: fix signatures like func(x, y, z int)

Fixes writing of function parameter, result lists which
consist of multiple named or unnamed items with same type.

Fixes #4011.

R=golang-dev, bsiegert, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/6475062

変更の背景

この変更は、Go言語のcmd/apiツールが、特定の形式の関数シグネチャを正しく出力できないというバグ(Issue 4011)を修正するために行われました。具体的には、func(x, y, z int)のように、複数の引数や戻り値が同じ型を持つ場合に、cmd/apiが生成するAPIシグネチャの文字列表現が不正確になる問題がありました。

Go言語では、関数パラメータや戻り値のリストにおいて、同じ型の複数の変数名をまとめて宣言することができます。例えば、func(x, y int)func(x int, y int)と同じ意味です。cmd/apiツールは、Goの標準ライブラリのAPIシグネチャを抽出・比較するために使用される重要なツールであり、その出力の正確性はGoエコシステム全体の整合性を保つ上で不可欠です。このバグは、APIの変更を正確に追跡する能力に影響を与える可能性があったため、修正が必要とされました。

前提知識の解説

このコミットを理解するためには、以下のGo言語および関連ツールの概念を理解しておく必要があります。

  1. Go言語のast (Abstract Syntax Tree) パッケージ: go/astパッケージは、Goのソースコードを抽象構文木(AST)として表現するためのデータ構造と関数を提供します。コンパイラ、リンタ、コード分析ツールなど、Goのコードをプログラム的に操作する多くのツールで利用されます。

    • ast.FuncDecl: 関数宣言(funcキーワードで始まる)を表すASTノードです。
    • ast.FuncType: 関数の型(パラメータと戻り値のリストを含む)を表すASTノードです。
    • ast.Field: 関数パラメータや構造体フィールドなど、名前と型を持つ単一の項目を表すASTノードです。Namesフィールドは、その項目が持つ名前のリスト(例: x, y, z intの場合、x, y, z)を含みます。Typeフィールドはその型を表します。
  2. Go言語の関数シグネチャ: Goの関数シグネチャは、関数のパラメータと戻り値の型を定義します。

    • パラメータリスト: func(param1 type1, param2 type2)のように記述されます。同じ型の複数のパラメータはfunc(x, y int)のようにまとめて宣言できます。
    • 戻り値リスト: func() (result1 type1, result2 type2)のように記述されます。戻り値もパラメータと同様に、func() (x, y int)のようにまとめて宣言できます。戻り値に名前を付けない場合は、型のみを記述します(例: func() (int, error))。
  3. cmd/apiツール: Goのソースコードリポジトリに含まれる内部ツールの一つで、Goの標準ライブラリのAPIシグネチャを抽出・比較するために使用されます。これは、Goの互換性保証(Go 1互換性保証など)を維持するために非常に重要です。このツールは、GoのASTを解析し、APIの公開された部分のシグネチャをテキスト形式で出力します。この出力は、APIの変更を検出するために使用されます。

技術的詳細

このバグは、cmd/apiツール内のfuncSigString関数に存在していました。この関数は、ast.FuncType構造体を受け取り、その関数シグネチャを文字列として整形して出力する役割を担っています。

問題の核心は、ast.Field構造体のNamesフィールドの扱いにありました。GoのASTでは、func(x, y, z int)のような宣言は、ast.Fieldのリストとして表現され、各ast.FieldNamesフィールドに複数の識別子(x, y, z)を持ち、Typeフィールドに共通の型(int)を持ちます。

しかし、funcSigString関数は、このNamesフィールドの複数の識別子を適切に展開せず、単一のast.Fieldを単一の型としてしか扱っていませんでした。その結果、func(x, y, z int)のようなシグネチャが、func(int)のように誤って出力されてしまうことがありました。同様に、戻り値のリストでもこの問題が発生していました。

修正は、以下の2つの主要な変更によって行われました。

  1. writeFieldヘルパー関数の導入: この新しいヘルパー関数は、ast.Fieldを受け取り、そのNamesフィールドに複数の識別子が含まれている場合に、それぞれの識別子に対して型を繰り返し出力するように修正されました。例えば、f.Names[x, y, z]f.Typeintの場合、int, int, intと出力されるようになります。これにより、func(x, y, z int)func(int, int, int)として正しく表現されるようになります。

  2. 戻り値の数の正確な計算: 戻り値のリストの処理において、len(ft.Results.List)だけでは、func() (x, y int)のような場合に実際の戻り値の数(この場合は2つ)を正確に把握できませんでした。これは、ft.Results.Listの各ast.Fieldが複数の名前を持つ可能性があるためです。修正では、nr変数を導入し、各ast.FieldNamesの数を合計することで、実際の戻り値の総数を正確に計算するように変更されました。これにより、戻り値が複数ある場合に括弧で囲むべきかどうかの判断が正しく行われるようになりました。

これらの変更により、cmd/apiはGoの関数シグネチャを、名前付き・名前なし、単一型・複数型にかかわらず、正確に文字列として表現できるようになりました。

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

変更は主にsrc/cmd/api/goapi.goファイル内のfuncSigString関数に集中しています。

--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -1017,18 +1017,38 @@ func (w *Walker) walkFuncDecl(f *ast.FuncDecl) {
 
 func (w *Walker) funcSigString(ft *ast.FuncType) string {
 	var b bytes.Buffer
+	writeField := func(b *bytes.Buffer, f *ast.Field) {
+		if n := len(f.Names); n > 1 {
+			for i := 0; i < n; i++ {
+				if i > 0 {
+					b.WriteString(", ")
+				}
+				b.WriteString(w.nodeString(w.namelessType(f.Type)))
+			}
+		} else {
+			b.WriteString(w.nodeString(w.namelessType(f.Type)))
+		}
+	}
 	b.WriteByte('(')
 	if ft.Params != nil {
 		for i, f := range ft.Params.List {
 			if i > 0 {
 				b.WriteString(", ")
 			}
-			b.WriteString(w.nodeString(w.namelessType(f.Type)))
+			writeField(&b, f)
 		}
 	}
 	b.WriteByte(')')
 	if ft.Results != nil {
-		if nr := len(ft.Results.List); nr > 0 {
+		nr := 0
+		for _, f := range ft.Results.List {
+			if n := len(f.Names); n > 1 {
+				nr += n
+			} else {
+				nr++
+			}
+		}
+		if nr > 0 {
 			b.WriteByte(' ')
 			if nr > 1 {
 				b.WriteByte('(')
@@ -1037,7 +1057,7 @@ func (w *Walker) funcSigString(ft *ast.FuncType) string {
 				if i > 0 {
 					b.WriteString(", ")
 				}
-				b.WriteString(w.nodeString(w.namelessType(f.Type)))
+				writeField(&b, f)
 			}
 			if nr > 1 {
 				b.WriteByte(')')

また、この修正を検証するための新しいテストケースが追加されています。

  • src/cmd/api/testdata/src/pkg/p3/golden.txt
  • src/cmd/api/testdata/src/pkg/p3/p3.go

p3.goには、以下のような関数が含まれており、これらが正しくシグネチャとして抽出されることをgolden.txtで確認しています。

package p3

type ThirdBase struct{}

func (tb *ThirdBase) GoodPlayer() (i, j, k int)
func BadHop(i, j, k int) (l, m bool, n, o *ThirdBase, err error)

コアとなるコードの解説

  1. writeFieldヘルパー関数: funcSigString関数内にwriteFieldというローカル関数が定義されました。

    • この関数は*ast.Fieldを受け取ります。
    • if n := len(f.Names); n > 1の条件で、フィールドに複数の名前が定義されているか(例: x, y, z int)をチェックします。
    • もし複数の名前がある場合、forループを使って、それぞれの名前に対してフィールドの型(w.nodeString(w.namelessType(f.Type)))をカンマで区切ってバッファに書き込みます。これにより、x, y, z intint, int, intとして出力されます。
    • 名前が1つまたはない場合(通常のパラメータや戻り値)、単にその型をバッファに書き込みます。
  2. パラメータリストの処理: ft.Params.Listをループする箇所で、以前は直接w.nodeString(w.namelessType(f.Type))を呼び出していましたが、これがwriteField(&b, f)に置き換えられました。これにより、パラメータリストの出力がwriteFieldのロジックに従って正確に行われるようになりました。

  3. 戻り値の数の正確な計算: ft.Results != nilのブロック内で、nr変数の計算方法が変更されました。

    • 以前はnr := len(ft.Results.List)としていましたが、これはast.Fieldのリストの長さであり、実際の戻り値の総数ではありませんでした。
    • 新しいコードでは、for _, f := range ft.Results.Listで各ast.Fieldをループし、len(f.Names)(そのフィールドが持つ名前の数)をnrに加算しています。これにより、func() (l, m bool, n, o *ThirdBase, err error)のようなシグネチャの場合、l, mで2つ、n, oで2つ、errで1つ、合計5つの戻り値があることを正確にnrが示すようになります。
    • この正確なnrの値に基づいて、戻り値が複数ある場合に()で囲むべきかどうかの判断(if nr > 1)が正しく行われます。
  4. 戻り値リストの処理: 戻り値リストの各フィールドをループする箇所でも、パラメータリストと同様にwriteField(&b, f)が使用されるようになりました。これにより、戻り値の出力も正確に行われます。

これらの変更により、cmd/apiはGoの関数シグネチャを、Goの言語仕様に厳密に従って、より正確に文字列として表現できるようになりました。

関連リンク

参考にした情報源リンク

  • Go言語のgo/astパッケージのドキュメント
  • Go言語の関数シグネチャに関する公式ドキュメントまたはチュートリアル
  • Go言語のIssueトラッカー (Issue 4011)
  • Go言語のコードレビューシステム (CL 6475062)