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

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

Go言語のパーサーが、式の解析において末尾に明示的なセミコロンが存在する場合に、それを不正な構文として適切に扱わない問題を修正するコミットです。具体的には、go/parser パッケージの ParseExpr 関数が、改行による自動セミコロン挿入と明示的なセミコロンを区別し、式に続く明示的なセミコロンをエラーとして報告するように変更されました。

コミット

commit 4f14d1520253bd5d3dc19ab8b8668308d5bdcd64
Author: Robert Griesemer <gri@golang.org>
Date:   Tue Jun 17 08:58:08 2014 -0700

    go/parser: don't accept trailing explicit semicolon
    
    Fixes #8207.
    
    LGTM=gordon.klaus, bradfitz
    R=golang-codereviews, wandakkelly, gordon.klaus, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/106010046

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

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

元コミット内容

go/parser: don't accept trailing explicit semicolon

Fixes #8207.

LGTM=gordon.klaus, bradfitz
R=golang-codereviews, wandakkelly, gordon.klaus, bradfitz
CC=golang-codereviews
https://golang.org/cl/106010046

変更の背景

このコミットは、Go言語の構文解析器(パーサー)が、式(expression)の解析において、その末尾に明示的なセミコロンが存在する場合の挙動を修正するものです。Go言語の文法では、セミコロンは文の終端を示すために使用されますが、多くの場合、改行によって自動的に挿入(Automatic Semicolon Insertion: ASI)されます。しかし、開発者がコード中に明示的にセミコロンを記述することも可能です。

go/parser パッケージの ParseExpr 関数は、与えられた文字列をGoの式として解析することを目的としています。しかし、以前の実装では、有効な式が解析された後に、その末尾に明示的なセミコロンが続く場合、パーサーがそれを適切にエラーとして扱わないという問題がありました。Goの文法において、式自体はセミコロンで終わる必要はなく、セミコロンは文の終端に挿入されるものです。したがって、ParseExpr が式を解析する際には、式が完全に解析された後に明示的なセミコロンが残っている場合、それは不正な入力としてエラーを報告すべきです。

コミットメッセージには Fixes #8207 とありますが、この特定のIssueの詳細は現在のGoのIssueトラッカーでは見つかりませんでした。しかし、コミット内容から判断すると、この修正はGoの文法規則の厳密な適用を保証し、パーサーが曖昧な構文を許容しないようにすることを目的としています。これにより、コンパイラやツールがより正確にGoコードを解釈できるようになります。

前提知識の解説

Go言語の構文解析 (Parsing)

Goコンパイラの一部であり、ソースコードを読み込み、その文法構造を解析して抽象構文木 (Abstract Syntax Tree: AST) を構築するプロセスです。go/parser パッケージは、この構文解析機能を提供し、Goのソースコードをプログラムで操作可能なASTに変換します。

抽象構文木 (AST)

ソースコードの抽象的な構文構造を木構造で表現したものです。ASTは、コンパイラがソースコードの意味を理解し、型チェック、最適化、コード生成などを行うための基盤となります。各ノードは、式、文、宣言などの構文要素を表します。

トークン (Token) と字句解析 (Lexical Analysis)

構文解析の前に、ソースコードは字句解析器(lexerまたはscanner)によってトークンのストリームに変換されます。トークンは、ソースコードの最小の意味のある単位です。例えば、a + b; というコードは、a (識別子)、+ (演算子)、b (識別子)、; (セミコロン) といったトークンに分解されます。go/token パッケージは、Go言語のトークン型を定義しています。

自動セミコロン挿入 (Automatic Semicolon Insertion: ASI)

Go言語の最も特徴的な文法規則の一つです。Goでは、特定の条件下で改行の後に自動的にセミコロンが挿入されます。これにより、開発者は多くのセミコロンを省略でき、コードの記述が簡潔になります。ASIの主なルールは以下の通りです。

  1. 識別子、整数、浮動小数点数、虚数、ルーン、文字列リテラルの後。
  2. break, continue, fallthrough, return の後。
  3. ++, --, ) の後。
  4. } の後。

これらのトークンの直後に改行がある場合、その改行の前に自動的にセミコロンが挿入されます。このコミットの文脈では、ParseExpr が式を解析する際に、明示的に記述されたセミコロンと、ASIによって挿入されたセミコロンをパーサーがどのように区別し、処理するかが重要になります。Goの文法では、式自体はセミコロンで終わる必要はなく、セミコロンは文の終端に挿入されるものです。

技術的詳細

Goのパーサーは、字句解析器からトークンを受け取り、それらを基に構文木を構築します。go/parser パッケージ内の ParseExpr 関数は、与えられた文字列をGoの式として解析し、有効な場合はその式のAST表現を返します。

パーサーは、現在のトークン (p.tok) とそのリテラル値 (p.lit) を内部的に保持しています。p.tok はトークンの種類(例: token.SEMICOLON)を示し、p.lit はそのトークンの実際の文字列値(例: ; または改行を表す \n)を示します。

このコミット以前の ParseExpr の実装では、式の解析を終えた後、もし現在のトークンが token.SEMICOLON であれば、それが明示的なセミコロンであろうとASIによるものであろうと、区別なく消費していました。そして、その後にさらにトークンがあればエラーとしていました。

しかし、この挙動には問題がありました。ParseExpr は「式」を解析する関数であり、式が完全に解析された後、その後に明示的なセミコロンが続くのはGoの文法上、不正な構文と見なされるべきです。例えば、a + b; という入力は、ParseExpr にとっては a + b が式であり、; は余分なトークンであるべきです。しかし、ASIによって挿入されるセミコロン(改行によって表現される)は、文脈によっては許容される場合があります。

このコミットの目的は、この曖昧さを解消し、ParseExpr がより厳密にGoの文法規則に従うようにすることです。具体的には、式が終了した後に残るセミコロンが、改行によって自動的に挿入されたものである場合のみそれを消費し、明示的に記述されたセミコロンである場合はエラーとして報告するように変更されました。これにより、ParseExpr は「式」の解析に特化し、式以外の余分なトークン(特に明示的なセミコロン)が存在する場合にはエラーを返すという、より堅牢な挙動をするようになります。

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

src/pkg/go/parser/interface.go

--- a/src/pkg/go/parser/interface.go
+++ b/src/pkg/go/parser/interface.go
@@ -184,7 +184,7 @@ func ParseExpr(x string) (ast.Expr, error) {

 	// If a semicolon was inserted, consume it;
 	// report an error if there's more tokens.
-	if p.tok == token.SEMICOLON {
+	if p.tok == token.SEMICOLON && p.lit == "\n" {
 		p.next()
 	}
 	p.expect(token.EOF)

src/pkg/go/parser/parser_test.go

--- a/src/pkg/go/parser/parser_test.go
+++ b/src/pkg/go/parser/parser_test.go
@@ -74,36 +74,54 @@ func TestParseExpr(t *testing.T) {
 	src := "a + b"
 	x, err := ParseExpr(src)
 	if err != nil {
-		t.Fatalf("ParseExpr(%s): %v", src, err)
+		t.Errorf("ParseExpr(%q): %v", src, err)
 	}
 	// sanity check
 	if _, ok := x.(*ast.BinaryExpr); !ok {
-		t.Errorf("ParseExpr(%s): got %T, want *ast.BinaryExpr", src, x)
+		t.Errorf("ParseExpr(%q): got %T, want *ast.BinaryExpr", src, x)
 	}

 	// a valid type expression
 	src = "struct{x *int}"
 	x, err = ParseExpr(src)
 	if err != nil {
-		t.Fatalf("ParseExpr(%s): %v", src, err)
+		t.Errorf("ParseExpr(%q): %v", src, err)
 	}
 	// sanity check
 	if _, ok := x.(*ast.StructType); !ok {
-		t.Errorf("ParseExpr(%s): got %T, want *ast.StructType", src, x)
+		t.Errorf("ParseExpr(%q): got %T, want *ast.StructType", src, err)
 	}

 	// an invalid expression
 	src = "a + *"
-	_, err = ParseExpr(src)
-	if err == nil {
-		t.Fatalf("ParseExpr(%s): got no error", src)
+	if _, err := ParseExpr(src); err == nil {
+		t.Errorf("ParseExpr(%q): got no error", src)
 	}

 	// a valid expression followed by extra tokens is invalid
 	src = "a[i] := x"
-	_, err = ParseExpr(src)
-	if err == nil {
-		t.Fatalf("ParseExpr(%s): got no error", src)
+	if _, err := ParseExpr(src); err == nil {
+		t.Errorf("ParseExpr(%q): got no error", src)
+	}
+
+	// a semicolon is not permitted unless automatically inserted
+	src = "a + b\n"
+	if _, err := ParseExpr(src); err != nil {
+		t.Errorf("ParseExpr(%q): got error %s", src, err)
+	}
+	src = "a + b;"
+	if _, err := ParseExpr(src); err == nil {
+		t.Errorf("ParseExpr(%q): got no error", src)
+	}
+
+	// various other stuff following a valid expression
+	const validExpr = "a + b"
+	const anything = "dh3*#D)#_"
+	for _, c := range "!)]};," {
+		src := validExpr + string(c) + anything
+		if _, err := ParseExpr(src); err == nil {
+			t.Errorf("ParseExpr(%q): got no error", src)
+		}
 	}

 	// ParseExpr must not crash

コアとなるコードの解説

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

この変更は、ParseExpr 関数内のセミコロン処理のロジックを修正しています。

  • 変更前:

    if p.tok == token.SEMICOLON {
        p.next()
    }
    

    これは、現在のトークンがセミコロンであれば、そのセミコロンを無条件に消費(スキップ)するという意味です。このロジックでは、明示的に記述されたセミコロンと、改行によって自動挿入されたセミコロンの区別がありませんでした。

  • 変更後:

    if p.tok == token.SEMICOLON && p.lit == "\n" {
        p.next()
    }
    

    この変更がこのコミットの核心です。p.tok == token.SEMICOLON に加えて、p.lit == "\n" という条件が追加されました。

    • p.tok == token.SEMICOLON: 現在のトークンがセミコロンであることを示します。
    • p.lit == "\n": そのセミコロンが、字句解析器によって改行文字 (\n) から生成されたものであることを示します。これは、Goの自動セミコロン挿入 (ASI) ルールによって挿入されたセミコロンであることを意味します。

    この修正により、パーサーは、改行によって自動的に挿入されたセミコロンである場合のみそれを消費するようになりました。もし p.tok == token.SEMICOLON であっても p.lit != "\n" であれば、それは明示的に記述されたセミコロンであり、ParseExpr が式を解析する文脈では、式が終了した後に明示的なセミコロンが続くのは不正な構文と見なされるべきです。この変更によって、パーサーはこのような不正な明示的セミコロンを消費せず、結果としてその後の p.expect(token.EOF) で「予期しないトークン」としてエラーを報告するようになります。

    これにより、ParseExpr は「式」の解析に特化し、式以外の余分なトークン(特に明示的なセミコロン)が存在する場合にはエラーを返すという、より厳密な挙動をするようになります。

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

テストファイルでは、主に以下の変更が行われています。

  • エラー報告の改善: 既存のテストケースで t.Fatalft.Errorf に変更されています。t.Fatalf はテストが失敗した場合に即座にテスト関数を終了させますが、t.Errorf はエラーを報告しつつもテストの実行を継続します。これにより、一つのエラーでテスト全体が停止することなく、複数のエラーを一度に検出できるようになります。

  • セミコロン処理に関する新しいテストケースの追加:

    • src = "a + b\n" のケース: これは、有効な式 a + b の後に改行が続くケースです。GoのASIルールにより、この改行は自動的にセミコロンとして解釈されるべきです。したがって、ParseExpr はエラーを返すべきではありません。このテストは、ASIによるセミコロンが正しく処理されることを確認します。
    • src = "a + b;" のケース: これは、有効な式 a + b の後に明示的なセミコロンが続くケースです。このコミットの修正により、ParseExpr はこのような明示的なセミコロンを不正な余分なトークンとして検出し、エラーを返すべきです。テストでは、エラーが返されない場合に t.Errorf を呼び出し、期待されるエラーが正しく発生することを確認しています。
    • validExpr + string(c) + anything のループ: a + b のような有効な式の後に、!, ), ], }, ;, , といった様々な区切り文字が続き、さらに任意の文字列が続く場合に、ParseExpr がエラーを返すことを確認しています。特に ; のケースは、上記の interface.go の変更が正しく機能し、明示的なセミコロンが正しくエラーとして検出されるかを検証するものです。

これらのテストケースは、ParseExpr が式の解析において、Goの文法規則に厳密に従い、式以外の余分なトークン(特に明示的なセミコロン)を正しくエラーとして検出できるようになったことを確認するために追加されました。これにより、パーサーの堅牢性と正確性が向上します。

関連リンク

参考にした情報源リンク