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

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

このコミットは、Go言語の型チェッカー (go/types パッケージ) におけるインポート処理の堅牢性を向上させるためのものです。具体的には、以下のファイルが変更されています。

  • src/pkg/go/types/expr.go: 式の型チェックに関連するファイル。特にセレクタ式(pkg.Nameのような形式)の処理が変更されています。
  • src/pkg/go/types/objects.go: 型チェッカーが扱う各種オブジェクト(パッケージ、定数、型名、変数、関数など)の定義と、それらのプロパティ(特にソースコード上の位置情報 token.Pos)を取得するメソッドが変更されています。
  • src/pkg/go/types/resolve.go: スコープ内の名前解決と宣言処理に関連するファイル。特にインポートされたオブジェクトの宣言と、ドットインポート時の衝突解決ロジックが変更されています。
  • src/pkg/go/types/testdata/decls0.src: 型チェッカーのテストデータ。ドットインポートと非エクスポートオブジェクトの可視性に関するテストケースが追加されています。
  • src/pkg/go/types/testdata/decls1.src: 型チェッカーのテストデータ。既存のテストケースが修正されています。

これらの変更は、Goコンパイラ (gc) が生成するエクスポートデータと、型チェッカーがそのデータを解釈する際の挙動の不整合を解消し、より正確でユーザーフレンドリーなエラーメッセージを提供することを目的としています。

コミット

commit 3f132a82365f49cda015b8c3ac694947d3ca54ae
Author: Robert Griesemer <gri@golang.org>
Date:   Mon Feb 25 20:43:35 2013 -0800

    go/types: more robust imports
    
    - imported objects don't have position information
    - gc exported data contains non-exported objects at
      the top-level, guard against them
    - better error message when dot-imports conflict
      with local declarations
    
    R=adonovan, r
    CC=golang-dev
    https://golang.org/cl/7379052

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

https://github.com/golang/go/commit/3f132a82365f49cda015b8c3ac694947d3ca54ae

元コミット内容

    go/types: more robust imports
    
    - imported objects don't have position information
    - gc exported data contains non-exported objects at
      the top-level, guard against them
    - better error message when dot-imports conflict
      with local declarations

変更の背景

このコミットは、Go言語の型チェッカーが直面していたいくつかの問題に対処するために導入されました。

  1. インポートされたオブジェクトの位置情報欠如: Goコンパイラ (gc) がパッケージをコンパイルし、その型情報をエクスポートする際、インポートされるオブジェクト(型、関数、変数など)の元のソースコード上の正確な位置情報 (token.Pos) が失われることがありました。型チェッカーがこれらのオブジェクトを参照する際に、位置情報が期待される場所で nil や無効な値が返されると、パニックを引き起こしたり、不正確なエラー報告につながる可能性がありました。
  2. gc エクスポートデータ内の非エクスポートオブジェクト: Go言語の仕様では、パッケージ外からアクセスできるのはエクスポートされた(名前が大文字で始まる)識別子のみです。しかし、gc コンパイラが生成するエクスポートデータには、エクスポートされた型が内部的に使用する非エクスポートオブジェクト(例えば、構造体のフィールドの型など)がトップレベルに誤って含まれてしまうケースがありました。型チェッカーがこれを適切に処理しないと、本来アクセスできないはずの非エクスポートオブジェクトが誤って参照可能と判断され、不正なコードがコンパイルされてしまうリスクがありました。
  3. ドットインポートとローカル宣言の衝突時のエラーメッセージの不明瞭さ: ドットインポート (import . "pkg") は、インポートされたパッケージのエクスポートされた識別子を、現在のパッケージのスコープに直接取り込みます。この際、インポートされた識別子とローカルで宣言された識別子の名前が衝突すると、再宣言エラーが発生します。しかし、以前のエラーメッセージは、どちらが原因で衝突しているのか、特にドットインポートが原因である場合に、その旨を明確に示していませんでした。これにより、開発者が問題の原因を特定しにくくなっていました。

これらの問題を解決し、型チェッカーの正確性、堅牢性、そしてユーザーエクスペリエンスを向上させることが、このコミットの主な目的です。

前提知識の解説

このコミットの理解には、以下のGo言語およびコンパイラの概念に関する知識が役立ちます。

Go言語のパッケージとエクスポート/アンエクスポートの概念

Go言語のコードは「パッケージ」という単位で組織されます。パッケージは、関連する機能やデータ型をまとめるための基本的なモジュール化の単位です。

  • エクスポートされた識別子 (Exported Identifiers): パッケージ外からアクセス可能な識別子(変数、関数、型など)は、その名前が大文字で始まる必要があります。例えば、fmt.PrintlnPrintln はエクスポートされた関数です。
  • 非エクスポートされた識別子 (Unexported Identifiers): パッケージ内でのみアクセス可能な識別子(変数、関数、型など)は、その名前が小文字で始まります。これらはパッケージの内部実装の詳細であり、外部からは隠蔽されます。

Goコンパイラ (gc) とそのエクスポートデータ (gc export data) の役割

Go言語の公式コンパイラは gc と呼ばれます。gc はGoのソースコードをコンパイルして実行可能なバイナリを生成します。 コンパイルの過程で、gc は他のパッケージからインポートされる可能性のあるパッケージの型情報を「エクスポートデータ」として生成します。このエクスポートデータは、コンパイル済みパッケージの .a ファイル(アーカイブファイル)内に格納され、他のパッケージがそのパッケージをインポートする際に型チェックやリンクのために利用されます。このデータは、Goの型システムがパッケージ間の依存関係を解決し、型安全性を保証するために不可欠です。

Goの型チェッカー (go/types パッケージ) の役割

go/types パッケージは、Go言語のソースコードの静的型チェックを行うためのライブラリです。これはGoコンパイラの一部として、またGoツールチェインの他の部分(例えば、go vet やIDEのコード補完機能など)でも利用されます。 型チェッカーの主な役割は以下の通りです。

  • ソースコードがGo言語の型規則に準拠しているか検証する。
  • 識別子の解決(どの宣言がどの識別子に対応するかを決定する)。
  • 式の型を決定する。
  • 型変換や代入の妥当性をチェックする。
  • パッケージ間のインポート関係を解決し、インポートされたオブジェクトの型情報を利用する。

token.Pos とソースコード上の位置情報

token.Pos は、Goのパーサーや型チェッカーがソースコード内の特定のトークン(識別子、キーワード、演算子など)がファイル内のどこに位置するかを示すために使用する型です。これは通常、ファイル名、行番号、列番号などの情報を含みます。エラーメッセージの生成やデバッグにおいて、正確な位置情報は非常に重要です。token.NoPos は、有効な位置情報がないことを示す特別な値です。

ドットインポート (import . "pkg") の挙動とスコープ

通常のインポート (import "pkg") は、インポートされたパッケージの識別子を pkg.Identifier の形式で参照することを要求します。 一方、ドットインポート (import . "pkg") は、インポートされたパッケージのエクスポートされた識別子を、現在のパッケージのスコープに直接取り込みます。これにより、pkg.Identifier と書く代わりに Identifier と書くだけで参照できるようになります。 例: import . "fmt" とすると、fmt.Println("Hello") の代わりに Println("Hello") と書けます。 利便性が高い一方で、ドットインポートは名前の衝突を引き起こしやすく、コードの可読性を低下させる可能性があるため、慎重に使用する必要があります。

ast.IsExported 関数の役割

go/ast パッケージは、Goのソースコードを抽象構文木 (AST) として表現するためのデータ構造と関数を提供します。ast.IsExported(name string) 関数は、与えられた文字列 name がGoの識別子のエクスポート規則(大文字で始まるか)に準拠しているかどうかをチェックします。この関数は、型チェッカーがインポートされたオブジェクトが実際にエクスポートされているかどうかを検証するために使用されます。

技術的詳細

このコミットは、Goの型チェッカー (go/types) がインポートされたパッケージの情報を処理する方法を改善し、より堅牢で正確な型チェックとエラー報告を実現します。

1. gc エクスポートデータ内の非エクスポートオブジェクトのガード

Goコンパイラ (gc) が生成するエクスポートデータには、本来外部に公開されるべきではない非エクスポートオブジェクトが誤って含まれることがありました。これは、エクスポートされた型が内部的に非エクスポート型を使用している場合などに発生し得ます。型チェッカーは、このエクスポートデータを読み込む際に、これらの非エクスポートオブジェクトを誤って参照可能と判断してしまう可能性がありました。

このコミットでは、src/pkg/go/types/expr.gosrc/pkg/go/types/resolve.goast.IsExported 関数を用いたチェックを追加することで、この問題に対処しています。

  • src/pkg/go/types/expr.go の変更: セレクタ式(例: pkg.Name)を解決する際、pkg.Scope.Lookup(sel) で見つかったオブジェクトが実際にエクスポートされているか (ast.IsExported(exp.GetName())) を確認するようになりました。もしオブジェクトが見つからないか、または非エクスポートオブジェクトである場合、エラーを報告します。これにより、gc エクスポートデータに誤って含まれた非エクスポートオブジェクトへの不正な参照を防ぎます。

    --- a/src/pkg/go/types/expr.go
    +++ b/src/pkg/go/types/expr.go
    @@ -900,8 +900,11 @@ func (check *checker) rawExpr(x *operand, e ast.Expr, hint Type, iota int, cycle
     		if ident, ok := e.X.(*ast.Ident); ok {
     			if pkg, ok := check.lookup(ident).(*Package); ok {
     				exp := pkg.Scope.Lookup(sel)
    -				if exp == nil {
    -					check.errorf(e.Sel.Pos(), "cannot refer to unexported %s", sel)
    +				// gcimported package scopes contain non-exported
    +				// objects such as types used in partially exported
    +				// objects - do not accept them
    +				if exp == nil || !ast.IsExported(exp.GetName()) {
    +					check.errorf(e.Pos(), "cannot refer to unexported %s", e)
     					goto Error
     				}
     				check.register(e.Sel, exp)
    
  • src/pkg/go/types/resolve.go の変更: ドットインポート (import . "pkg") の処理において、インポートされたパッケージのスコープからオブジェクトを現在のファイルスコープにマージする際、そのオブジェクトがエクスポートされているか (ast.IsExported(obj.GetName())) を厳密にチェックするようになりました。非エクスポートオブジェクトはマージされません。

    --- a/src/pkg/go/types/resolve.go
    +++ b/src/pkg/go/types/resolve.go
    @@ -137,7 +151,12 @@ func (check *checker) resolve(importer Importer) (methods []*ast.FuncDecl) {
     		if name == "." {
     			// merge imported scope with file scope
     			for _, obj := range imp.Scope.Entries {
    -				check.declareObj(fileScope, pkg.Scope, obj)
    +				// gcimported package scopes contain non-exported
    +				// objects such as types used in partially exported
    +				// objects - do not accept them
    +				if ast.IsExported(obj.GetName()) {
    +					check.declareObj(fileScope, pkg.Scope, obj, spec.Pos())
    +				}
     			}
     			// TODO(gri) consider registering the "." identifier
     			// if we have Context.Ident callbacks for say blank
    

2. インポートされたオブジェクトの位置情報欠如への対応

インポートされたオブジェクトは、元のソースコード上の位置情報 (token.Pos) を持たない場合があります。以前の実装では、これらのオブジェクトに対して GetPos() メソッドが呼ばれた際に、無効なポインタ参照やパニックを引き起こす可能性がありました。

このコミットでは、src/pkg/go/types/objects.go 内の GetPos() メソッドの実装を堅牢化し、specdecl フィールドが nil の場合に token.NoPos を返すように変更しました。これにより、位置情報が利用できない場合でも安全に処理を続行できるようになります。

  • Package.GetPos(): obj.specnil の場合、token.NoPos を返します。
  • TypeName.GetPos(): obj.specnil の場合、token.NoPos を返します。
  • Func.GetPos(): obj.decl または obj.decl.Namenil の場合、token.NoPos を返します。
--- a/src/pkg/go/types/objects.go
+++ b/src/pkg/go/types/objects.go
@@ -89,7 +89,13 @@ func (obj *TypeName) GetType() Type { return obj.Type }
 func (obj *Var) GetType() Type      { return obj.Type }
 func (obj *Func) GetType() Type     { return obj.Type }
 
-func (obj *Package) GetPos() token.Pos { return obj.spec.Pos() }
+func (obj *Package) GetPos() token.Pos {
+	if obj.spec != nil {
+		return obj.spec.Pos()
+	}
+	return token.NoPos
+}
+
 func (obj *Const) GetPos() token.Pos {
 	for _, n := range obj.spec.Names {
 		if n.Name == obj.Name {
@@ -98,7 +104,13 @@ func (obj *Const) GetPos() token.Pos {
 	}
 	return token.NoPos
 }
-func (obj *TypeName) GetPos() token.Pos { return obj.spec.Pos() }
+func (obj *TypeName) GetPos() token.Pos {
+	if obj.spec != nil {
+		return obj.spec.Pos()
+	}
+	return token.NoPos
+}
+
 func (obj *Var) GetPos() token.Pos {
 	switch d := obj.decl.(type) {
 	case *ast.Field:
@@ -122,7 +134,12 @@ func (obj *Var) GetPos() token.Pos {
 	}
 	return token.NoPos
 }
-func (obj *Func) GetPos() token.Pos { return obj.decl.Name.Pos() }
+func (obj *Func) GetPos() token.Pos {
+	if obj.decl != nil && obj.decl.Name != nil {
+		return obj.decl.Name.Pos()
+	}
+	return token.NoPos
+}
 
 func (*Package) anObject()  {}
 func (*Const) anObject()    {}

3. ドットインポートとローカル宣言の衝突時のエラーメッセージ改善

ドットインポートによってインポートされた識別子が、現在のスコープ内のローカル宣言と名前衝突を起こした場合、以前のエラーメッセージは原因が不明瞭でした。このコミットでは、src/pkg/go/types/resolve.godeclareObj 関数を変更し、より詳細で分かりやすいエラーメッセージを提供するようにしました。

  • declareObj 関数に dotImport token.Pos 引数が追加されました。これは、オブジェクトがドットインポートによって宣言された場合に、そのインポート文の位置情報を示します。
  • 名前衝突が発生し、かつ dotImport が有効な位置情報を持つ場合、エラーメッセージは「%s redeclared in this block by dot-import at %s」(%s はオブジェクト名、%s はドットインポートの位置)という形式になります。これにより、衝突の原因がドットインポートであることが明確に示されます。
  • また、以前の宣言の位置を示すメッセージも「previous declaration at %s」から「other declaration at %s」に変更され、より一般的な表現になりました。
--- a/src/pkg/go/types/resolve.go
+++ b/src/pkg/go/types/resolve.go
@@ -11,7 +11,7 @@ import (
 	"strconv"
 )
 
-func (check *checker) declareObj(scope, altScope *Scope, obj Object) {
+func (check *checker) declareObj(scope, altScope *Scope, obj Object, dotImport token.Pos) {
 	alt := scope.Insert(obj)
 	if alt == nil && altScope != nil {
 		// see if there is a conflicting declaration in altScope
@@ -19,8 +19,22 @@ func (check *checker) declareObj(scope, altScope *Scope, obj Object) {
 	}
 	if alt != nil {
 		prevDecl := ""
+\
+\t\t// for dot-imports, local declarations are declared first - swap messages
+\t\tif dotImport.IsValid() {
+\t\t\tif pos := alt.GetPos(); pos.IsValid() {
+\t\t\t\tcheck.errorf(pos, fmt.Sprintf("%s redeclared in this block by dot-import at %s",
+\t\t\t\t\tobj.GetName(), check.fset.Position(dotImport)))
+\t\t\t\treturn
+\t\t\t}
+\
+\t\t\t// get by w/o other position
+\t\t\tcheck.errorf(dotImport, fmt.Sprintf("dot-import redeclares %s", obj.GetName()))
+\t\t\treturn
+\t\t}
+\
 		if pos := alt.GetPos(); pos.IsValid() {
-\t\t\tprevDecl = fmt.Sprintf("\n\tprevious declaration at %s", check.fset.Position(pos))
+\t\t\tprevDecl = fmt.Sprintf("\n\tother declaration at %s", check.fset.Position(pos))
 		}
 		check.errorf(obj.GetPos(), fmt.Sprintf("%s redeclared in this block%s", obj.GetName(), prevDecl))
 	}
@@ -149,7 +168,7 @@ func (check *checker) resolve(importer Importer) (methods []*ast.FuncDecl) {
 			// a new object instead; the Decl field is different
 			// for different files)
 			obj := &Package{Name: name, Scope: imp.Scope, spec: spec}
-			check.declareObj(fileScope, pkg.Scope, obj)
+			check.declareObj(fileScope, pkg.Scope, obj, token.NoPos)
 		}
 	}

これらの変更により、Goの型チェッカーはより正確に型を検証し、開発者に対してより分かりやすいエラーメッセージを提供できるようになりました。

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

src/pkg/go/types/expr.go

// gcimported package scopes contain non-exported
// objects such as types used in partially exported
// objects - do not accept them
if exp == nil || !ast.IsExported(exp.GetName()) {
	check.errorf(e.Pos(), "cannot refer to unexported %s", e)
	goto Error
}

src/pkg/go/types/objects.go

func (obj *Package) GetPos() token.Pos {
	if obj.spec != nil {
		return obj.spec.Pos()
	}
	return token.NoPos
}

func (obj *TypeName) GetPos() token.Pos {
	if obj.spec != nil {
		return obj.spec.Pos()
	}
	return token.NoPos
}

func (obj *Func) GetPos() token.Pos {
	if obj.decl != nil && obj.decl.Name != nil {
		return obj.decl.Name.Pos()
	}
	return token.NoPos
}

src/pkg/go/types/resolve.go

func (check *checker) declareObj(scope, altScope *Scope, obj Object, dotImport token.Pos) {
	alt := scope.Insert(obj)
	if alt == nil && altScope != nil {
		// see if there is a conflicting declaration in altScope
		alt = altScope.Insert(obj)
	}
	if alt != nil {
		prevDecl := ""

		// for dot-imports, local declarations are declared first - swap messages
		if dotImport.IsValid() {
			if pos := alt.GetPos(); pos.IsValid() {
				check.errorf(pos, fmt.Sprintf("%s redeclared in this block by dot-import at %s",
					obj.GetName(), check.fset.Position(dotImport)))
				return
			}

			// get by w/o other position
			check.errorf(dotImport, fmt.Sprintf("dot-import redeclares %s", obj.GetName()))
			return
		}

		if pos := alt.GetPos(); pos.IsValid() {
			prevDecl = fmt.Sprintf("\n\tother declaration at %s", check.fset.Position(pos))
		}
		check.errorf(obj.GetPos(), fmt.Sprintf("%s redeclared in this block%s", obj.GetName(), prevDecl))
	}
}

// Inside resolve function, handling dot imports:
// gcimported package scopes contain non-exported
// objects such as types used in partially exported
// objects - do not accept them
if ast.IsExported(obj.GetName()) {
	check.declareObj(fileScope, pkg.Scope, obj, spec.Pos())
}

コアとなるコードの解説

src/pkg/go/types/expr.go の変更点

この変更は、パッケージセレクタ式(例: math.Pi)の解決時に、参照しようとしている識別子 (exp) が実際にエクスポートされているかを確認するガードを追加しています。 gc コンパイラが生成するエクスポートデータには、エクスポートされた型が内部的に使用する非エクスポートオブジェクトが誤って含まれることがありました。このコードは、exp == nil (オブジェクトが見つからない場合) または !ast.IsExported(exp.GetName()) (オブジェクトが非エクスポートである場合) のいずれかの条件が真であれば、エラー (cannot refer to unexported %s) を報告します。これにより、本来アクセスできないはずの非エクスポートオブジェクトへの不正な参照を型チェック段階で防ぎます。

src/pkg/go/types/objects.go の変更点

このファイルでは、Package, TypeName, Func といった型チェッカーが扱うオブジェクトの GetPos() メソッドが修正されています。これらのメソッドは、オブジェクトがソースコード上のどこで宣言されたかを示す token.Pos を返します。 以前の実装では、インポートされたオブジェクトのように、対応する spec (仕様) や decl (宣言) フィールドが nil である場合に、nil ポインタ参照によるパニックが発生する可能性がありました。 修正後は、これらのフィールドが nil でないことを確認してから Pos() メソッドを呼び出すようになりました。もし nil であれば、有効な位置情報がないことを示す token.NoPos を返します。これにより、型チェッカーが位置情報を安全に取得できるようになり、堅牢性が向上します。

src/pkg/go/types/resolve.go の変更点

このファイルには二つの主要な変更があります。

  1. declareObj 関数の変更: declareObj 関数は、スコープ内に新しいオブジェクトを宣言する際に、既存の宣言との衝突をチェックします。 新しい dotImport token.Pos 引数は、この宣言がドットインポートによって行われた場合に、そのドットインポート文の位置情報を提供します。 衝突が発生した場合、dotImport.IsValid() が真であれば、エラーメッセージが「%s redeclared in this block by dot-import at %s」という形式に変わります。これにより、衝突がドットインポートに起因することが明確になり、開発者は問題の原因を迅速に特定できます。 また、以前の宣言の位置を示すメッセージも「previous declaration at %s」から「other declaration at %s」に変更され、より一般的な表現になりました。

  2. ドットインポート処理の変更: resolve 関数内でドットインポート (import . "pkg") を処理するループにおいて、インポートされたパッケージのスコープ (imp.Scope.Entries) からオブジェクトを現在のファイルスコープ (fileScope) にマージする際に、if ast.IsExported(obj.GetName()) という条件が追加されました。 これは、gc エクスポートデータに誤って含まれる可能性のある非エクスポートオブジェクトが、ドットインポートによって現在のスコープに持ち込まれるのを防ぐためのガードです。これにより、ドットインポートはGo言語の仕様通り、エクスポートされた識別子のみを現在のスコープに導入するようになります。

これらの変更は、Goの型チェッカーがより正確に言語仕様を強制し、開発者に対してより有用なフィードバックを提供するための重要な改善です。

関連リンク

参考にした情報源リンク