[インデックス 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)