[インデックス 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
に渡した場合に問題を引き起こしました。ParseExpr
は a[i]
の部分を式として解析し、残りの := x
を無視してしまいます。その結果、gofmt
は意図しない、または不完全なASTに基づいてコードを再構築し、予期せぬ、あるいは誤ったコードを生成してしまう可能性がありました。
Issue 6099 はこの問題点を指摘しており、gofmt
のリライト機能の信頼性を向上させるために、ParseExpr
がより厳密に式の解析を行う必要があるという認識が生まれました。このコミットは、その問題に部分的に対処し、ParseExpr
が余分なトークンを検出した場合に明示的にエラーを報告するようにすることで、gofmt
やその他のツールがより堅牢に動作するための基盤を強化します。
前提知識の解説
このコミットを理解するためには、以下のGo言語のパーサー関連の概念とツールに関する知識が必要です。
go/parser
パッケージ: Go言語のソースコードを解析し、抽象構文木 (AST) を構築するための標準ライブラリです。Goのコンパイラ、gofmt
、go vet
などの多くのツールがこのパッケージを利用してソースコードを理解します。go/ast
パッケージ:go/parser
が生成する抽象構文木 (AST) のノード構造を定義するパッケージです。ASTは、ソースコードの構造を木構造で表現したもので、プログラムの意味を解析したり、変換したりするために使用されます。ast.Expr
は、Goの式を表すASTノードのインターフェースです。ParseExpr
関数:go/parser
パッケージが提供する関数の一つで、与えられた文字列をGoの式として解析し、対応するast.Expr
を返します。この関数は、例えばa + b
やfoo.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.go
の ParseExpr
関数に以下のロジックが追加されました。
- セミコロンの消費:
p.tok == token.SEMICOLON
のチェックが追加されました。これは、Goの文法規則において、式がセミコロンで終わることが許容される場合があるためです。もし現在のトークンがセミコロンであれば、p.next()
を呼び出してそのセミコロンを消費します。これは、自動セミコロン挿入 (ASI) の結果としてセミコロンが存在する場合や、明示的にセミコロンが記述されている場合に対応するためです。 - 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
はエラーを報告し、ParseExpr
はnil
とエラーを返します。
この変更により、ParseExpr
は、与えられた文字列が厳密に単一の式であるかどうかを検証するようになり、余分なトークンが存在する場合にはエラーを報告するようになりました。
src/pkg/go/parser/parser_test.go
の変更
テストファイルには、主に以下の2つの変更があります。
-
エラーメッセージの修正:
t.Errorf("ParseExpr(%s): got %T, expected *ast.BinaryExpr", src, x)
がt.Errorf("ParseExpr(%s): got %T, want *ast.BinaryExpr", src, x)
のように、expected
がwant
に変更されています。これは、Goのテストにおける慣用的なエラーメッセージの表現に合わせたものです。 -
新しいテストケースの追加:
// 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
のようなツールがより堅牢に動作するための基盤を強化します。
関連リンク
- Go Issue 6099: cmd/gofmt: rewrite term not a valid expression is silently consumed
- Go CL 68920044: go/parser: report error if ParseExpr argument contains extra tokens