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

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

変更の背景

このコミットは主に二つの目的を持っています。

  1. 修飾子付き識別子の解決のテスト: Go言語の型チェッカーは、fmt.Println のような修飾子付き識別子(パッケージ名とそれに続く識別子)を正しく解決する必要があります。これは、どのパッケージのどの要素を参照しているのかを正確に判断するために不可欠です。このコミット以前は、exp/types パッケージにおいて、この重要な機能に対する包括的なテストが存在しなかった可能性があります。そのため、このコミットでは、この解決ロジックが期待通りに機能するかを検証するための新しいテストスイートが導入されました。
  2. exp/types/GcImport のバグ修正: exp/types パッケージは、Goコンパイラ(gc)によって生成されたパッケージ情報(バイナリ形式)をインポートする機能を持っています。このインポート処理において、既に部分的にインポートされたパッケージが再度インポートされようとした際に、不適切なエラー(panic)が発生するバグが存在していました。これは、インポート処理のロバスト性を損なうものであり、特に複雑な依存関係を持つプロジェクトや、インポート処理が複数回行われる可能性のあるシナリオにおいて問題となる可能性がありました。このコミットは、このバグを修正し、インポート処理の堅牢性を向上させることを目的としています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の内部構造と概念に関する知識が必要です。

  • go/ast パッケージ (Abstract Syntax Tree): Go言語のソースコードを抽象構文木(AST)として表現するためのデータ構造と関数を提供します。コンパイラやツールは、このASTを解析してコードの意味を理解し、様々な処理を行います。
    • ast.Object: AST内の名前付きエンティティ(変数、関数、型、パッケージなど)を表します。
    • ast.Pkg: パッケージを表す ast.ObjectKind です。
    • 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] が部分的にインポートされたパッケージである可能性があり、完全なインポートを完了させる必要があるためです。この変更により、インポート処理のロバスト性が向上し、不完全な状態のパッケージ情報が原因で発生する可能性のある問題が回避されます。

また、parsePkgIdparseExport 関数においても、pkg.Dataast.NewScope(nil) を設定する際に、pkgnil の場合にのみ新しい 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 テスト関数:
    1. ソースコードのパース: sources 変数に定義された複数のGoソースコードスニペットを go/parser を使用してパースし、ast.File オブジェクトのマップを作成します。これらのスニペットには、fmt.Printlnmath.Pi のような修飾子付き識別子が含まれています。
    2. パッケージASTの解決: パースされたファイルから ast.NewPackage を使用して ast.Package オブジェクトを作成します。この際、GcImport 関数がインポート処理のために使用されます。
    3. インポートされたパッケージの確認: pkgnames にリストされたパッケージ(fmt, go/parser, math)がすべて正しくインポートされていることを確認します。
    4. 未解決のグローバル識別子の確認: パッケージ内のトップレベルで未解決の識別子がないことを確認します。
    5. 修飾子付き識別子の解決: 上記で定義された ResolveQualifiedIdents 関数を呼び出し、修飾子付き識別子の解決を実行します。
    6. 解決結果の検証: 最後に、ast.Inspect を使用してパッケージASTを再度走査し、すべての ast.SelectorExpr において、パッケージ識別子(s.X)とセレクタ(s.Sel)の両方が正しく ast.Object に解決されていることを確認します。これにより、fmt.PrintlnfmtPrintln がそれぞれ正しいオブジェクトにリンクされているかが検証されます。

このテストの追加により、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 を設定するロジックが変更されました。
    • 変更前は、pkgnil でない場合でも、新しい ast.Scope を作成して pkg.Data に設定していました。
    • 変更後は、pkgnil の場合にのみ新しい ast.Object を作成し、その Dataast.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 関数に将来的に統合される予定であることが示唆されています。
  • TestResolveQualifiedIdents テスト関数:
    • このテストは、ResolveQualifiedIdents 関数が正しく機能することを検証するための統合テストです。
    • 複数のGoソースコードスニペットをパースし、それらを ast.Package に結合します。この際、GcImport 関数がインポート処理に使用されます。
    • テストは、すべての期待されるパッケージが正しくインポートされていること、およびトップレベルで未解決の識別子がないことを確認します。
    • ResolveQualifiedIdents を呼び出して、修飾子付き識別子の解決を実行します。
    • 最後に、解決後のASTを再度走査し、すべての修飾子付き識別子 (ast.SelectorExpr) のパッケージ部分 (s.X.Obj) とセレクタ部分 (s.Sel.Obj) が正しく解決されていることをアサートします。これにより、fmt.PrintlnfmtPrintln がそれぞれ正しい ast.Object にリンクされていることが検証されます。

この新しいテストは、exp/types パッケージがGo言語のセマンティクスに沿って修飾子付き識別子を正確に処理できることを保証するための重要な追加です。

関連リンク

参考にした情報源リンク

  • 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