[インデックス 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 .に関するドキュメントや解説記事 - コンパイラの型チェックとシンボル解決に関する一般的な知識