[インデックス 13331] ファイルの概要
このコミットは、Go言語の実験的な型チェッカーである exp/types
パッケージにおける、修飾子付き識別子(qualified identifiers)の解決に関するテストを追加し、同時に exp/types/gcimporter.go
内のバグを修正するものです。具体的には、パッケージのインポート処理における重複インポートの取り扱いに関するロジックが改善され、修飾子付き識別子の解決が正しく行われることを検証するための新しいテストファイル resolver_test.go
が追加されました。
コミット
- コミットハッシュ:
49d6e490876a9bbfa5dfa27a4377b822edbf656c
- 作者: Robert Griesemer gri@golang.org
- コミット日時: Mon Jun 11 11:06:27 2012 -0700
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/49d6e490876a9bbfa5dfa27a4377b822edbf656c
元コミット内容
exp/types: testing resolution of qualified identifiers
Also: fix a bug with exp/types/GcImport.
R=rsc, r
CC=golang-dev
https://golang.org/cl/6302060
変更の背景
このコミットは主に二つの目的を持っています。
- 修飾子付き識別子の解決のテスト: Go言語の型チェッカーは、
fmt.Println
のような修飾子付き識別子(パッケージ名とそれに続く識別子)を正しく解決する必要があります。これは、どのパッケージのどの要素を参照しているのかを正確に判断するために不可欠です。このコミット以前は、exp/types
パッケージにおいて、この重要な機能に対する包括的なテストが存在しなかった可能性があります。そのため、このコミットでは、この解決ロジックが期待通りに機能するかを検証するための新しいテストスイートが導入されました。 exp/types/GcImport
のバグ修正:exp/types
パッケージは、Goコンパイラ(gc
)によって生成されたパッケージ情報(バイナリ形式)をインポートする機能を持っています。このインポート処理において、既に部分的にインポートされたパッケージが再度インポートされようとした際に、不適切なエラー(panic
)が発生するバグが存在していました。これは、インポート処理のロバスト性を損なうものであり、特に複雑な依存関係を持つプロジェクトや、インポート処理が複数回行われる可能性のあるシナリオにおいて問題となる可能性がありました。このコミットは、このバグを修正し、インポート処理の堅牢性を向上させることを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語の内部構造と概念に関する知識が必要です。
go/ast
パッケージ (Abstract Syntax Tree): Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造と関数を提供します。コンパイラやツールは、このASTを解析してコードの意味を理解し、様々な処理を行います。ast.Object
: AST内の名前付きエンティティ(変数、関数、型、パッケージなど)を表します。ast.Pkg
: パッケージを表すast.Object
のKind
です。ast.Scope
: スコープ(識別子の有効範囲)を表します。ast.SelectorExpr
:pkg.Name
のようなセレクタ式を表します。X
はパッケージ識別子、Sel
はセレクタ(パッケージ内の名前)です。ast.Ident
: 識別子(変数名、関数名など)を表します。
go/token
パッケージ: ソースコード内の位置(ファイル、行、列)を管理するための型と関数を提供します。go/parser
パッケージ: Go言語のソースコードを解析し、ASTを生成するための関数を提供します。exp/types
パッケージ: Go言語の実験的な型チェッカーです。Goのコンパイラが内部的に使用する型チェックロジックを、より汎用的な形で提供することを目指しています。これは、IDE、リンター、コード分析ツールなど、Goコードのセマンティックな理解を必要とする様々なツールにとって重要です。- 修飾子付き識別子 (Qualified Identifiers):
パッケージ名.識別子
の形式で記述される識別子です。例えば、fmt.Println
におけるfmt
はパッケージ名、Println
はそのパッケージ内で定義された関数です。型チェッカーは、このfmt
がどのパッケージを指し、そのパッケージ内にPrintln
という関数が実際に存在するかを解決する必要があります。 - パッケージインポートメカニズム: Go言語では、
import "path/to/package"
のようにして他のパッケージをインポートします。コンパイラは、インポートされたパッケージの公開された識別子(関数、変数、型など)を、インポート元のパッケージから参照できるようにします。 gcimporter
:exp/types
パッケージの一部で、Goコンパイラ(gc
)が生成するバイナリ形式のパッケージ情報(.a
ファイルなど)を読み込み、exp/types
が利用できる内部表現に変換する役割を担います。これにより、exp/types
はGoの標準ライブラリや他のGoパッケージの型情報を利用できるようになります。
技術的詳細
src/pkg/exp/types/gcimporter.go
の変更
このファイルは、Goコンパイラが生成するパッケージ情報をインポートするロジックを扱っています。主な変更点は、パッケージの重複インポートに関する挙動の修正です。
変更前:
func GcImportData(imports map[string]*ast.Object, filename, id string, data *buf) (pkg *ast.Object, err error) {
// ...
if imports[id] != nil {
panic(fmt.Sprintf("package %s already imported", id))
}
// ...
}
func GcImport(imports map[string]*ast.Object, path string) (pkg *ast.Object, err error) {
// ...
if pkg = imports[id]; pkg != nil {
return // package was imported before
}
// ...
}
変更前は、GcImportData
関数内で、もし imports
マップに既に同じIDのパッケージが存在する場合、panic
を発生させていました。また、GcImport
関数では、既にインポートされているパッケージであれば、すぐにリターンしていました。
変更後:
func GcImportData(imports map[string]*ast.Object, filename, id string, data *buf) (pkg *ast.Object, err error) {
// ...
// if imports[id] != nil { // Removed this panic
// panic(fmt.Sprintf("package %s already imported", id))
// }
// ...
}
func GcImport(imports map[string]*ast.Object, path string) (pkg *ast.Object, err error) {
// ...
// Note: imports[id] may already contain a partially imported package.
// We must continue doing the full import here since we don't
// know if something is missing.
// TODO: There's no need to re-import a package if we know that we
// have done a full import before. At the moment we cannot
// tell from the available information in this function alone.
// ...
}
GcImportData
から panic
が削除されました。これは、部分的にインポートされたパッケージが存在する場合でも、処理を継続できるようにするためです。GcImport
関数では、既に imports[id]
にエントリが存在する場合でも、インポート処理を継続するようになりました。これは、コメントにもあるように、imports[id]
が部分的にインポートされたパッケージである可能性があり、完全なインポートを完了させる必要があるためです。この変更により、インポート処理のロバスト性が向上し、不完全な状態のパッケージ情報が原因で発生する可能性のある問題が回避されます。
また、parsePkgId
と parseExport
関数においても、pkg.Data
に ast.NewScope(nil)
を設定する際に、pkg
が nil
の場合にのみ新しい ast.Object
を作成し、既存の pkg
があればそれを利用するように変更されています。これにより、既に部分的に作成されたパッケージオブジェクトがある場合でも、それを再利用して情報を追加できるようになります。
src/pkg/exp/types/resolver_test.go
の追加
この新しいテストファイルは、exp/types
パッケージが修飾子付き識別子を正しく解決できることを検証するために追加されました。
ResolveQualifiedIdents
関数: この関数は、ast.Package
内のすべてのast.SelectorExpr
を走査し、そのセレクタ(s.Sel
)が指すast.Object
を解決します。具体的には、セレクタのX
部分がパッケージ識別子(ast.Pkg
)である場合、そのパッケージのスコープ内でセレクタ名(s.Sel.Name
)に対応するオブジェクトを検索し、s.Sel.Obj
に設定します。この関数は、最終的にはCheck
関数(型チェックの主要な関数)に統合される予定であることがコメントで示されています。TestResolveQualifiedIdents
テスト関数:- ソースコードのパース:
sources
変数に定義された複数のGoソースコードスニペットをgo/parser
を使用してパースし、ast.File
オブジェクトのマップを作成します。これらのスニペットには、fmt.Println
やmath.Pi
のような修飾子付き識別子が含まれています。 - パッケージASTの解決: パースされたファイルから
ast.NewPackage
を使用してast.Package
オブジェクトを作成します。この際、GcImport
関数がインポート処理のために使用されます。 - インポートされたパッケージの確認:
pkgnames
にリストされたパッケージ(fmt
,go/parser
,math
)がすべて正しくインポートされていることを確認します。 - 未解決のグローバル識別子の確認: パッケージ内のトップレベルで未解決の識別子がないことを確認します。
- 修飾子付き識別子の解決: 上記で定義された
ResolveQualifiedIdents
関数を呼び出し、修飾子付き識別子の解決を実行します。 - 解決結果の検証: 最後に、
ast.Inspect
を使用してパッケージASTを再度走査し、すべてのast.SelectorExpr
において、パッケージ識別子(s.X
)とセレクタ(s.Sel
)の両方が正しくast.Object
に解決されていることを確認します。これにより、fmt.Println
のfmt
とPrintln
がそれぞれ正しいオブジェクトにリンクされているかが検証されます。
- ソースコードのパース:
このテストの追加により、exp/types
パッケージがGo言語の基本的な識別子解決ルールに準拠していることが保証されます。
コアとなるコードの変更箇所
src/pkg/exp/types/gcimporter.go
--- a/src/pkg/exp/types/gcimporter.go
+++ b/src/pkg/exp/types/gcimporter.go
@@ -89,10 +89,6 @@ func GcImportData(imports map[string]*ast.Object, filename, id string, data *buf
\t\tfmt.Printf(\"importing %s (%s)\\n\", id, filename)\n \t}\n \n-\tif imports[id] != nil {\n-\t\tpanic(fmt.Sprintf(\"package %s already imported\", id))\n-\t}\n-\n \t// support for gcParser error handling\n \tdefer func() {\n \t\tif r := recover(); r != nil {\n@@ -128,9 +124,12 @@ func GcImport(imports map[string]*ast.Object, path string) (pkg *ast.Object, err
\t\treturn\n \t}\n \n-\tif pkg = imports[id]; pkg != nil {\n-\t\treturn // package was imported before\n-\t}\n+\t// Note: imports[id] may already contain a partially imported package.\n+\t// We must continue doing the full import here since we don\'t\n+\t// know if something is missing.\n+\t// TODO: There\'s no need to re-import a package if we know that we\n+\t// have done a full import before. At the moment we cannot\n+\t// tell from the available information in this function alone.\n \n \t// open file\n \tf, err := os.Open(filename)\n@@ -294,9 +293,8 @@ func (p *gcParser) parsePkgId() *ast.Object {\n \n \tpkg := p.imports[id]\n \tif pkg == nil {\n-\t\tscope = ast.NewScope(nil)\n \t\tpkg = ast.NewObj(ast.Pkg, \"\")\n-\t\tpkg.Data = scope\n+\t\tpkg.Data = ast.NewScope(nil)\n \t\tp.imports[id] = pkg\n \t}\n \n@@ -867,10 +865,12 @@ func (p *gcParser) parseExport() *ast.Object {\n \t}\n \tp.expect(\'\\n\')\n \n-\tassert(p.imports[p.id] == nil)\n-\tpkg := ast.NewObj(ast.Pkg, name)\n-\tpkg.Data = ast.NewScope(nil)\n-\tp.imports[p.id] = pkg\n+\tpkg := p.imports[p.id]\n+\tif pkg == nil {\n+\t\tpkg = ast.NewObj(ast.Pkg, name)\n+\t\tpkg.Data = ast.NewScope(nil)\n+\t\tp.imports[p.id] = pkg\n+\t}\n \n \tfor p.tok != \'$\' && p.tok != scanner.EOF {\n \t\tp.parseDecl()\n```
### `src/pkg/exp/types/resolver_test.go`
```diff
--- /dev/null
+++ b/src/pkg/exp/types/resolver_test.go
@@ -0,0 +1,130 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package types
+
+import (
+ "fmt"
+ "go/ast"
+ "go/parser"
+ "go/scanner"
+ "go/token"
+ "testing"
+)
+
+var sources = []string{
+ `package p
+ import "fmt"
+ import "math"
+ const pi = math.Pi
+ func sin(x float64) float64 {
+ return math.Sin(x)
+ }
+ var Println = fmt.Println
+ `,
+ `package p
+ import "fmt"
+ func f() string {
+ return fmt.Sprintf("%d", g())
+ }
+ `,
+ `package p
+ import . "go/parser"
+ func g() Mode { return ImportsOnly }`,
+}
+
+var pkgnames = []string{
+ "fmt",
+ "go/parser",
+ "math",
+}
+
+// ResolveQualifiedIdents resolves the selectors of qualified
+// identifiers by associating the correct ast.Object with them.
+// TODO(gri): Eventually, this functionality should be subsumed
+// by Check.
+//
+func ResolveQualifiedIdents(fset *token.FileSet, pkg *ast.Package) error {
+ var errors scanner.ErrorList
+
+ findObj := func(pkg *ast.Object, name *ast.Ident) *ast.Object {
+ scope := pkg.Data.(*ast.Scope)
+ obj := scope.Lookup(name.Name)
+ if obj == nil {
+ errors.Add(fset.Position(name.Pos()), fmt.Sprintf("no %s in package %s", name.Name, pkg.Name))
+ }
+ return obj
+ }
+
+ ast.Inspect(pkg, func(n ast.Node) bool {
+ if s, ok := n.(*ast.SelectorExpr); ok {
+ if x, ok := s.X.(*ast.Ident); ok && x.Obj != nil && x.Obj.Kind == ast.Pkg {
+ // find selector in respective package
+ s.Sel.Obj = findObj(x.Obj, s.Sel)
+ }
+ return false
+ }
+ return true
+ })
+
+ return errors.Err()
+}
+
+func TestResolveQualifiedIdents(t *testing.T) {
+ // parse package files
+ fset := token.NewFileSet()
+ files := make(map[string]*ast.File)
+ for i, src := range sources {
+ filename := fmt.Sprintf("file%d", i)
+ f, err := parser.ParseFile(fset, filename, src, parser.DeclarationErrors)
+ if err != nil {
+ t.Fatal(err)
+ }
+ files[filename] = f
+ }
+
+ // resolve package AST
+ pkg, err := ast.NewPackage(fset, files, GcImport, Universe)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // check that all packages were imported
+ for _, name := range pkgnames {
+ if pkg.Imports[name] == nil {
+ t.Errorf("package %s not imported", name)
+ }
+ }
+
+ // check that there are no top-level unresolved identifiers
+ for _, f := range pkg.Files {
+ for _, x := range f.Unresolved {
+ t.Errorf("%s: unresolved global identifier %s", fset.Position(x.Pos()), x.Name)
+ }
+ }
+
+ // resolve qualified identifiers
+ if err := ResolveQualifiedIdents(fset, pkg); err != nil {
+ t.Error(err)
+ }
+
+ // check that qualified identifiers are resolved
+ ast.Inspect(pkg, func(n ast.Node) bool {
+ if s, ok := n.(*ast.SelectorExpr); ok {
+ if x, ok := s.X.(*ast.Ident); ok {
+ if x.Obj == nil {
+ t.Errorf("%s: unresolved qualified identifier %s", fset.Position(x.Pos()), x.Name)
+ return false
+ }
+ if x.Obj.Kind == ast.Pkg && s.Sel != nil && s.Sel.Obj == nil {
+ t.Errorf("%s: unresolved selector %s", fset.Position(s.Sel.Pos()), s.Sel.Name)
+ return false
+ }
+ return false
+ }
+ return false
+ }
+ return true
+ })
+}
コアとなるコードの解説
src/pkg/exp/types/gcimporter.go
の変更点
GcImportData
関数からのpanic
の削除:- 変更前は、
imports
マップに既に同じid
のパッケージが存在する場合、panic
を発生させていました。これは、インポート処理が複数回呼び出されるシナリオや、部分的なインポートが既に存在する場合に問題を引き起こす可能性がありました。 - 変更後は、この
panic
が削除され、処理が継続されるようになりました。これにより、GcImportData
は、たとえ部分的にインポートされたパッケージであっても、その情報を更新または完了させることができます。
- 変更前は、
GcImport
関数におけるインポートロジックの変更:- 変更前は、
imports[id]
にパッケージが既に存在すれば、すぐにリターンしていました。 - 変更後は、
imports[id]
にパッケージが存在する場合でも、インポート処理を継続するようになりました。これは、コメントで説明されているように、imports[id]
が部分的にインポートされたパッケージである可能性があり、完全なインポートを完了させる必要があるためです。この変更は、インポート処理の堅牢性を高め、不完全なパッケージ情報が原因で発生する可能性のある問題を回避します。
- 変更前は、
parsePkgId
およびparseExport
関数におけるpkg.Data
の初期化:- これらの関数では、パッケージオブジェクト (
pkg
) のData
フィールドにast.Scope
を設定するロジックが変更されました。 - 変更前は、
pkg
がnil
でない場合でも、新しいast.Scope
を作成してpkg.Data
に設定していました。 - 変更後は、
pkg
がnil
の場合にのみ新しいast.Object
を作成し、そのData
にast.NewScope(nil)
を設定するように変更されました。これにより、既に部分的に作成されたパッケージオブジェクトがある場合でも、それを再利用して情報を追加できるようになり、不必要なオブジェクトの再作成が避けられます。
- これらの関数では、パッケージオブジェクト (
これらの変更は、exp/types
パッケージがGoのコンパイラによって生成されたパッケージ情報をより柔軟かつ堅牢に処理できるようにすることを目的としています。
src/pkg/exp/types/resolver_test.go
の追加点
ResolveQualifiedIdents
関数:- この関数は、GoのASTを走査し、
fmt.Println
のような修飾子付き識別子 (ast.SelectorExpr
) を解決する中心的なロジックを提供します。 findObj
ヘルパー関数は、指定されたパッケージスコープ内で識別子名に対応するast.Object
を検索します。見つからない場合はエラーを報告します。ast.Inspect
を使用してASTを再帰的に走査し、ast.SelectorExpr
を見つけると、そのセレクタのX
部分がパッケージ (ast.Pkg
) であることを確認し、findObj
を呼び出してセレクタ (s.Sel
) に対応するオブジェクトを解決し、s.Sel.Obj
に設定します。- この関数は、型チェッカーの主要な機能である
Check
関数に将来的に統合される予定であることが示唆されています。
- この関数は、GoのASTを走査し、
TestResolveQualifiedIdents
テスト関数:- このテストは、
ResolveQualifiedIdents
関数が正しく機能することを検証するための統合テストです。 - 複数のGoソースコードスニペットをパースし、それらを
ast.Package
に結合します。この際、GcImport
関数がインポート処理に使用されます。 - テストは、すべての期待されるパッケージが正しくインポートされていること、およびトップレベルで未解決の識別子がないことを確認します。
ResolveQualifiedIdents
を呼び出して、修飾子付き識別子の解決を実行します。- 最後に、解決後のASTを再度走査し、すべての修飾子付き識別子 (
ast.SelectorExpr
) のパッケージ部分 (s.X.Obj
) とセレクタ部分 (s.Sel.Obj
) が正しく解決されていることをアサートします。これにより、fmt.Println
のfmt
とPrintln
がそれぞれ正しいast.Object
にリンクされていることが検証されます。
- このテストは、
この新しいテストは、exp/types
パッケージがGo言語のセマンティクスに沿って修飾子付き識別子を正確に処理できることを保証するための重要な追加です。
関連リンク
- Go CL 6302060: https://golang.org/cl/6302060
参考にした情報源リンク
- Go言語の
go/ast
パッケージのドキュメント: https://pkg.go.dev/go/ast - Go言語の
go/token
パッケージのドキュメント: https://pkg.go.dev/go/token - Go言語の
go/parser
パッケージのドキュメント: https://pkg.go.dev/go/parser - Go言語の
exp/types
パッケージに関する情報 (Goの公式ドキュメントや関連するGoのIssue/CL): (具体的なURLはコミット時点では存在しない実験的なパッケージのため、一般的なGoの型システムやコンパイラに関する情報源を参照)- Go言語のコンパイラとツールに関する一般的な情報: https://go.dev/doc/
- Go言語のASTに関するブログ記事やチュートリアル (例: "Go AST: A Practical Guide"): (具体的なURLは検索結果による)
- Go言語のパッケージインポートに関する仕様: https://go.dev/ref/spec#Import_declarations