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

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

このコミットは、Go言語のパーサーライブラリ go/parser における ParseExpr 関数の挙動を改善し、予期せぬトークンが引数に含まれる場合にエラーを報告するように変更するものです。これにより、gofmt のようなツールが不正な入力によって誤ったリライトを行う問題を部分的に解決します。

コミット

commit 85f59b34291a9e16bf3a2e7db586cd824a121825
Author: Robert Griesemer <gri@golang.org>
Date:   Wed Feb 26 09:54:01 2014 -0800

    go/parser: report error if ParseExpr argument contains extra tokens
    
    This partly addresses issue 6099 where a gofmt rewrite is behaving
    unexpectedly because the provided rewrite term is not a valid expression
    but is silently consumed anyway.
    
    LGTM=bradfitz
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/68920044

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

https://github.com/golang/go/commit/85f59b34291a9e16bf3a2e7db586cd824a121825

元コミット内容

go/parser: report error if ParseExpr argument contains extra tokens

このコミットは、ParseExpr 関数の引数に余分なトークンが含まれている場合にエラーを報告するようにします。これは、gofmt のリライトが予期せぬ動作をする原因となっていた、提供されたリライト項が有効な式ではないにもかかわらず、黙って消費されてしまうという Issue 6099 に部分的に対処するものです。

変更の背景

Go言語のツールチェインには、コードのフォーマットを自動的に行う gofmt という非常に重要なツールがあります。gofmt は、Goのソースコードを解析し、抽象構文木 (AST: Abstract Syntax Tree) を構築し、それを標準的なフォーマット規則に従って再出力することで動作します。このプロセスにおいて、gofmt はコードの一部をリライト(書き換え)する機能も持っています。

しかし、このコミットが修正しようとしている問題は、gofmt がリライトを行う際に、与えられた入力が完全な式ではないにもかかわらず、go/parser パッケージの ParseExpr 関数がその入力を黙って受け入れてしまうというものでした。具体的には、ParseExpr は与えられた文字列をGoの式として解析しますが、式の終端以降に余分なトークンが存在しても、それをエラーとして報告せず、単に無視していました。

この「黙って消費する」挙動は、gofmt がリライトを行う際に、例えば a[i] := x のような、式としては不完全な(または式ではない)文字列を ParseExpr に渡した場合に問題を引き起こしました。ParseExpra[i] の部分を式として解析し、残りの := x を無視してしまいます。その結果、gofmt は意図しない、または不完全なASTに基づいてコードを再構築し、予期せぬ、あるいは誤ったコードを生成してしまう可能性がありました。

Issue 6099 はこの問題点を指摘しており、gofmt のリライト機能の信頼性を向上させるために、ParseExpr がより厳密に式の解析を行う必要があるという認識が生まれました。このコミットは、その問題に部分的に対処し、ParseExpr が余分なトークンを検出した場合に明示的にエラーを報告するようにすることで、gofmt やその他のツールがより堅牢に動作するための基盤を強化します。

前提知識の解説

このコミットを理解するためには、以下のGo言語のパーサー関連の概念とツールに関する知識が必要です。

  • go/parser パッケージ: Go言語のソースコードを解析し、抽象構文木 (AST) を構築するための標準ライブラリです。Goのコンパイラ、gofmtgo vet などの多くのツールがこのパッケージを利用してソースコードを理解します。
  • go/ast パッケージ: go/parser が生成する抽象構文木 (AST) のノード構造を定義するパッケージです。ASTは、ソースコードの構造を木構造で表現したもので、プログラムの意味を解析したり、変換したりするために使用されます。ast.Expr は、Goの式を表すASTノードのインターフェースです。
  • ParseExpr 関数: go/parser パッケージが提供する関数の一つで、与えられた文字列をGoの式として解析し、対応する ast.Expr を返します。この関数は、例えば a + bfoo.Bar() のような単一の式を解析するのに使われます。
  • go/token パッケージ: Go言語の字句解析(トークン化)で使われるトークン(キーワード、識別子、演算子など)を定義するパッケージです。
    • token.SEMICOLON: Go言語におけるセミコロン(;)を表すトークンです。Goでは、文の終わりにセミコロンが自動挿入される(ASI: Automatic Semicolon Insertion)ルールがありますが、明示的に記述されることもあります。
    • token.EOF: End Of File の略で、入力ストリームの終端を表すトークンです。パーサーが入力の最後まで到達したことを示します。
  • gofmt: Go言語の公式フォーマッターツールです。Goのソースコードを標準的なスタイルに自動的に整形します。gofmt は、コードの可読性を高め、スタイルに関する議論を減らすことを目的としています。また、コードのリファクタリングや変換を行う際にも利用されることがあります。
  • 抽象構文木 (AST): プログラムのソースコードの抽象的な構文構造を、木構造で表現したものです。各ノードはソースコードの構成要素(式、文、宣言など)を表し、その子ノードは構成要素の内部構造を表します。コンパイラやリンター、コードフォーマッターなどのツールは、ASTを操作することでコードを分析・変換します。

技術的詳細

このコミットの技術的な核心は、go/parser パッケージの ParseExpr 関数が、入力文字列の解析後に余分なトークンが残っていないかを厳密にチェックするようになった点です。

変更前は、ParseExpr は式として有効な部分を解析し終えると、それ以降のトークンを黙って無視していました。例えば、"a + b ; c" という文字列が与えられた場合、a + b を式として解析し、; c の部分はエラーとせずに無視していました。これは、ParseExpr が単一の式を解析することを意図しているにもかかわらず、その意図に反する挙動でした。

このコミットでは、src/pkg/go/parser/interface.goParseExpr 関数に以下のロジックが追加されました。

  1. セミコロンの消費: p.tok == token.SEMICOLON のチェックが追加されました。これは、Goの文法規則において、式がセミコロンで終わることが許容される場合があるためです。もし現在のトークンがセミコロンであれば、p.next() を呼び出してそのセミコロンを消費します。これは、自動セミコロン挿入 (ASI) の結果としてセミコロンが存在する場合や、明示的にセミコロンが記述されている場合に対応するためです。
  2. EOFの期待: p.expect(token.EOF) が呼び出されます。これは、パーサーが入力の終端 (EOF) に到達していることを期待するという明示的なチェックです。もし EOF 以外のトークンが残っていた場合(つまり、式として解析された部分の後に余分なトークンが存在した場合)、p.expect 関数はエラーを報告します。

この変更により、ParseExpr("a[i] := x") のような入力に対して、以前は a[i] を式として解析し、残りを無視してエラーを報告しなかったものが、今後は := x の部分が余分なトークンとして検出され、エラーが報告されるようになります。

また、src/pkg/go/parser/parser_test.go には、この新しい挙動を検証するためのテストケースが追加されています。特に、"a[i] := x" のような「有効な式の後に余分なトークンが続く」ケースが、エラーを発生させるべきであることを確認するテストが追加されました。これにより、ParseExpr がより厳密な入力チェックを行うようになったことが保証されます。

この変更は、go/parser の堅牢性を高め、gofmt のようなツールがより信頼性の高いコード変換を行えるようにするための重要なステップです。

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

--- a/src/pkg/go/parser/interface.go
+++ b/src/pkg/go/parser/interface.go
@@ -182,6 +182,13 @@ func ParseExpr(x string) (ast.Expr, error) {
 	p.closeScope()
 	assert(p.topScope == nil, "unbalanced scopes")
 
+	// If a semicolon was inserted, consume it;
+	// report an error if there's more tokens.
+	if p.tok == token.SEMICOLON {
+		p.next()
+	}
+	p.expect(token.EOF)
+
 	if p.errors.Len() > 0 {
 		p.errors.Sort()
 		return nil, p.errors.Err()
--- a/src/pkg/go/parser/parser_test.go
+++ b/src/pkg/go/parser/parser_test.go
@@ -78,7 +78,7 @@ func TestParseExpr(t *testing.T) {
 	}
 	// sanity check
 	if _, ok := x.(*ast.BinaryExpr); !ok {
-		t.Errorf("ParseExpr(%s): got %T, expected *ast.BinaryExpr", src, x)
+		t.Errorf("ParseExpr(%s): got %T, want *ast.BinaryExpr", src, x)
 	}
 
 	// a valid type expression
@@ -89,17 +89,24 @@ func TestParseExpr(t *testing.T) {
 	}
 	// sanity check
 	if _, ok := x.(*ast.StructType); !ok {
-		t.Errorf("ParseExpr(%s): got %T, expected *ast.StructType", src, x)
+		t.Errorf("ParseExpr(%s): got %T, want *ast.StructType", src, x)
 	}
 
 	// an invalid expression
 	src = "a + *"
 	_, err = ParseExpr(src)
 	if err == nil {
-		t.Fatalf("ParseExpr(%s): %v", src, err)
+		t.Fatalf("ParseExpr(%s): 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)
 	}
 
-	// it must not crash
+	// ParseExpr must not crash
 	for _, src := range valids {
 		ParseExpr(src)
 	}

コアとなるコードの解説

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

ParseExpr 関数の末尾に、以下の7行が追加されています。

	// If a semicolon was inserted, consume it;
	// report an error if there's more tokens.
	if p.tok == token.SEMICOLON {
		p.next()
	}
	p.expect(token.EOF)
  • p.tok == token.SEMICOLON のチェック: パーサーの現在のトークン (p.tok) がセミコロンであるかをチェックします。Goの文法では、文の終わりにセミコロンが自動挿入されるルール(Automatic Semicolon Insertion, ASI)があるため、式がセミコロンで終わることは有効な場合があります。
  • p.next(): もし現在のトークンがセミコロンであれば、p.next() を呼び出して次のトークンに進みます。これにより、有効なセミコロンを消費し、その後の p.expect(token.EOF) が正しく機能するようにします。
  • p.expect(token.EOF): これは、パーサーが入力の終端 (EOF) に到達していることを期待する重要なアサーションです。ParseExpr は単一の式を解析することを目的としているため、式が完全に解析された後には、入力ストリームにこれ以上トークンが残っていない(つまり、EOF である)ことを期待します。もし EOF 以外のトークンが残っていた場合、p.expect はエラーを報告し、ParseExprnil とエラーを返します。

この変更により、ParseExpr は、与えられた文字列が厳密に単一の式であるかどうかを検証するようになり、余分なトークンが存在する場合にはエラーを報告するようになりました。

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

テストファイルには、主に以下の2つの変更があります。

  1. エラーメッセージの修正: t.Errorf("ParseExpr(%s): got %T, expected *ast.BinaryExpr", src, x)t.Errorf("ParseExpr(%s): got %T, want *ast.BinaryExpr", src, x) のように、expectedwant に変更されています。これは、Goのテストにおける慣用的なエラーメッセージの表現に合わせたものです。

  2. 新しいテストケースの追加: // 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) }

    このテストケースは、このコミットの主要な目的を検証するものです。

    • src = "a[i] := x": a[i] は有効な式ですが、:= x は式としては余分なトークンです。
    • _, err = ParseExpr(src): この文字列を ParseExpr で解析します。
    • if err == nil { t.Fatalf("ParseExpr(%s): got no error", src) }: 以前の挙動ではエラーが報告されませんでしたが、このコミットの変更によりエラーが報告されるはずです。もしエラーが報告されなければ、テストは失敗します。これにより、ParseExpr が余分なトークンを正しく検出してエラーを報告するようになったことが確認されます。

これらの変更は、ParseExpr の挙動をより厳密にし、gofmt のようなツールがより堅牢に動作するための基盤を強化します。

関連リンク

参考にした情報源リンク