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

[インデックス 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(定数)として扱っていました。

この誤った種別設定は、以下のような問題を引き起こす可能性があります。

  1. 型チェックの不正確さ: 変数と定数では、その値の変更可能性や使用方法に関するルールが異なります。AST上で種別が誤っていると、型チェッカーが誤った前提で解析を進め、本来エラーとなるべきコードを見逃したり、逆に正当なコードをエラーと判断したりする可能性があります。
  2. ツール連携の障害: Goのツールエコシステム(go vet, goplsなどのLSPサーバー、IDE連携など)は、パーサーが生成する正確なASTに依存しています。ObjKindの誤りは、これらのツールがコードの意味を正しく理解することを妨げ、リファクタリング、コード補完、静的解析などの機能に悪影響を及ぼします。
  3. デバッグの困難さ: コンパイラ内部で発生する問題の根本原因を特定する際に、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という宣言では、xintがそれぞれast.IdentとしてASTに表現されます。

4. ast.Objectast.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.VARtoken.CONSTといったキーワードを識別し、それに応じて適切なASTノードを生成する必要があります。

技術的詳細

このコミットの技術的詳細は、go/parserパッケージ内のparseValueSpec関数の修正と、その検証のためのテストコードの追加に集約されます。

parseValueSpec関数の役割

parseValueSpec関数は、Goソースコードにおけるconstまたはvarキーワードで始まる値の宣言(例: const pi = 3.14var x int)を解析し、対応するast.ValueSpecノードを構築する役割を担っています。この関数は、宣言された識別子(例: pix)に対して、その種類(定数か変数か)を示す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...) // 正しい種別で宣言

これにより、keywordtoken.VAR(つまりvar宣言)である場合にはkindast.Varに設定され、それ以外の場合(主にconst宣言)にはast.Conが使用されるようになります。この変更によって、var宣言された識別子には正しくast.VarObjKindが割り当てられるようになり、ASTの正確性が保証されます。

テストの追加

この修正の正しさを検証するために、src/pkg/go/parser/parser_test.goTestObjectsという新しいテスト関数が追加されました。このテストは、様々な種類の宣言(import, const, type, var, func, ラベル)を含むGoソースコードを解析し、生成されたAST内の各識別子(ast.Ident)が持つObjKindが期待される値と一致するかどうかを検証します。

特に、var x intという宣言に対して、xObjKindast.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...)

変更後は、keywordtoken.VAR(つまりvarキーワード)であるかどうかをチェックする条件分岐が追加されました。

+	kind := ast.Con
+	if keyword == token.VAR {
+		kind = ast.Var
+	}
+	p.declare(spec, iota, p.topScope, kind, idents...)
  1. kind := ast.Con: まず、デフォルトのオブジェクト種別をast.Con(定数)に設定します。これは、const宣言の場合に適用されます。
  2. if keyword == token.VAR: keywordtoken.VARvarキーワード)と等しいかどうかをチェックします。
  3. kind = ast.Var: もしkeywordtoken.VARであれば、kindast.Var(変数)に上書きします。
  4. p.declare(spec, iota, p.topScope, kind, idents...): 最後に、決定された正しいkindast.Conまたはast.Var)を使用して、識別子をスコープに宣言します。これにより、var宣言された識別子には正しくast.Varの種別が割り当てられるようになります。

src/pkg/go/parser/parser_test.go の変更

TestObjectsという新しいテスト関数が追加されました。このテストは、パーサーが様々な種類のGo言語の要素に対して正しいObjKindを割り当てることを検証します。

  1. テスト対象のソースコード (src):

    const src = `
    package p
    import fmt "fmt"
    const pi = 3.14
    type T struct{}
    var x int
    func f() { L: }
    `
    

    この文字列には、パッケージ宣言、インポート、定数、型、変数、関数、ラベルといった、Go言語の主要な宣言が含まれています。

  2. 期待されるオブジェクト種別のマップ (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 ppはパッケージ名であり、通常の変数や定数とは異なる扱いです。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
  3. 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 { ... }は、現在のノードnast.Ident(識別子)である場合に処理を進めます。
    • obj := ident.Obj: 識別子identが参照するast.Objectを取得します。
    • if obj.Kind != kind: 取得したobjKindフィールドが、objectsマップで定義された期待されるkindと一致しない場合、テストエラーを報告します。

このTestObjectsの追加により、var宣言が正しくast.Varとして解析されることを含め、パーサーが様々なGo言語の要素に対して正確なObjKindを割り当てる能力が保証されるようになりました。

関連リンク

参考にした情報源リンク