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

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

このコミットは、Goコンパイラ(gc)において、現在のパッケージ外のシンボル名を宣言しようとした場合にエラーを発生させるように変更を加えるものです。具体的には、src/cmd/gc/dcl.c内のdeclare関数に新しいチェックを追加し、test/fixedbugs/bug388.goという新しいテストファイルを追加して、この修正が正しく機能することを確認しています。これにより、Go言語のパッケージシステムにおける名前解決の整合性が向上し、開発者が意図しない、または不正な名前の宣言を行うことを防ぎます。

コミット

gc: disallow declaration of variables outside package.

Fixes #2231.

Declaring main.i in package main in the same way already triggers syntax errors.

R=rsc
CC=golang-dev
https://golang.org/cl/5483078

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

https://github.com/golang/go/commit/4bcc9c6b5e91eca8de1329ea2151610e2f3e3ada

元コミット内容

commit 4bcc9c6b5e91eca8de1329ea2151610e2f3e3ada
Author: Luuk van Dijk <lvd@golang.org>
Date:   Tue Jan 10 11:18:56 2012 +0100

    gc: disallow declaration of variables outside package.
    
    Fixes #2231.
    
    Declaring main.i in package main in the same way already triggers syntax errors.
    
    R=rsc
    CC=golang-dev
    https://golang.org/cl/5483078

変更の背景

この変更は、Go言語のコンパイラが、現在のパッケージに属さない名前(例えば、runtime.UintTypemain.iのような、他のパッケージの識別子を修飾子として含む名前)を宣言しようとした際に、一貫性のない振る舞いをしていた問題を解決するために行われました。

コミットメッセージにある「Fixes #2231」は、GoのIssue 2231を修正したことを示しています。このIssueは、Goコンパイラにおける、宣言されていない名前や関数パラメータ、変数宣言における不正な構文に対するエラーチェックの不備に関連するバグでした。

既存のGoコンパイラでは、main.iのような形式でmainパッケージ内の変数を宣言しようとすると、既に構文エラーが発生していました。しかし、runtime.UintTypeのように、別のパッケージの型をパラメータとして宣言しようとしたり、別のパッケージの識別子を修飾子として変数宣言に含めようとしたりする場合には、適切なエラーが報告されないケースがありました。

このコミットの目的は、このような「パッケージ外のシンボル名の宣言」を明確に禁止し、コンパイラがより厳密かつ一貫したエラーチェックを行うようにすることです。これにより、開発者がGoのパッケージシステムと名前解決のルールに違反するコードを記述することを防ぎ、より堅牢なプログラムの作成を促進します。

前提知識の解説

Go言語のパッケージシステム

Go言語は、コードを整理し、再利用性を高めるためにパッケージシステムを採用しています。各Goファイルは必ずpackage宣言を持ち、そのパッケージに属する関数、変数、型などを定義します。

  • エクスポートされた名前: パッケージ外からアクセス可能な名前(関数名、変数名、型名など)は、先頭が大文字で始まります。例えば、fmt.PrintlnPrintlnfmtパッケージからエクスポートされています。
  • エクスポートされていない名前: パッケージ内でのみアクセス可能な名前は、先頭が小文字で始まります。
  • 修飾子付き識別子: 別のパッケージからエクスポートされた名前を参照する場合、パッケージ名.名前の形式で記述します(例: runtime.UintType)。これは、その名前がどのパッケージに属しているかを明示するものです。

Goコンパイラ (gc)

gcは、Go言語の公式コンパイラです。Goのソースコードを機械語に変換する役割を担っています。コンパイルの過程で、構文解析、型チェック、最適化など、様々な段階を経てコードの正当性を検証します。

変数宣言と名前解決

Goでは、varキーワードや:=演算子を使って変数を宣言します。変数を宣言する際には、その変数がどのスコープ(関数内、パッケージレベルなど)に属し、どのような名前を持つかが決定されます。名前解決とは、コード中で使用されている識別子が、どの宣言に対応するかを決定するプロセスです。

このコミットが対象としているのは、runtime.UintTypemain.iのように、パッケージ名を修飾子として含む識別子を「宣言」しようとするケースです。Goの設計では、このような修飾子付き識別子は、既存のパッケージからエクスポートされた名前を「参照」するために使用されるものであり、新しい変数を「宣言」するために使用されるものではありません。新しい変数は、現在のパッケージ内で一意の名前を持つ必要があります。

typecheckokimportpkg (Goコンパイラ内部)

Goコンパイラの内部では、typecheckokというフラグが、型チェックの主要なフェーズが完了したかどうかを示します。また、importpkgは、現在処理中のコードがパッケージのインポートに関連しているかどうかを示す変数です。これらの内部状態は、コンパイラがコードのどの段階を処理しているかを判断し、適切なルールを適用するために使用されます。

技術的詳細

このコミットの技術的な核心は、Goコンパイラのdeclare関数における新しいチェックの導入です。declare関数は、Goプログラム内で新しい名前(変数、関数、型など)が宣言される際に呼び出されるコンパイラの重要な部分です。

Go言語の設計思想では、変数の宣言は常に現在のパッケージのスコープ内で行われるべきであり、他のパッケージの識別子を修飾子として使用して新しい変数を宣言することは許可されていません。例えば、runtime.UintType := 1のような記述は、runtimeパッケージのUintTypeという既存の型を参照するものであり、runtimeパッケージ内に新しい変数UintTypeを宣言するものではありません。このような記述は、Goのパッケージシステムにおける名前解決の原則に反します。

このコミット以前は、コンパイラがこのような不正な宣言に対して常に適切なエラーを報告していませんでした。特に、構文解析の段階では見過ごされ、後の型チェックフェーズで問題が顕在化する可能性がありました。

追加されたコードは、declare関数が呼び出された際に、宣言しようとしているシンボル(s)が現在のローカルパッケージ(localpkg)に属しているかどうかをチェックします。

  • importpkg == nil: 現在、パッケージのインポート処理中ではないことを確認します。インポート処理中は、他のパッケージのシンボルが一時的に扱われるため、このチェックをスキップする必要があります。
  • !typecheckok: 型チェックの主要なフェーズがまだ完了していないことを確認します。型チェックが完了した後(typecheckoktrueの場合)に、genwrapperのような内部的な処理がパッケージ外の名前を宣言する可能性があるため、このチェックをスキップする必要があります。これは、コンパイラ内部の特殊なケースを考慮した「kludgy」(間に合わせの、ごまかしの)な対応とコメントされています。
  • s->pkg != localpkg: 宣言しようとしているシンボルsが、現在のローカルパッケージlocalpkgに属していないことを確認します。

上記の3つの条件がすべて真である場合、つまり、通常の宣言処理中に、現在のパッケージ外のシンボル名を宣言しようとしていると判断された場合に、yyerror("cannot declare name %S", s);というエラーメッセージを出力してコンパイルを停止します。

この変更により、Goコンパイラは、パッケージ外のシンボル名を使った不正な変数宣言を早期に検出し、開発者に明確なエラーメッセージを提示できるようになりました。これは、Goコードの健全性を保ち、予期せぬ動作を防ぐ上で重要な改善です。

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

diff --git a/src/cmd/gc/dcl.c b/src/cmd/gc/dcl.c
index f9f638ceb1..87dab3eeca 100644
--- a/src/cmd/gc/dcl.c
+++ b/src/cmd/gc/dcl.c
@@ -175,6 +175,11 @@ declare(Node *n, int ctxt)\n \n 	n->lineno = parserline();
 	s = n->sym;
+\n+	// kludgy: typecheckok means we're past parsing.  Eg genwrapper may declare out of package names later.
+	if(importpkg == nil && !typecheckok && s->pkg != localpkg)
+		yyerror("cannot declare name %S", s);
+\n 	gen = 0;
 	if(ctxt == PEXTERN) {
 		externdcl = list(externdcl, n);
diff --git a/test/fixedbugs/bug388.go b/test/fixedbugs/bug388.go
new file mode 100644
index 0000000000..d480e852f0
--- /dev/null
+++ b/test/fixedbugs/bug388.go
@@ -0,0 +1,39 @@
+// errchk $G $D/$F.go
+
+// 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.
+
+// Issue 2231
+
+package main
+import "runtime"
+
+func foo(runtime.UintType, i int) {  // ERROR "cannot declare name runtime.UintType"
+	println(i, runtime.UintType) 
+}
+
+func bar(i int) {
+	runtime.UintType := i       // ERROR "cannot declare name runtime.UintType"
+	println(runtime.UintType)
+}
+
+func baz() {
+	main.i := 1	// ERROR "non-name main.i"
+	println(main.i)
+}
+
+func qux() {
+	var main.i	// ERROR "unexpected [.]"
+	println(main.i)
+}
+
+func corge() {
+	var foo.i int  // ERROR "unexpected [.]"
+	println(foo.i)
+}
+
+func main() {
+	foo(42,43)
+	bar(1969)
+}

コアとなるコードの解説

src/cmd/gc/dcl.c の変更

dcl.cはGoコンパイラの宣言処理を担当するファイルです。declare関数は、新しいシンボルが宣言されるたびに呼び出されます。

追加された5行のコードは以下の通りです。

	// kludgy: typecheckok means we're past parsing.  Eg genwrapper may declare out of package names later.
	if(importpkg == nil && !typecheckok && s->pkg != localpkg)
		yyerror("cannot declare name %S", s);
  • // kludgy: ...: このコメントは、このチェックがコンパイラの特定の内部状態(typecheckok)に依存していること、そしてgenwrapperのような後続の処理がパッケージ外の名前を宣言する可能性があるため、そのケースを考慮していることを示しています。これは、コンパイラの複雑な内部ロジックに対応するための「間に合わせ」または「特殊な」処理であることを示唆しています。
  • if(importpkg == nil && !typecheckok && s->pkg != localpkg):
    • importpkg == nil: 現在、パッケージのインポート処理中ではないことを確認します。インポート中は、他のパッケージのシンボルが一時的に扱われるため、このチェックは適用されません。
    • !typecheckok: コンパイラがまだ構文解析フェーズを終え、主要な型チェックフェーズに入っていないことを確認します。型チェックが完了した後(typecheckoktrueの場合)は、コンパイラ内部のコード生成ロジック(例: genwrapper)が一時的にパッケージ外の名前を扱う可能性があるため、このチェックは適用されません。
    • s->pkg != localpkg: 宣言しようとしているシンボルsが、現在のローカルパッケージlocalpkgに属していないことを確認します。
  • yyerror("cannot declare name %S", s);: 上記の条件がすべて真である場合、つまり、通常の宣言処理中に、現在のパッケージ外のシンボル名を宣言しようとしていると判断された場合に、"cannot declare name %S"というエラーメッセージを出力します。%Sはシンボル名に置き換えられます。

この変更により、Goコンパイラは、runtime.UintTypeのような修飾子付き識別子を新しい変数として宣言しようとする不正な試みを、コンパイルの早い段階で捕捉し、エラーとして報告できるようになりました。

test/fixedbugs/bug388.go の追加

この新しいテストファイルは、このコミットで修正された問題の様々なケースを網羅し、コンパイラが期待通りにエラーを報告することを確認します。

  • // errchk $G $D/$F.go: この行は、Goのテストフレームワークに対する指示で、このファイルがコンパイルエラーを生成することを期待していることを示します。
  • func foo(runtime.UintType, i int): runtime.UintTypeを関数パラメータとして宣言しようとするケース。これは不正な宣言であり、ERROR "cannot declare name runtime.UintType"が期待されます。
  • runtime.UintType := i: runtime.UintTypeを短い変数宣言(:=)で宣言しようとするケース。これも不正な宣言であり、ERROR "cannot declare name runtime.UintType"が期待されます。
  • main.i := 1: mainパッケージ内のiを修飾子付きで宣言しようとするケース。これは既に既存のコンパイラでERROR "non-name main.i"として捕捉されていましたが、このテストでその振る舞いを再確認しています。
  • var main.i: mainパッケージ内のivar宣言で修飾子付きで宣言しようとするケース。ERROR "unexpected [.]"が期待されます。
  • var foo.i int: fooという未定義のパッケージ内のiを修飾子付きで宣言しようとするケース。ERROR "unexpected [.]"が期待されます。

これらのテストケースは、Goコンパイラがパッケージ外のシンボル名を使った宣言に対して、様々な状況で適切にエラーを報告するようになったことを検証しています。

関連リンク

  • Go CL (Code Review): https://golang.org/cl/5483078
  • Go Issue 2231: このコミットが修正したGoのIssue。Goの公式Issueトラッカーで詳細を確認できます。

参考にした情報源リンク

  • Go言語の公式ドキュメント(パッケージ、変数宣言、コンパイラに関する情報)
  • Goコンパイラのソースコード(src/cmd/gc/dcl.c
  • Go Issue Tracker (Issue 2231)
  • Go言語のパッケージと名前解決に関する一般的な情報源