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

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

このコミットは、Go言語のコマンドラインツール cmd/go における、ディレクトリパスからインポートパスへの変換ロジックのバグを修正するものです。具体的には、ファイルシステム上のディレクトリ名にGoのインポートパスとして不正な文字が含まれている場合に、正しくインポートパスを生成できない問題を解決します。これにより、特殊文字を含むディレクトリからのパッケージのビルドやテストが正常に行われるようになります。

コミット

commit 95a8bab7b6db9c63281a0055b1a2471818129cd5
Author: Russ Cox <rsc@golang.org>
Date:   Wed Mar 14 15:12:57 2012 -0400

    cmd/go: fix directory->import path conversion
    
    Fixes #3306.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5821048

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

https://github.com/golang/go/commit/95a8bab7b6db9c63281a0055b1a2471818129cd5

元コミット内容

cmd/go: fix directory->import path conversion

Fixes #3306.

変更の背景

Go言語では、ソースコードのパッケージはファイルシステム上のディレクトリ構造と密接に関連しています。go buildgo install などのコマンドは、指定されたディレクトリパスをGoのインポートパスに変換して処理を行います。しかし、従来の dirToImportPath 関数は、ディレクトリ名に含まれる特定の特殊文字(例: :)を単純に _ に置換するだけでした。

このアプローチでは、Goのインポートパスとして許可されていない文字(例: !, ", #, $, %, &, ', (, ), *, ,, :, ;, <, =, >, ?, [, \, ], ^, {, |, }, `, \uFFFD (Unicode replacement character) など)がディレクトリ名に含まれていた場合、生成されるインポートパスがGoの仕様に準拠せず、ビルドエラーや予期せぬ動作を引き起こす可能性がありました。

このコミットは、この問題を解決し、ディレクトリパスから常に有効なGoインポートパスを生成できるようにすることで、より堅牢なGo開発環境を提供することを目的としています。特に、ファイルシステムが許容する文字とGoのインポートパスが許容する文字の間のギャップを埋めることが重要でした。

前提知識の解説

  • Goインポートパス: Go言語では、パッケージはインポートパスによって識別されます。これは通常、リポジトリのURLやファイルシステム上のパスに似た文字列です(例: github.com/user/repo/package../subpackage)。Goのインポートパスには、使用できる文字に厳密な制限があります。これは、Goの仕様、コンパイラ、および go/parser パッケージの isValidImport 関数によって定義されています。
  • filepath.ToSlash: Goの標準ライブラリ path/filepath パッケージの関数で、オペレーティングシステム固有のパス区切り文字(Windowsの \ など)をスラッシュ (/) に変換します。これにより、パスがプラットフォームに依存しない形式になります。
  • pathpkg.Join: Goの標準ライブラリ path パッケージの関数で、複数のパス要素を結合してクリーンなパスを生成します。これは、ファイルシステムパスではなく、スラッシュ区切りのパス(URLパスなど)を扱うのに適しています。
  • unicode パッケージ: Goの標準ライブラリ unicode パッケージは、Unicode文字のプロパティを扱うための関数を提供します。
    • unicode.IsGraphic(r rune) bool: ルーン r がグラフィック文字(表示可能な文字)であるかどうかを判定します。
    • unicode.IsSpace(r rune) bool: ルーン r が空白文字であるかどうかを判定します。
  • strings.ContainsRune(s string, r rune) bool: 文字列 s にルーン r が含まれているかどうかを判定します。
  • strings.Map(mapping func(rune) rune, s string) string: 文字列 s の各ルーンに mapping 関数を適用し、その結果として新しい文字列を生成します。これは、文字列内の文字を変換する強力な方法です。
  • go install: Goのコマンドで、パッケージをコンパイルし、その結果の実行可能ファイルまたはアーカイブファイルを $GOPATH/bin または $GOPATH/pkg にインストールします。このコマンドは、インポートパスの解決に依存しています。

技術的詳細

このコミットの主要な変更点は、src/cmd/go/pkg.go ファイル内の dirToImportPath 関数に makeImportValid という新しいヘルパー関数を導入したことです。

変更前: dirToImportPath 関数は、ディレクトリパスをスラッシュ形式に変換した後、コロン (:) をアンダースコア (_) に置換するだけでした。

func dirToImportPath(dir string) string {
	return pathpkg.Join("_", strings.Replace(filepath.ToSlash(dir), ":", "_", -1))
}

この単純な置換では、Goのインポートパスとして不正な他の多くの特殊文字が処理されませんでした。

変更後: 新しい makeImportValid 関数が導入され、strings.Map を使用してディレクトリパスの各文字を検証・変換するようになりました。

func dirToImportPath(dir string) string {
	return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
}

func makeImportValid(r rune) rune {
	// Should match Go spec, compilers, and ../../pkg/go/parser/parser.go:/isValidImport.
	const illegalChars = `!"#$%&'()*,:;<=>?[\\]^{|}` + "`\uFFFD"
	if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
		return '_'
	}
	return r
}

makeImportValid 関数は以下のロジックで動作します。

  1. Goのインポートパスとして不正な文字のセット illegalChars を定義します。これには、Goの仕様、コンパイラ、および go/parser/parser.goisValidImport 関数で定義されている不正な文字が含まれます。
  2. 入力されたルーン r が以下のいずれかの条件を満たす場合、そのルーンをアンダースコア (_) に変換します。
    • !unicode.IsGraphic(r): ルーンがグラフィック文字ではない場合(例: 制御文字)。
    • unicode.IsSpace(r): ルーンが空白文字である場合。
    • strings.ContainsRune(illegalChars, r): ルーンが illegalChars 定数に含まれる不正な文字である場合。
  3. 上記の条件に当てはまらない場合、ルーンはそのまま返されます。

この変更により、dirToImportPath は、ファイルシステム上のディレクトリ名にどのような文字が含まれていても、Goのインポートパスとして常に有効な文字列を生成するようになりました。

また、src/cmd/go/test.bash にも変更が加えられ、この修正を検証するための新しいテストケースが追加されました。特に、ディレクトリ名に不正な文字(#$%:, &()*;<=>?\^{})を含むパスで go buildgo install が正しく動作するかを確認する testlocal 関数が導入され、その関数が bad という変数で定義された特殊文字を含むディレクトリ名で呼び出されています。これにより、この修正が意図通りに機能していることが保証されます。

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

src/cmd/go/pkg.go の変更:

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -17,6 +17,7 @@ import (
 	"sort"
 	"strings"
 	"time"
+	"unicode"
 )
 
 // A Package describes a single package found in a directory.
@@ -174,7 +175,16 @@ func reloadPackage(arg string, stk *importStack) *Package {
 // a special case, so that all the code to deal with ordinary imports works
 // automatically.
 func dirToImportPath(dir string) string {
-	return pathpkg.Join("_", strings.Replace(filepath.ToSlash(dir), ":", "_", -1))
+	return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
+}
+
+func makeImportValid(r rune) rune {
+	// Should match Go spec, compilers, and ../../pkg/go/parser/parser.go:/isValidImport.
+	const illegalChars = `!"#$%&'()*,:;<=>?[\\]^{|}` + "`\uFFFD"
+	if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
+		return '_'
+	}
+	return r
 }
 
 // loadImport scans the directory named by path, which must be an import path,

src/cmd/go/test.bash の変更:

テストスクリプトに testlocal 関数が追加され、特殊文字を含むディレクトリ名でのテストが実行されるようになりました。

--- a/src/cmd/go/test.bash
+++ b/src/cmd/go/test.bash
@@ -22,37 +22,50 @@ do
 done
 
 # Test local (./) imports.
-./testgo build -o hello testdata/local/easy.go
-./hello >hello.out
-if ! grep -q '^easysub\.Hello' hello.out; then
-	echo "testdata/local/easy.go did not generate expected output"
-	cat hello.out
-	ok=false
-fi
-
-./testgo build -o hello testdata/local/easysub/main.go
-./hello >hello.out
-if ! grep -q '^easysub\.Hello' hello.out; then
-	echo "testdata/local/easysub/main.go did not generate expected output"
-	cat hello.out
-	ok=false
-fi
-
-./testgo build -o hello testdata/local/hard.go
-./hello >hello.out
-if ! grep -q '^sub\.Hello' hello.out || ! grep -q '^subsub\.Hello' hello.out ; then
-	echo "testdata/local/hard.go did not generate expected output"
-	cat hello.out
-	ok=false
-fi
+testlocal() {
+	local="`$1`"
+	./testgo build -o hello "testdata/$local/easy.go"
+	./hello >hello.out
+	if ! grep -q '^easysub\.Hello' hello.out; then
+		echo "testdata/$local/easy.go did not generate expected output"
+		cat hello.out
+		ok=false
+	fi
+	
+	./testgo build -o hello "testdata/$local/easysub/main.go"
+	./hello >hello.out
+	if ! grep -q '^easysub\.Hello' hello.out; then
+		echo "testdata/$local/easysub/main.go did not generate expected output"
+		cat hello.out
+		ok=false
+	fi
+	
+	./testgo build -o hello "testdata/$local/hard.go"
+	./hello >hello.out
+	if ! grep -q '^sub\.Hello' hello.out || ! grep -q '^subsub\.Hello' hello.out ; then
+		echo "testdata/$local/hard.go did not generate expected output"
+		cat hello.out
+		ok=false
+	fi
+	
+	rm -f err.out hello.out hello
+	
+	# Test that go install x.go fails.
+	if ./testgo install "testdata/$local/easy.go" >/dev/null 2>&1; then
+		echo "go install testdata/$local/easy.go succeeded"
+		ok=false
+	fi
+}
 
-rm -f err.out hello.out hello
+# Test local imports
+testlocal local
 
-# Test that go install x.go fails.
-if ./testgo install testdata/local/easy.go >/dev/null 2>&1; then
-	echo "go install testdata/local/easy.go succeeded"
-	ok=false
-fi
+# Test local imports again, with bad characters in the directory name.
+bad='#$%:, &()*;<=>?\^{}'
+rm -rf "testdata/$bad"
+cp -R testdata/local "testdata/$bad"
+testlocal "$bad"
+rm -rf "testdata/$bad"
 
 # Test tests with relative imports.
 if ! ./testgo test ./testdata/testimport; then

コアとなるコードの解説

このコミットの核心は、dirToImportPath 関数が strings.MapmakeImportValid 関数を組み合わせて、ディレクトリパスの各文字をGoのインポートパスの要件に適合するように変換する点にあります。

  • dirToImportPath 関数:

    • まず filepath.ToSlash(dir) を呼び出し、OS固有のパス区切り文字をスラッシュに統一します。
    • 次に、strings.Map(makeImportValid, ...) を使用して、変換されたパス文字列の各ルーン(Unicodeコードポイント)に対して makeImportValid 関数を適用します。これにより、パス内のすべての文字がGoのインポートパスとして有効な形式に変換されます。
    • 最後に pathpkg.Join("_", ...) を使用して、変換されたパスの先頭に _ を付加します。これは、Goの内部的なインポートパスの処理における慣習的なプレフィックスであり、通常のインポートパスとの衝突を避ける目的があります。
  • makeImportValid 関数:

    • この関数は、Goのインポートパスの文字セットに関する厳密なルールを実装しています。
    • illegalChars 定数には、Goのインポートパスとして明示的に禁止されている文字が列挙されています。これには、Goの仕様やコンパイラ、そしてGoのパーサーがインポートパスを検証する際に使用するルールが反映されています。
    • !unicode.IsGraphic(r) は、表示できない制御文字などを除外します。
    • unicode.IsSpace(r) は、空白文字を除外します。
    • strings.ContainsRune(illegalChars, r) は、定義された不正な文字セットに含まれる文字を除外します。
    • これらの条件のいずれかに合致する文字は、すべてアンダースコア (_) に置換されます。これにより、不正な文字がインポートパスに含まれることを防ぎ、常に有効なインポートパスが生成されるようになります。

この修正により、Goツールチェーンは、ファイルシステム上の多様なディレクトリ名(特に特殊文字を含むもの)に対しても、堅牢かつ予測可能なインポートパスの変換を提供できるようになりました。

関連リンク

参考にした情報源リンク

  • コミット情報: /home/orange/Project/comemo/commit_data/12636.txt
  • Go言語の公式ドキュメント (Goの仕様、パッケージ、インポートパスに関する情報)
  • Goのソースコード (src/cmd/go/pkg.go, src/cmd/go/test.bash, go/parser/parser.go)