[インデックス 14078] ファイルの概要
このコミットは、Go言語のパーサー(go/parser
パッケージ)におけるバグ修正を目的としています。具体的には、var
(変数)宣言が誤ってconst
(定数)として解析され、抽象構文木(AST)上で不正確なオブジェクト種別(ObjKind
)が割り当てられてしまう問題を修正します。この修正により、パーサーが生成するASTの正確性が向上し、それに続くコンパイラやツールチェーンの処理が正しく行われるようになります。また、この修正を検証するための新しいテストケースが追加されています。
コミット
commit 05fc42ab0272a72b5c8e67b757398c328f5bbcac
Author: Robert Griesemer <gri@golang.org>
Date: Sun Oct 7 17:58:13 2012 -0700
go/parser: fix object kind
Bug introduced with CL 6624047.
R=r
CC=golang-dev
https://golang.org/cl/6620073
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/05fc42ab0272a72b5c8e67b757398c328f5bbcac
元コミット内容
このコミットは、Go言語のパーサーにおいて、オブジェクトの種別(ObjKind
)が正しく設定されないバグを修正します。このバグは、以前の変更セット(CL 6624047)によって導入されたものです。
変更の背景
Go言語のコンパイラや開発ツールは、ソースコードを解析して抽象構文木(AST: Abstract Syntax Tree)を構築します。このASTは、コードの構造を表現するデータ構造であり、その後の型チェック、最適化、コード生成などのフェーズで利用されます。AST内の各ノードは、ソースコードの特定の要素(変数、定数、関数、型など)に対応し、それぞれが適切な「種別」(ObjKind
)を持つことが極めて重要です。
このコミットで修正されたバグは、go/parser
パッケージのparseValueSpec
関数に存在していました。この関数は、const
(定数)やvar
(変数)といった値の宣言を解析する役割を担っています。しかし、バグのあるバージョンでは、var
宣言であっても、常にそのオブジェクト種別をast.Con
(定数)として扱っていました。
この誤った種別設定は、以下のような問題を引き起こす可能性があります。
- 型チェックの不正確さ: 変数と定数では、その値の変更可能性や使用方法に関するルールが異なります。AST上で種別が誤っていると、型チェッカーが誤った前提で解析を進め、本来エラーとなるべきコードを見逃したり、逆に正当なコードをエラーと判断したりする可能性があります。
- ツール連携の障害: Goのツールエコシステム(
go vet
,gopls
などのLSPサーバー、IDE連携など)は、パーサーが生成する正確なASTに依存しています。ObjKind
の誤りは、これらのツールがコードの意味を正しく理解することを妨げ、リファクタリング、コード補完、静的解析などの機能に悪影響を及ぼします。 - デバッグの困難さ: コンパイラ内部で発生する問題の根本原因を特定する際に、ASTの不正確さがデバッグを困難にすることがあります。
このバグは、CL 6624047という以前の変更によって導入されたと明記されており、これはリグレッション(回帰バグ)であることを示しています。したがって、このコミットは、パーサーの正確性を回復し、Go言語のコンパイラおよび関連ツールの信頼性を維持するために不可欠な修正でした。
前提知識の解説
このコミットを理解するためには、Go言語の構文解析と抽象構文木(AST)に関する基本的な知識が必要です。
1. Go言語の構文解析(Parsing)
Goコンパイラの最初の段階の一つは、ソースコードを解析し、その構造をコンピュータが扱いやすい形式に変換することです。このプロセスは「構文解析」または「パース」と呼ばれ、go/parser
パッケージがその役割を担います。パーサーは、ソースコードの文字列を読み込み、Go言語の文法規則に従って、そのコードがどのように構成されているかを理解します。
2. 抽象構文木(AST: Abstract Syntax Tree)
構文解析の結果として生成されるのがASTです。ASTは、ソースコードの構造を木構造で表現したものです。各ノードは、プログラムの特定の構文要素(例えば、関数宣言、変数宣言、式、ステートメントなど)を表します。ASTは、ソースコードの具体的な構文(括弧やセミコロンなど)を抽象化し、その意味的な構造に焦点を当てます。
go/ast
パッケージ: Go言語の標準ライブラリには、ASTを表現するためのgo/ast
パッケージが含まれています。このパッケージは、ASTノードの型定義(例:ast.File
,ast.FuncDecl
,ast.Ident
など)を提供します。
3. ast.Ident
(識別子)
ast.Ident
は、GoのASTにおいて、変数名、関数名、型名、定数名、パッケージ名など、プログラム内の「名前」を表すノードです。例えば、var x int
という宣言では、x
とint
がそれぞれast.Ident
としてASTに表現されます。
4. ast.Object
とast.ObjKind
(オブジェクトとオブジェクト種別)
GoのASTでは、識別子(ast.Ident
)が参照する実体(変数、定数、関数など)をast.Object
として表現します。ast.Object
は、その識別子がプログラム内でどのような役割を持つかを示す「種別」(Kind
フィールド)を持っています。この種別はast.ObjKind
型で定義されており、以下のような種類があります。
ast.Bad
: 不正なオブジェクト。解析エラーなど。ast.Pkg
: パッケージ。ast.Con
: 定数(const
宣言)。ast.Typ
: 型(type
宣言)。ast.Var
: 変数(var
宣言)。ast.Fun
: 関数(func
宣言)。ast.Lbl
: ラベル(L:
のようなラベル宣言)。
このコミットの核心は、var
宣言された識別子にast.Var
ではなくast.Con
が誤って割り当てられていた点にあります。
5. token
パッケージとtoken.Token
Go言語の字句解析(Lexing)段階では、ソースコードの文字列を意味のある最小単位(トークン)に分割します。token
パッケージは、これらのトークンを表現するための定数を提供します。
token.VAR
:var
キーワードを表すトークン。token.CONST
:const
キーワードを表すトークン。
パーサーは、これらのトークンを読み取り、それに基づいてASTを構築します。parseValueSpec
関数は、token.VAR
やtoken.CONST
といったキーワードを識別し、それに応じて適切なASTノードを生成する必要があります。
技術的詳細
このコミットの技術的詳細は、go/parser
パッケージ内のparseValueSpec
関数の修正と、その検証のためのテストコードの追加に集約されます。
parseValueSpec
関数の役割
parseValueSpec
関数は、Goソースコードにおけるconst
またはvar
キーワードで始まる値の宣言(例: const pi = 3.14
や var x int
)を解析し、対応するast.ValueSpec
ノードを構築する役割を担っています。この関数は、宣言された識別子(例: pi
やx
)に対して、その種類(定数か変数か)を示すObjKind
を割り当て、スコープに登録します。
修正前の問題点
修正前のparseValueSpec
関数では、値の宣言を処理する際に、常にast.Con
(定数)というObjKind
を使用していました。これは、const
宣言の場合は正しい動作ですが、var
宣言の場合には誤りとなります。
// 修正前の関連コード(概念)
func (p *parser) parseValueSpec(...) {
// ...
// ここで常に ast.Con を使用していた
p.declare(spec, iota, p.topScope, ast.Con, idents...)
// ...
}
p.declare
関数は、識別子を現在のスコープに登録し、その際に指定されたObjKind
を識別子のObj
フィールドに設定します。したがって、var
宣言であってもast.Con
が渡されると、その変数はAST上で定数として扱われてしまうという問題がありました。
修正内容
このコミットでは、parseValueSpec
関数にシンプルな条件分岐が追加されました。この条件分岐は、現在解析している宣言がconst
宣言なのかvar
宣言なのかを、その宣言のキーワード(keyword
引数)に基づいて判断します。
// 修正後の関連コード
kind := ast.Con // デフォルトは定数
if keyword == token.VAR { // もしキーワードが 'var' なら
kind = ast.Var // 種別を変数に設定
}
p.declare(spec, iota, p.topScope, kind, idents...) // 正しい種別で宣言
これにより、keyword
がtoken.VAR
(つまりvar
宣言)である場合にはkind
がast.Var
に設定され、それ以外の場合(主にconst
宣言)にはast.Con
が使用されるようになります。この変更によって、var
宣言された識別子には正しくast.Var
のObjKind
が割り当てられるようになり、ASTの正確性が保証されます。
テストの追加
この修正の正しさを検証するために、src/pkg/go/parser/parser_test.go
にTestObjects
という新しいテスト関数が追加されました。このテストは、様々な種類の宣言(import
, const
, type
, var
, func
, ラベル)を含むGoソースコードを解析し、生成されたAST内の各識別子(ast.Ident
)が持つObjKind
が期待される値と一致するかどうかを検証します。
特に、var x int
という宣言に対して、x
のObjKind
がast.Var
であることを確認するアサーションが含まれており、これが今回のバグ修正を直接的に検証する役割を果たしています。このテストは、ast.Inspect
関数を使用してASTを走査し、各識別子のObj
フィールドにアクセスしてKind
をチェックするという、GoのAST操作における一般的なパターンを示しています。
コアとなるコードの変更箇所
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -2116,7 +2116,11 @@ func (p *parser) parseValueSpec(doc *ast.CommentGroup, keyword token.Token, iota
Values: values,
Comment: p.lineComment,
}
- p.declare(spec, iota, p.topScope, ast.Con, idents...)
+ kind := ast.Con
+ if keyword == token.VAR {
+ kind = ast.Var
+ }
+ p.declare(spec, iota, p.topScope, kind, idents...)
return spec
}
src/pkg/go/parser/parser_test.go
--- a/src/pkg/go/parser/parser_test.go
+++ b/src/pkg/go/parser/parser_test.go
@@ -135,6 +135,53 @@ func TestVarScope(t *testing.T) {
}\n`
}
+func TestObjects(t *testing.T) {
+ const src = `
+package p
+import fmt "fmt"
+const pi = 3.14
+type T struct{}
+var x int
+func f() { L: }
+`
+
+ f, err := ParseFile(fset, "", src, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ objects := map[string]ast.ObjKind{
+ "p": ast.Bad, // not in a scope
+ "fmt": ast.Bad, // not resolved yet
+ "pi": ast.Con,
+ "T": ast.Typ,
+ "x": ast.Var,
+ "int": ast.Bad, // not resolved yet
+ "f": ast.Fun,
+ "L": ast.Lbl,
+ }
+
+ ast.Inspect(f, func(n ast.Node) bool {
+ if ident, ok := n.(*ast.Ident); ok {
+ obj := ident.Obj
+ if obj == nil {
+ if objects[ident.Name] != ast.Bad {
+ t.Errorf("no object for %s", ident.Name)
+ }
+ return true
+ }
+ if obj.Name != ident.Name {
+ t.Errorf("names don't match: obj.Name = %s, ident.Name = %s", obj.Name, ident.Name)
+ }
+ kind := objects[ident.Name]
+ if obj.Kind != kind {
+ t.Errorf("%s: obj.Kind = %s; want %s", ident.Name, obj.Kind, kind)
+ }
+ }
+ return true
+ })
+}
+
func TestUnresolved(t *testing.T) {
f, err := ParseFile(fset, "", `
package p
コアとなるコードの解説
src/pkg/go/parser/parser.go
の変更
parseValueSpec
関数は、const
またはvar
宣言を解析する際に呼び出されます。この関数は、宣言のキーワード(keyword
引数)を受け取ります。
変更前は、p.declare
関数を呼び出す際に、オブジェクトの種別として常にast.Con
(定数)を渡していました。
- p.declare(spec, iota, p.topScope, ast.Con, idents...)
変更後は、keyword
がtoken.VAR
(つまりvar
キーワード)であるかどうかをチェックする条件分岐が追加されました。
+ kind := ast.Con
+ if keyword == token.VAR {
+ kind = ast.Var
+ }
+ p.declare(spec, iota, p.topScope, kind, idents...)
kind := ast.Con
: まず、デフォルトのオブジェクト種別をast.Con
(定数)に設定します。これは、const
宣言の場合に適用されます。if keyword == token.VAR
:keyword
がtoken.VAR
(var
キーワード)と等しいかどうかをチェックします。kind = ast.Var
: もしkeyword
がtoken.VAR
であれば、kind
をast.Var
(変数)に上書きします。p.declare(spec, iota, p.topScope, kind, idents...)
: 最後に、決定された正しいkind
(ast.Con
またはast.Var
)を使用して、識別子をスコープに宣言します。これにより、var
宣言された識別子には正しくast.Var
の種別が割り当てられるようになります。
src/pkg/go/parser/parser_test.go
の変更
TestObjects
という新しいテスト関数が追加されました。このテストは、パーサーが様々な種類のGo言語の要素に対して正しいObjKind
を割り当てることを検証します。
-
テスト対象のソースコード (
src
):const src = ` package p import fmt "fmt" const pi = 3.14 type T struct{} var x int func f() { L: } `
この文字列には、パッケージ宣言、インポート、定数、型、変数、関数、ラベルといった、Go言語の主要な宣言が含まれています。
-
期待されるオブジェクト種別のマップ (
objects
):objects := map[string]ast.ObjKind{ "p": ast.Bad, // not in a scope "fmt": ast.Bad, // not resolved yet "pi": ast.Con, "T": ast.Typ, "x": ast.Var, "int": ast.Bad, // not resolved yet "f": ast.Fun, "L": ast.Lbl, }
このマップは、ソースコード内の各識別子名(キー)に対して、パーサーが割り当てるべき期待される
ObjKind
(値)を定義しています。"p"
,"fmt"
,"int"
がast.Bad
とされているのは、これらが現在のテストのスコープでは解決されない(または解決を期待しない)識別子であるためです。例えば、package p
のp
はパッケージ名であり、通常の変数や定数とは異なる扱いです。fmt
はインポートされたパッケージ名、int
は組み込み型であり、これらはgo/parser
の段階ではast.Bad
として扱われるのが期待されます。"pi"
はconst
宣言なのでast.Con
。"T"
はtype
宣言なのでast.Typ
。"x"
はvar
宣言なのでast.Var
。このエントリが、parser.go
の修正を直接検証する重要な部分です。"f"
はfunc
宣言なのでast.Fun
。"L"
はラベルなのでast.Lbl
。
-
ASTの走査と検証:
ast.Inspect(f, func(n ast.Node) bool { if ident, ok := n.(*ast.Ident); ok { obj := ident.Obj if obj == nil { // ... (objがnilの場合のチェック) return true } // ... (名前の一致チェック) kind := objects[ident.Name] if obj.Kind != kind { t.Errorf("%s: obj.Kind = %s; want %s", ident.Name, obj.Kind, kind) } } return true })
ast.Inspect(f, func(n ast.Node) bool { ... })
は、解析されたファイルf
のASTを深さ優先で走査します。if ident, ok := n.(*ast.Ident); ok { ... }
は、現在のノードn
がast.Ident
(識別子)である場合に処理を進めます。obj := ident.Obj
: 識別子ident
が参照するast.Object
を取得します。if obj.Kind != kind
: 取得したobj
のKind
フィールドが、objects
マップで定義された期待されるkind
と一致しない場合、テストエラーを報告します。
このTestObjects
の追加により、var
宣言が正しくast.Var
として解析されることを含め、パーサーが様々なGo言語の要素に対して正確なObjKind
を割り当てる能力が保証されるようになりました。
関連リンク
- Go Change-Id: https://golang.org/cl/6620073
参考にした情報源リンク
- Go言語公式ドキュメント:
go/ast
パッケージ - Go言語公式ドキュメント:
go/parser
パッケージ - Go言語公式ドキュメント:
go/token
パッケージ - Go言語のASTに関する一般的な解説(例: Go AST Explorerなど)
- https://go.dev/blog/go-ast-part1 (Go公式ブログのASTに関する記事)
- https://astexplorer.net/ (Goを含む様々な言語のASTを視覚化するツール)