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

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

このコミットは、Goコンパイラ(cmd/gc)において、シンボルの再宣言エラーメッセージを改善し、特にインポートによって引き起こされた再宣言の場合に、どのパッケージが原因であるかを明確に表示するように変更を加えるものです。これにより、開発者が再宣言エラーの原因を特定しやすくなり、デバッグの効率が向上します。

コミット

commit d06dcd45959f83c3b82c10116acc9a64beff14cd
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date:   Thu Sep 13 18:40:50 2012 +0200

    cmd/gc: Specify which package import caused an redeclaration error.
    
    Fixes #4012.
    
    R=dave, remyoudompheng, rsc
    CC=golang-dev
    https://golang.org/cl/6490082

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

https://github.com/golang/go/commit/d06dcd45959f83c3b82c10116acc9a64beff14cd

元コミット内容

cmd/gc: Specify which package import caused an redeclaration error.

Fixes #4012.

R=dave, remyoudompheng, rsc
CC=golang-dev
https://golang.org/cl/6490082

変更の背景

Go言語では、同じスコープ内で同じ名前の識別子(変数、関数、型など)を複数回宣言することはできません。これを「再宣言(redeclaration)」と呼び、コンパイル時にエラーとなります。特に、複数のパッケージをインポートする際に、異なるパッケージが同じ名前の識別子をエクスポートしている場合、名前の衝突が発生し、再宣言エラーとなることがあります。

このコミットが作成された時点では、Goコンパイラがインポートによって引き起こされた再宣言エラーを報告する際、エラーメッセージがどのパッケージが衝突の原因であるかを具体的に示していませんでした。例えば、「X redeclared during import」のような一般的なメッセージが表示されるだけで、開発者はどのインポートが問題を引き起こしているのかを手動で特定する必要がありました。これは、特に大規模なプロジェクトや多数の依存関係を持つ場合に、デバッグ作業を非常に困難にしていました。

この問題は、Goのイシュートラッカーで「Issue #4012」として報告されていたと考えられます(ただし、現在の公開されているGoのイシュートラッカーで直接この番号のイシューが見つからない場合、内部的なイシュー番号であるか、または既に解決済みでクローズされている可能性があります)。このコミットは、このユーザーエクスペリエンスの課題を解決し、より具体的で役立つエラーメッセージを提供することを目的としています。

前提知識の解説

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

1. Goコンパイラ (cmd/gc) の役割

cmd/gcは、Go言語の公式コンパイラのフロントエンド部分であり、Goのソースコードを解析し、中間表現を生成する主要な役割を担っています。これには、字句解析、構文解析、型チェック、シンボル解決などが含まれます。再宣言エラーの検出と報告も、この段階で行われます。

2. シンボルテーブルと Sym 構造体

コンパイラは、プログラム内のすべての識別子(変数名、関数名、型名、パッケージ名など)を管理するために「シンボルテーブル」を使用します。シンボルテーブルは、各識別子に関する情報(名前、型、スコープ、定義場所など)を格納します。

Goコンパイラの内部では、これらの識別子情報は Sym (Symbol) 構造体によって表現されます。Sym構造体は、シンボルの名前、それが属するパッケージ、そしてそのシンボルが定義されているASTノードへの参照など、様々なメタデータを保持します。

  • Sym->Pkg: シンボルが属するパッケージへのポインタ。
  • Sym->Name: シンボルの名前(例: fmt.PrintlnPrintln)。
  • Sym->Def: シンボルが定義されている抽象構文木(AST)ノードへの参照。
  • Sym->lastlineno: シンボルが最後に宣言されたソースコード上の行番号。これが0の場合、通常はインポート処理中に宣言されたことを示します。

3. パッケージとインポート

Go言語はパッケージによってコードをモジュール化します。import文を使用して、他のパッケージで定義された識別子を現在のパッケージで使用できるようになります。

  • 通常インポート: import "fmt" のように、パッケージ名を指定してインポートします。この場合、fmt.Printlnのようにパッケージ名をプレフィックスとして使用します。
  • ドットインポート (import . "pkg"): import . "fmt" のように、パッケージ名の代わりにドットを使用します。これにより、インポートされたパッケージの識別子を、パッケージ名をプレフィックスなしで直接使用できるようになります(例: Println)。ドットインポートは名前の衝突を引き起こしやすいため、注意が必要です。

4. 再宣言エラー (redeclaration error)

Goでは、同じスコープ内で同じ名前の識別子を複数回宣言することはできません。例えば、以下のようなコードは再宣言エラーになります。

package main

func main() {
    var x int = 10
    var x int = 20 // エラー: x redeclared in this block
}

インポートにおいても同様で、異なるパッケージから同じ名前の識別子がインポートされた場合、名前の衝突が発生し、再宣言エラーとなります。

5. redeclare 関数

redeclare関数は、Goコンパイラの内部で再宣言エラーを検出した際に、エラーメッセージを生成し報告するために使用されるユーティリティ関数です。この関数は、再宣言されたシンボルと、それがどこで再宣言されたかを示す文字列(例: "during import")を受け取ります。

6. importsym および importdot 関数

  • importsym: コンパイラが外部パッケージからシンボルをインポートする際に使用される内部関数です。インポートされたシンボルが既に現在のスコープで宣言されていないかを確認し、もし再宣言であればredeclareを呼び出します。
  • importdot: ドットインポート(import . "pkg")を処理するための内部関数です。インポートされたパッケージのすべてのエクスポートされたシンボルを現在のパッケージのスコープに導入し、名前の衝突がないかを確認します。

技術的詳細

このコミットは、Goコンパイラのcmd/gcディレクトリ内の4つのファイル(dcl.c, export.c, go.h, subr.c)に変更を加えています。主な変更点は、Sym構造体に新しいフィールドを追加し、再宣言エラーを報告するredeclare関数が、インポート元のパッケージパスをエラーメッセージに含めるように修正されたことです。

1. Sym構造体への origpkg フィールドの追加 (src/cmd/gc/go.h)

src/cmd/gc/go.hファイルでは、Sym構造体に新しいフィールドorigpkgが追加されました。

// src/cmd/gc/go.h
struct	Sym
{
	// ... 既存のフィールド ...
	int32	lastlineno;	// last declaration for diagnostic
	Pkg*	origpkg;	// original package for . import
};
  • Pkg* origpkg;: このフィールドは、ドットインポート(import . "pkg")によって現在のパッケージに導入されたシンボルが、元々どのパッケージに属していたかを追跡するために使用されます。これにより、再宣言エラーが発生した際に、そのシンボルがどの元のパッケージから来たのかを特定できるようになります。

2. redeclare 関数の変更 (src/cmd/gc/dcl.c)

src/cmd/gc/dcl.credeclare関数が修正され、インポートによる再宣言の場合に、元のパッケージパスをエラーメッセージに含めるようになりました。

// src/cmd/gc/dcl.c
void
redeclare(Sym *s, char *where)
{
	Strlit *pkgstr;

	if(s->lastlineno == 0) { // インポート時に宣言された場合
		pkgstr = s->origpkg ? s->origpkg->path : s->pkg->path;
		yyerror("%S redeclared %s\n"
			"\tprevious declaration during import \"%Z\"",
			s, where, pkgstr);
	} else
		yyerror("%S redeclared %s\n"
			"\tprevious declaration at %L",
			s, where, s->lastlineno);
}
  • s->lastlineno == 0という条件は、シンボルがインポート処理中に宣言されたことを示します。
  • この条件が真の場合、pkgstr変数に、シンボルの元のパッケージ(s->origpkgが設定されていればそれを使用、そうでなければ通常のs->pkg)のパスが格納されます。
  • エラーメッセージのフォーマット文字列が変更され、\"%Z\"というプレースホルダーが追加されました。これはsmprint関数(後述)によってパッケージパスに置き換えられます。これにより、エラーメッセージに「previous declaration during import "path/to/package"」のような具体的な情報が含まれるようになります。

3. importsym 関数の変更 (src/cmd/gc/export.c)

src/cmd/gc/export.cimportsym関数は、外部パッケージからシンボルをインポートする際に呼び出されます。この関数が、再宣言エラーを報告する際に、インポート元のパッケージパスをredeclare関数に渡すように変更されました。

// src/cmd/gc/export.c
Sym*
importsym(Sym *s, int op)
{
	char *pkgstr;

	if(s->def != N && s->def->op != op) {
		pkgstr = smprint("during import \"%Z\"", importpkg->path);
		redeclare(s, pkgstr);
	}

	// ... 既存の処理 ...
}
  • 再宣言が検出された場合、smprint関数を使用して「during import "path/to/package"」という形式の文字列を生成し、これをredeclare関数に渡します。importpkgは現在インポート中のパッケージを指します。

4. importdot 関数の変更 (src/cmd/gc/subr.c)

src/cmd/gc/subr.cimportdot関数は、ドットインポートを処理します。この関数が、ドットインポートされたシンボルに対してSym->origpkgフィールドを設定し、再宣言エラー時にこの情報を使用するように変更されました。

// src/cmd/gc/subr.c
void
importdot(Pkg *opkg, Node *pack)
{
	// ... 既存の処理 ...
	char *pkgerror;

	// ... ループ内 ...
	if(s1->def != N) {
		pkgerror = smprint("during import \"%Z\"", opkg->path);
		redeclare(s1, pkgerror);
		continue;
	}
	s1->def = s->def;
	s1->block = s->block;
	s1->def->pack = pack;
	s1->origpkg = opkg; // ここでorigpkgを設定
	n++;
	// ... ループの終わり ...
}
  • ドットインポートによってシンボルss1として現在のスコープに導入される際、s1->origpkg = opkg;という行が追加され、そのシンボルが元々opkg(インポート元のパッケージ)に属していたことを記録します。
  • 再宣言エラーが発生した場合、importsymと同様にsmprintを使ってパッケージパスを含むエラー文字列を生成し、redeclareに渡します。

smprint 関数について

smprintは、Goコンパイラ内部で使用される文字列フォーマット関数です。%Zというフォーマット指定子は、Pkg構造体のパス(Pkg->path)を文字列として展開するために使用されます。これにより、動的にパッケージパスをエラーメッセージに埋め込むことが可能になります。

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

src/cmd/gc/dcl.c

--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -150,11 +150,14 @@ testdclstack(void)
 void
 redeclare(Sym *s, char *where)
 {
-	if(s->lastlineno == 0)
-		yyerror("%S redeclared %s\n"
-			"\tprevious declaration during import",
-			s, where);
-	else
+	Strlit *pkgstr;
+
+	if(s->lastlineno == 0) {
+		pkgstr = s->origpkg ? s->origpkg->path : s->pkg->path;
+		yyerror("%S redeclared %s\n"
+			"\tprevious declaration during import \"%Z\"",
+			s, where, pkgstr);
+	} else
 		yyerror("%S redeclared %s\n"
 			"\tprevious declaration at %L",
 			s, where, s->lastlineno);

src/cmd/gc/export.c

--- a/src/cmd/gc/export.c
+++ b/src/cmd/gc/export.c
@@ -349,8 +349,12 @@ dumpexport(void)
 Sym*
 importsym(Sym *s, int op)
 {
-	if(s->def != N && s->def->op != op)
-		redeclare(s, "during import");
+	char *pkgstr;
+
+	if(s->def != N && s->def->op != op) {
+		pkgstr = smprint("during import \"%Z\"", importpkg->path);
+		redeclare(s, pkgstr);
+	}
 
 	// mark the symbol so it is not reexported
 	if(s->def == N) {

src/cmd/gc/go.h

--- a/src/cmd/gc/go.h
+++ b/src/cmd/gc/go.h
@@ -365,6 +365,7 @@ struct	Sym
 	Label*	label;	// corresponding label (ephemeral)
 	int32	block;		// blocknumber to catch redeclaration
 	int32	lastlineno;	// last declaration for diagnostic
+	Pkg*	origpkg;	// original package for . import
 };
 #define	S	((Sym*)0)
 

src/cmd/gc/subr.c

--- a/src/cmd/gc/subr.c
+++ b/src/cmd/gc/subr.c
@@ -382,6 +382,7 @@ importdot(Pkg *opkg, Node *pack)
 	Sym *s, *s1;
 	uint32 h;
 	int n;
+	char *pkgerror;
 
 	n = 0;
 	for(h=0; h<NHASH; h++) {
@@ -394,12 +395,14 @@ importdot(Pkg *opkg, Node *pack)
 			continue;
 		s1 = lookup(s->name);
 		if(s1->def != N) {
-			redeclare(s1, "during import");
+			pkgerror = smprint("during import \"%Z\"", opkg->path);
+			redeclare(s1, pkgerror);
 			continue;
 		}
 		s1->def = s->def;
 		s1->block = s->block;
 		s1->def->pack = pack;
+		s1->origpkg = opkg;
 		n++;
 	}
 	// ... 既存の処理 ...

コアとなるコードの解説

このコミットの核心は、Goコンパイラが再宣言エラーを報告する際の診断メッセージの質を向上させることにあります。

  1. Sym構造体の拡張 (go.h):

    • Sym構造体にorigpkgという新しいフィールドが追加されました。これは、特にドットインポート(import . "pkg")によって現在のスコープに導入されたシンボルが、元々どのパッケージに由来するのかを記録するためのものです。これにより、コンパイラはシンボルの「真の」起源を追跡できるようになります。
  2. redeclare関数の診断強化 (dcl.c):

    • redeclare関数は、再宣言エラーが発生した際に呼び出される中心的な関数です。
    • 変更前は、インポートによる再宣言の場合(s->lastlineno == 0)、単に「previous declaration during import」と表示されていました。
    • 変更後、s->origpkg(ドットインポートの場合)またはs->pkg(通常のインポートの場合)からパッケージパスを取得し、yyerror関数に渡されるメッセージフォーマットに\"%Z\"というプレースホルダーを追加しました。この%Zは、smprint関数によって実際のパッケージパスに置き換えられます。
    • 結果として、エラーメッセージは「previous declaration during import "path/to/conflicting/package"」のように、より具体的で役立つ情報を提供するようになりました。
  3. インポート処理におけるパッケージパスの伝達 (export.c, subr.c):

    • importsym関数(一般的なシンボルインポート)とimportdot関数(ドットインポート)の両方で、再宣言エラーが発生する可能性がある箇所が修正されました。
    • これらの関数は、再宣言エラーをredeclare関数に報告する際に、smprint関数を使用して、現在インポート中のパッケージのパスを含む文字列を生成し、redeclare関数に渡すようになりました。
    • 特にimportdotでは、ドットインポートされたシンボルs1に対して、その元のパッケージopkgs1->origpkgに明示的に設定する行が追加されました。これにより、redeclare関数がorigpkgフィールドを利用して正確なパッケージパスを特定できるようになります。

これらの変更により、Goコンパイラは、インポートによる名前の衝突が原因で発生する再宣言エラーに対して、より詳細でデバッグに役立つ情報を提供できるようになりました。これは、開発者の生産性向上に大きく貢献する改善です。

関連リンク

参考にした情報源リンク