[インデックス 14051] ファイルの概要
このコミットは、Goコンパイラのcmd/gc
におけるバグ修正であり、複合リテラル(struct literalなど)のキーとして修飾名(qualified name、例: os.File
)が誤って受け入れられてしまう問題を解決します。具体的には、異なるパッケージからインポートされたシンボルが、複合リテラルのフィールド名として不正に解決されるのを防ぐための型チェックロジックの改善が含まれています。
コミット
commit 87c35d8df1607c0a13840390bee5e1de3eb7838a
Author: Daniel Morsing <daniel.morsing@gmail.com>
Date: Sun Oct 7 16:47:53 2012 +0200
cmd/gc: Don't accept qualified names as literal keys
Fixes #4067.
R=golang-dev, minux.ma, dave, rsc
CC=golang-dev
https://golang.org/cl/6622056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/87c35d8df1607c0a13840390bee5e1de3eb7838a
元コミット内容
このコミットの元のメッセージは以下の通りです。
cmd/gc: Don't accept qualified names as literal keys
Fixes #4067.
R=golang-dev, minux.ma, dave, rsc
CC=golang-dev
https://golang.org/cl/6622056
これは、Goコンパイラ(cmd/gc
)が、複合リテラル(例: 構造体リテラル)のキーとして修飾名(例: os.File
)を受け入れないようにする変更であることを示しています。この変更は、Go issue #4067で報告されたバグを修正するものです。
変更の背景
Go言語では、構造体リテラルを初期化する際に、フィールド名を指定して値を割り当てることができます。例えば、MyStruct{Field1: value1, Field2: value2}
のように記述します。ここで、Field1
やField2
は、その構造体の定義内で宣言されたフィールド名である必要があります。
しかし、Go issue #4067("cmd/gc: qualified names in struct literals")で報告された問題は、コンパイラが誤ってos.File
のような修飾名(パッケージ名と識別子を.
で結合したもの)を構造体リテラルのフィールド名として受け入れてしまうというものでした。os.File
はos
パッケージのFile
型を指すものであり、通常は構造体のフィールド名として直接使用されることはありません。
このバグは、コンパイラのシンボル解決ロジック、特にimport .
(ドットインポート)を使用している場合に、異なるパッケージのシンボルが現在のパッケージのシンボルとして誤って解決され、その結果、複合リテラルのキーとして不正な修飾名が許可されてしまうことに起因していました。このような挙動はGoの言語仕様に反しており、予期せぬコンパイルエラーや混乱を招く可能性がありました。
このコミットは、この誤った挙動を修正し、Goの言語仕様に厳密に従うようにコンパイラの型チェックを強化することを目的としています。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびコンパイラに関する知識が必要です。
-
Go言語の複合リテラル (Composite Literals):
- Go言語で構造体、配列、スライス、マップなどの複合型を初期化するための構文です。
- 構造体リテラルの場合、
TypeName{FieldName: Value, ...}
の形式でフィールド名と値を指定して初期化できます。フィールド名は、その構造体の定義内で宣言されたものである必要があります。
-
Go言語のパッケージとシンボル (Packages and Symbols):
- Goのコードはパッケージに組織化されます。
- パッケージ内の識別子(変数、関数、型など)は「シンボル」と呼ばれます。
- 他のパッケージのシンボルを参照するには、通常
packageName.Identifier
のように修飾名を使用します(例:os.File
)。 import .
(ドットインポート)を使用すると、インポートしたパッケージの識別子を修飾なしで直接参照できるようになります(例:import . "os"
とすると、File
と書くだけでos.File
を参照できる)。これは便利ですが、名前の衝突を引き起こす可能性もあります。
-
Goコンパイラの
cmd/gc
:- Go言語の公式コンパイラの一部であり、Goソースコードをコンパイルして実行可能バイナリを生成する役割を担います。
gc
は、字句解析、構文解析、型チェック、中間コード生成、最適化、コード生成といった複数のフェーズで構成されます。
-
型チェック (Type Checking):
- コンパイラの重要なフェーズの一つで、プログラムが言語の型規則に準拠しているかを確認します。
- 例えば、変数の型と代入される値の型が一致するか、関数呼び出しの引数の型が正しいか、構造体リテラルのフィールド名が正しいかなどを検証します。
- このコミットでは、複合リテラルのキーが有効なフィールド名であるかをチェックするロジックが関係しています。
-
シンボルテーブルとシンボル解決 (Symbol Table and Symbol Resolution):
- コンパイラは、プログラム内で宣言されたすべての識別子(シンボル)に関する情報をシンボルテーブルに格納します。
- シンボル解決は、コード中の識別子がどの宣言に対応するかを決定するプロセスです。これには、スコープ規則やパッケージの可視性などが考慮されます。
- このコミットのバグは、シンボル解決の過程で、異なるパッケージのシンボルが誤って現在のコンテキストで有効なフィールド名として扱われてしまうことにありました。
技術的詳細
このコミットの技術的な核心は、Goコンパイラのsrc/cmd/gc/typecheck.c
ファイル内のtypecheckcomplit
関数におけるシンボル解決ロジックの修正です。
typecheckcomplit
関数は、複合リテラル(Node
型で表現される)の型チェックを担当します。この関数内で、リテラルのキー(構造体リテラルの場合はフィールド名)が処理されます。
問題の箇所は、シンボルs
が現在のパッケージ(localpkg
)のものではなく、かつエクスポートされた名前(exportname(s->name)
)である場合に発生していました。これは、import .
によって他のパッケージのシンボルが現在のスコープに持ち込まれた場合に起こりえます。
変更前のロジック:
// Sym might have resolved to name in other top-level
// package, because of import dot. Redirect to correct sym
// before we do the lookup.
if(s->pkg != localpkg && exportname(s->name))
s = lookup(s->name);
このコードは、s
が異なるパッケージのシンボルであり、かつエクスポートされている場合、その名前(s->name
)を使って再度lookup
関数を呼び出し、シンボルを再解決しようとしていました。このlookup(s->name)
は、現在のパッケージのスコープでその名前を持つシンボルを探します。もし現在のパッケージに同じ名前のフィールドが存在しない場合でも、os.File
のような修飾名が、os
パッケージからドットインポートされたFile
シンボルとして誤って解決され、それが構造体リテラルのフィールド名として受け入れられてしまう可能性がありました。
変更後のロジック:
if(s->pkg != localpkg && exportname(s->name)) {
s1 = lookup(s->name);
if(s1->origpkg == s->pkg)
s = s1;
}
変更後のコードでは、lookup(s->name)
の結果をs1
という新しいシンボルに格納します。そして、重要な追加チェックとして、s1->origpkg == s->pkg
という条件が導入されました。
s1 = lookup(s->name);
: これは以前と同様に、現在のパッケージのスコープでs->name
という名前のシンボルを探します。if(s1->origpkg == s->pkg)
: この条件が追加されたことで、s1
(現在のパッケージで解決されたシンボル)の元のパッケージ(origpkg
)が、s
(最初に問題となった異なるパッケージのシンボル)のパッケージと一致する場合にのみ、s
をs1
に置き換えるようになりました。
この追加チェックにより、os.File
のようなケースで、File
がos
パッケージから来たものであることを確認できます。もしT
という構造体にFile
というフィールドが定義されていれば、lookup("File")
はT
のFile
フィールドを指すシンボルを返しますが、そのorigpkg
はT
が定義されているパッケージになります。一方、os.File
のFile
シンボルはos
パッケージをorigpkg
として持ちます。この不一致により、os.File
がT
のフィールドとして誤って認識されることが防がれます。
つまり、この修正は、ドットインポートによって名前が衝突する可能性のある状況で、シンボル解決がより厳密に行われるようにし、複合リテラルのキーが常に現在の構造体の有効なフィールド名であることを保証します。
コアとなるコードの変更箇所
変更は主にsrc/cmd/gc/typecheck.c
ファイル内のtypecheckcomplit
関数にあります。
--- a/src/cmd/gc/typecheck.c
+++ b/src/cmd/gc/typecheck.c
@@ -2136,7 +2136,7 @@ typecheckcomplit(Node **np)
Node *l, *n, *r, **hash;
NodeList *ll;
Type *t, *f;
- Sym *s;
+ Sym *s, *s1; // s1が追加された
int32 lno;
ulong nhash;
Node *autohash[101];
@@ -2302,9 +2302,11 @@ typecheckcomplit(Node **np)
// Sym might have resolved to name in other top-level
// package, because of import dot. Redirect to correct sym
// before we do the lookup.
- if(s->pkg != localpkg && exportname(s->name))
- s = lookup(s->name);
-
+ if(s->pkg != localpkg && exportname(s->name)) {
+ s1 = lookup(s->name); // 新しいシンボルs1を導入
+ if(s1->origpkg == s->pkg) // origpkgの比較を追加
+ s = s1;
+ }
f = lookdot1(nil, s, t, t->type, 0);
if(f == nil) {
yyerror("unknown %T field '%S' in struct literal", t, s);
また、この修正を検証するための新しいテストケースがtest/fixedbugs/bug462.go
として追加されています。
// errorcheck
// Copyright 2012 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 main
import "os"
type T struct {
File int
}
func main() {
_ = T {
os.File: 1, // ERROR "unknown T field"
}
}
このテストは、T
という構造体にFile
というフィールドがあるにもかかわらず、os.File
という修飾名を使って初期化しようとすると、コンパイラが「unknown T field」というエラーを出すことを期待しています。これは、os.File
がT
のFile
フィールドとは異なるシンボルとして正しく識別され、複合リテラルのキーとして不正であると判断されることを意味します。
コアとなるコードの解説
typecheckcomplit
関数は、Goコンパイラの型チェックフェーズにおいて、複合リテラル(例: struct{}
、[]int{}
、map[string]int{}
など)の構文とセマンティクスがGo言語の仕様に準拠しているかを検証する役割を担っています。
この関数内で、特に構造体リテラルの場合、キー(フィールド名)の解決が行われます。Go言語では、構造体リテラルのフィールド名は、その構造体が定義されているパッケージ内で宣言された識別子である必要があります。
変更されたコードブロックは、以下のシナリオを扱います。
s->pkg != localpkg
: 現在処理しているシンボルs
が、現在のパッケージ(localpkg
)ではなく、別のパッケージに属している。exportname(s->name)
: そのシンボルs
の名前がエクスポートされている(つまり、大文字で始まる)。
この2つの条件が満たされるのは、主にimport . "some/package"
のようにドットインポートが使用され、some/package
内のエクスポートされたシンボルが現在のパッケージのスコープに持ち込まれた場合です。
変更前:
s = lookup(s->name);
この行は、s
が別のパッケージのシンボルであっても、その名前(例: File
)を使って現在のパッケージのスコープでシンボルを再検索していました。もし現在のパッケージに同じ名前のフィールドが存在すれば、それが解決されます。しかし、もしos.File
のような修飾名が、ドットインポートによってFile
として現在のスコープに持ち込まれた場合、lookup("File")
はos
パッケージのFile
シンボルを返してしまう可能性がありました。そして、コンパイラはそれを現在の構造体のフィールド名として誤って解釈してしまうことがありました。
変更後:
s1 = lookup(s->name);
if(s1->origpkg == s->pkg)
s = s1;
この修正は、lookup(s->name)
で得られたシンボルs1
が、元のシンボルs
と同じ「元のパッケージ」(origpkg
)に由来するかどうかを厳密にチェックします。
s1 = lookup(s->name);
: まず、現在のパッケージのスコープでs->name
(例:File
)を検索します。s1->origpkg
: これは、s1
が実際にどのパッケージで定義されたかを示す情報です。s->pkg
: これは、最初に問題となったシンボルs
がどのパッケージに属していたかを示す情報です。
もしos.File
がドットインポートされてFile
として参照された場合、s
はos
パッケージのFile
シンボルを指します。lookup("File")
は、現在のパッケージにFile
というフィールドがなければ、やはりos
パッケージのFile
シンボルをs1
として返すでしょう。この場合、s1->origpkg
とs->pkg
は両方ともos
パッケージを指すため、条件s1->origpkg == s->pkg
は真となり、s
はs1
に更新されます。これは正しい挙動です。
しかし、もしT
という構造体にFile
というフィールドがあり、かつimport . "os"
があったとします。そして、T{File: 1}
と書くべきところを誤ってT{os.File: 1}
と書いてしまった場合(これはGoの構文エラーですが、バグのあるコンパイラはこれを処理しようとしました)。
この時、s
はos
パッケージのFile
シンボルを指します。lookup("File")
は、現在のパッケージのT
構造体のFile
フィールドを指すシンボルをs1
として返します。このs1
のorigpkg
は現在のパッケージであり、s
のpkg
はos
パッケージです。これらは異なるため、s1->origpkg == s->pkg
の条件は偽となり、s
は更新されません。結果として、lookdot1
関数(フィールドのルックアップを行う関数)は、os.File
がT
の有効なフィールドではないと判断し、適切なエラー(unknown T field
)を報告するようになります。
この修正により、コンパイラは複合リテラルのキーとして、そのリテラルが属する型(例: 構造体)の有効なフィールド名のみを受け入れるようになり、異なるパッケージの修飾名が誤ってフィールド名として解釈されることがなくなりました。
関連リンク
- Go issue #4067: https://github.com/golang/go/issues/4067
- Gerrit Code Review 6622056: https://golang.org/cl/6622056
参考にした情報源リンク
- Go issue #4067の議論と詳細
- Go言語の仕様書(特に複合リテラルとパッケージに関するセクション)
- Goコンパイラのソースコード(
src/cmd/gc/typecheck.c
) - Go言語の
import .
に関するドキュメントや解説記事 - コンパイラの型チェックとシンボル解決に関する一般的な知識