[インデックス 14242] ファイルの概要
このコミットは、Go言語のAPIチェッカーツールである cmd/api
における、コンテキストの再収束(re-converging contexts)のハンドリングに関する修正と機能改善を目的としています。具体的には、異なるビルドコンテキスト(例: darwin-386
と darwin-amd64
)で定義されたAPI要素が、より一般的なコンテキスト(例: pkg syscall
)で再定義された場合に、APIの互換性チェックが正しく行われるように変更されています。
コミット
commit 71d9e956a00e95f734f633056882475832d534f4
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Oct 30 13:12:59 2012 +0100
cmd/api: handle contexts re-converging
Fixes #4303
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6816058
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/71d9e956a00e95f734f633056882475832d534f4
元コミット内容
このコミットの元の内容は以下の通りです。
cmd/api: handle contexts re-converging
Fixes #4303
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6816058
これは、cmd/api
ツールが「コンテキストの再収束」を処理するように変更されたことを示しています。Fixes #4303
は、Goプロジェクトのイシュートラッカーにおける問題 #4303 を修正するものであることを意味します。ただし、現在のGoイシュートラッカーではこの番号のイシューは公開されていません。これは古いイシュー番号であるか、内部的な参照である可能性があります。
変更の背景
Go言語の cmd/api
ツールは、Goの標準ライブラリのAPI互換性をチェックするために使用されます。このツールは、新しいGoのバージョンがリリースされる際に、既存のコードが壊れないように、APIの変更を監視します。
以前の cmd/api
ツールでは、特定のビルドコンテキスト(例: darwin-386
や darwin-amd64
のようなOSとアーキテクチャの組み合わせ)に特化したAPI要素が、より一般的なコンテキスト(例: pkg syscall
のようにコンテキスト指定がないもの)で再定義された場合に、互換性チェックが正しく機能しない問題がありました。
例えば、pkg syscall (darwin-386), type RawSockaddrInet6 struct
と pkg syscall (darwin-amd64), type RawSockaddrInet6 struct
のように、異なるコンテキストで同じ構造体が定義されている場合を考えます。もし将来的に、これらのコンテキスト固有の定義が削除され、代わりに pkg syscall, type RawSockaddrInet6 struct
のようにコンテキストに依存しない単一の定義に「再収束」した場合、cmd/api
はこれを互換性のない変更として誤って報告してしまう可能性がありました。
このコミットは、このような「コンテキストの再収束」のシナリオを正しく認識し、APIの互換性が維持されている場合にはエラーを報告しないように cmd/api
のロジックを修正することを目的としています。
前提知識の解説
- Go言語のAPI互換性: Go言語は、後方互換性を非常に重視しています。新しいバージョンがリリースされても、既存のGoプログラムが問題なく動作し続けることが期待されています。
cmd/api
ツールは、この互換性を保証するための重要なツールの一つです。 cmd/api
ツール: Goの標準ライブラリのAPI定義を抽出し、以前のバージョンと比較することで、APIの変更(追加、削除、変更)を検出するコマンドラインツールです。これにより、互換性のない変更が誤って導入されるのを防ぎます。- ビルドコンテキスト: Goのビルドシステムでは、OSやアーキテクチャなどの環境に応じて、異なるコードパスやAPI定義が選択されることがあります。例えば、
syscall
パッケージの定義は、darwin-386
(macOS 32-bit) とdarwin-amd64
(macOS 64-bit) で異なる場合があります。API定義には、このようなコンテキスト情報が付加されることがあります。 - API要素の表現:
cmd/api
ツールが扱うAPI要素は、通常、pkg <パッケージ名>, <要素の種類> <要素名>
のような文字列形式で表現されます。コンテキスト情報が含まれる場合、pkg <パッケージ名> (<コンテキスト>), <要素の種類> <要素名>
のようになります。- 例:
pkg syscall (darwin-386), type RawSockaddrInet6 struct
- 例:
pkg syscall, type RawSockaddrInet6 struct
(コンテキストなし)
- 例:
regexp
パッケージ: Go言語の標準ライブラリで提供される正規表現エンジンです。文字列のパターンマッチングや置換に使用されます。map[string]bool
(Set): Go言語でセット(集合)を表現する一般的なイディオムです。キーの存在をtrue
またはfalse
でチェックすることで、要素がセットに含まれているかを効率的に判断できます。
技術的詳細
このコミットの主要な変更点は、src/cmd/api/goapi.go
の compareAPI
関数におけるAPI要素の比較ロジックの改善です。
-
set
関数の追加:func set(items []string) map[string]bool
というヘルパー関数が追加されました。これは、文字列スライスを受け取り、その要素をキーとするmap[string]bool
を作成します。これにより、optionalSet
やexceptionSet
の初期化が簡潔になり、コードの可読性が向上しました。 -
spaceParensRx
正規表現の追加:var spaceParensRx = regexp.MustCompile(
(\S+?))\n
という正規表現が追加されました。この正規表現は、API要素の文字列から(コンテキスト)
の部分(例:(darwin-386)
)を抽出するために使用されます。\s
は空白文字、\(
と\)
はリテラルの括弧、\S+?
は非空白文字が1回以上出現する最短のマッチを意味します。 -
featureWithoutContext
関数の追加:func featureWithoutContext(f string) string
という関数が追加されました。この関数は、与えられたAPI要素の文字列f
に括弧(
が含まれている場合、spaceParensRx
を使用して(コンテキスト)
の部分を削除した文字列を返します。これにより、コンテキストに依存しないAPI要素の「ベース名」を取得できるようになります。 -
compareAPI
関数のロジック変更:compareAPI
関数内のfor len(required) > 0 || len(features) > 0
ループ内のswitch
ステートメントが変更されました。- 以前は
required[0] < features[0]
の条件で、required
に存在するがfeatures
に存在しないAPI要素(つまり削除されたAPI)を検出していました。 - 変更後、このケースに
else if featureSet[featureWithoutContext(feature)]
という新しい条件が追加されました。これは、required
にあるAPI要素が、features
に存在するAPI要素の「コンテキストなしバージョン」と一致する場合、それを互換性のある変更と見なすことを意味します。- 具体的には、
required
にpkg syscall (darwin-386), type RawSockaddrInet6 struct
があり、features
にpkg syscall, type RawSockaddrInet6 struct
がある場合、featureWithoutContext
を適用することで両者が一致し、互換性のある変更として扱われます。これにより、コンテキストの再収束が正しく処理されます。
- 具体的には、
optionalSet
とexceptionSet
の初期化も、新しく追加されたset
関数を使用するように変更され、より簡潔になりました。
- 以前は
コアとなるコードの変更箇所
src/cmd/api/goapi.go
--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -29,6 +29,7 @@ import (
"os/exec"
"path"
"path/filepath"
+ "regexp"
"runtime"
"sort"
"strconv"
@@ -192,17 +193,29 @@ func main() {
fail = !compareAPI(bw, features, required, optional, exception)
}
+func set(items []string) map[string]bool {
+ s := make(map[string]bool)
+ for _, v := range items {
+ s[v] = true
+ }
+ return s
+}
+
+var spaceParensRx = regexp.MustCompile(` \(\S+?\)`)\n
+
+func featureWithoutContext(f string) string {
+ if !strings.Contains(f, "(") {
+ return f
+ }
+ return spaceParensRx.ReplaceAllString(f, "")
+}
+
func compareAPI(w io.Writer, features, required, optional, exception []string) (ok bool) {
ok = true
- var optionalSet = make(map[string]bool) // feature => true
- var exceptionSet = make(map[string]bool) // exception => true
- for _, f := range optional {
- optionalSet[f] = true
- }
- for _, f := range exception {
- exceptionSet[f] = true
- }
+ optionalSet := set(optional)
+ exceptionSet := set(exception)
+ featureSet := set(features)
sort.Strings(features)
sort.Strings(required)
@@ -215,15 +228,17 @@ func compareAPI(w io.Writer, features, required, optional, exception []string) (
for len(required) > 0 || len(features) > 0 {
switch {
-\t\tcase len(features) == 0 || required[0] < features[0]:
+\t\tcase len(features) == 0 || (len(required) > 0 && required[0] < features[0]):
feature := take(&required)
if exceptionSet[feature] {
fmt.Fprintf(w, "~%s\\n\", feature)
+\t\t\t} else if featureSet[featureWithoutContext(feature)] {
+\t\t\t\t// okay.
} else {
fmt.Fprintf(w, "-%s\\n\", feature)
ok = false // broke compatibility
}
-\t\tcase len(required) == 0 || required[0] > features[0]:
+\t\tcase len(required) == 0 || (len(features) > 0 && required[0] > features[0]):
newFeature := take(&features)
if optionalSet[newFeature] {
// Known added feature to the upcoming release.
src/cmd/api/goapi_test.go
--- a/src/cmd/api/goapi_test.go
+++ b/src/cmd/api/goapi_test.go
@@ -84,10 +84,10 @@ func TestCompareAPI(t *testing.T) {
}{
{
name: "feature added",
-\t\t\tfeatures: []string{"C", "A", "B"},
-\t\t\trequired: []string{"A", "C"},
+\t\t\tfeatures: []string{"A", "B", "C", "D", "E", "F"},
+\t\t\trequired: []string{"B", "D"},
ok: true,
-\t\t\tout: "+B\n",
+\t\t\tout: "+A\n+C\n+E\n+F\n",
},
{
name: "feature removed",
@@ -112,6 +112,21 @@ func TestCompareAPI(t *testing.T) {
ok: true,
out: "~B\n",
},
+\t\t{\n
+\t\t\t// http://golang.org/issue/4303\n
+\t\t\tname: "contexts reconverging",
+\t\t\trequired: []string{\n
+\t\t\t\t"A",\n
+\t\t\t\t"pkg syscall (darwin-386), type RawSockaddrInet6 struct",\n
+\t\t\t\t"pkg syscall (darwin-amd64), type RawSockaddrInet6 struct",\n
+\t\t\t},\n
+\t\t\tfeatures: []string{\n
+\t\t\t\t"A",\n
+\t\t\t\t"pkg syscall, type RawSockaddrInet6 struct",\n
+\t\t\t},\n
+\t\t\tok: true,\n
+\t\t\tout: "+pkg syscall, type RawSockaddrInet6 struct\n",\n
+\t\t},\n
}\n for _, tt := range tests {
buf := new(bytes.Buffer)
コアとなるコードの解説
src/cmd/api/goapi.go
の変更点
regexp
パッケージのインポート: 正規表現を使用するためにregexp
パッケージが追加されました。set
関数の導入:
この関数は、文字列のスライスをfunc set(items []string) map[string]bool { s := make(map[string]bool) for _, v := range items { s[v] = true } return s }
map[string]bool
に変換するユーティリティです。これにより、要素の存在チェックを効率的に行えるようになります。optionalSet
とexceptionSet
の初期化がこの関数を使うように変更され、コードがより簡潔になりました。また、featureSet
という新しいセットが追加され、features
スライスから作成されます。これは、現在のAPIセットに存在するすべてのAPI要素を効率的に検索するために使用されます。spaceParensRx
とfeatureWithoutContext
の導入:var spaceParensRx = regexp.MustCompile(` \(\S+?\)`)\n func featureWithoutContext(f string) string { if !strings.Contains(f, "(") { return f } return spaceParensRx.ReplaceAllString(f, "") }
spaceParensRx
は、(コンテキスト)
のパターンにマッチする正規表現です。featureWithoutContext
関数は、API要素の文字列からこのコンテキスト部分を削除し、コンテキストに依存しないAPI要素の「ベース名」を抽出します。これは、異なるコンテキストで定義されたAPIが、より一般的なコンテキストで再定義された場合に、それらを同じAPIとして認識するために不可欠です。compareAPI
関数のロジック修正:compareAPI
関数は、required
(以前のAPIセット) とfeatures
(現在のAPIセット) を比較し、互換性のない変更を検出します。 変更されたswitch
ステートメントの最初のcase
は、required
に存在するがfeatures
に存在しないAPI要素(つまり削除されたAPI)を処理します。
ここで重要なのは、case len(features) == 0 || (len(required) > 0 && required[0] < features[0]): feature := take(&required) if exceptionSet[feature] { fmt.Fprintf(w, "~%s\n", feature) } else if featureSet[featureWithoutContext(feature)] { // <-- 追加されたロジック // okay. } else { fmt.Fprintf(w, "-%s\n", feature) ok = false // broke compatibility }
else if featureSet[featureWithoutContext(feature)]
の行です。required
からfeature
を取り出します。- もし
feature
がexceptionSet
に含まれていれば、それは例外として扱われます。 - そうでなく、かつ
feature
のコンテキストなしバージョン (featureWithoutContext(feature)
) がfeatureSet
(現在のAPIセット) に存在する場合、これは「コンテキストの再収束」と見なされ、互換性のある変更として扱われます。つまり、特定のコンテキストに依存していたAPIが、より一般的な形で提供されるようになった場合、これは互換性を壊す変更ではないと判断されます。 - 上記のいずれにも該当しない場合、その
feature
は削除されたと見なされ、互換性のない変更として-
プレフィックスで出力され、ok
がfalse
に設定されます。
src/cmd/api/goapi_test.go
の変更点
- 新しいテストケースの追加:
この新しいテストケースは、まさに「コンテキストの再収束」のシナリオを検証するために追加されました。{ // http://golang.org/issue/4303 name: "contexts reconverging", required: []string{ "A", "pkg syscall (darwin-386), type RawSockaddrInet6 struct", "pkg syscall (darwin-amd64), type RawSockaddrInet6 struct", }, features: []string{ "A", "pkg syscall, type RawSockaddrInet6 struct", }, ok: true, out: "+pkg syscall, type RawSockaddrInet6 struct\n", },
required
(以前のAPIセット) には、A
という一般的なAPIと、darwin-386
およびdarwin-amd64
という特定のコンテキストを持つRawSockaddrInet6 struct
が含まれています。features
(現在のAPIセット) には、A
と、コンテキストを持たないpkg syscall, type RawSockaddrInet6 struct
が含まれています。- 期待される結果は
ok: true
であり、out: "+pkg syscall, type RawSockaddrInet6 struct\n"
です。これは、コンテキスト固有のAPIが削除され、より一般的なAPIに置き換えられた場合でも、cmd/api
がそれを互換性のある変更として認識し、新しいAPIが追加されたものとして報告することを示しています。
関連リンク
- Go言語のAPI互換性に関するドキュメントやポリシー(公式ドキュメントを参照)
cmd/api
ツールの詳細な説明(Goのソースコードリポジトリ内の関連ドキュメントやコメントを参照)
参考にした情報源リンク
- Go言語の公式ドキュメント: https://go.dev/
- Go言語のソースコードリポジトリ: https://github.com/golang/go
- Go言語のイシュートラッカー(ただし、#4303は現在公開されていないようです)
- 正規表現に関する一般的な情報源(例: https://ja.wikipedia.org/wiki/%E6%AD%A3%E8%A6%8F%E8%A1%A8%E7%8F%BE)