[インデックス 12426] ファイルの概要
このコミットは、Go言語のパーサー(go/parser
パッケージ)におけるエラー同期の改善を目的としています。特に、コード内でカンマが欠落している場合に、より適切なエラーメッセージを提供し、パーサーが解析を継続できるようにする変更が加えられました。
コミット
Go言語のパーサーにおいて、カンマが欠落している場合のより良いエラー同期を実現。
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/67cbe9431f9440f9d801b8dd2c7eec32d6ed2ab5
元コミット内容
go/parser: better error sync. if commas are missing
R=rsc, bradfitz
CC=golang-dev
https://golang.org/cl/5756045
変更の背景
Go言語のような厳格な構文を持つプログラミング言語では、カンマのような句読点の欠落は、コンパイラやパーサーにとって深刻な問題を引き起こす可能性があります。通常、パーサーは構文エラーに遭遇すると、それ以降のコードを正しく解釈できなくなり、多数の連鎖的なエラーメッセージを出力してしまうことがあります。これは、開発者にとって真の原因を特定することを困難にし、デバッグの効率を低下させます。
このコミットの背景には、Goのコードでカンマが欠落している、特に改行の直前にカンマがないという一般的な誤りに対して、パーサーがより賢く振る舞うべきだという認識があります。パーサーがこのような状況を検出し、単にエラーを報告するだけでなく、あたかもカンマが存在するかのように解析を継続できれば、開発者はより正確で少ないエラーメッセージを受け取ることができ、開発体験が向上します。この変更は、パーサーの堅牢性を高め、よりユーザーフレンドリーなエラー報告を実現することを目的としています。
前提知識の解説
このコミットを理解するためには、以下の概念について基本的な知識が必要です。
-
Go言語の構文解析(パーシング): Goコンパイラは、ソースコードを機械が理解できる形式に変換する過程で、まずソースコードを解析します。この解析プロセスは、大きく分けて「字句解析(Lexical Analysis)」と「構文解析(Syntactic Analysis)」の二段階で行われます。
- 字句解析(Lexical Analysis): ソースコードを最小単位の「トークン(token)」に分割します。例えば、
func
,main
,(
,)
,{
,}
、識別子、リテラルなどがトークンです。Go言語ではgo/token
パッケージがこれに関連します。 - 構文解析(Syntactic Analysis): 字句解析で生成されたトークンの並びが、言語の文法規則に合致しているかを検証し、抽象構文木(AST: Abstract Syntax Tree)を構築します。ASTは、プログラムの構造を木構造で表現したものです。Go言語では
go/parser
パッケージがこの役割を担います。
- 字句解析(Lexical Analysis): ソースコードを最小単位の「トークン(token)」に分割します。例えば、
-
go/parser
パッケージ: Go標準ライブラリの一部であり、Goのソースコードを解析してASTを生成するための機能を提供します。このパッケージは、Goツールチェインの多くの部分で利用されており、コンパイラ、go fmt
、go vet
などのツールがこれに依存しています。 -
token
パッケージ: Go言語の字句トークンを定義するパッケージです。token.COMMA
はカンマを表すトークン、token.SEMICOLON
はセミコロンを表すトークン、token.NEWLINE
は改行を表すトークンです。 -
パーサーにおけるエラー同期: パーサーが構文エラーに遭遇した際に、それ以降の解析をどのように継続するかという戦略です。単純なパーサーはエラーで停止するか、無関係なエラーを多数報告する可能性があります。より洗練されたパーサーは、エラーを検出した後も、入力ストリーム内の適切な「同期点」を見つけて解析を再開しようとします。これにより、単一の構文エラーが原因で多数の誤ったエラーメッセージが出力されるのを防ぎ、開発者にとってより有用なエラー報告が可能になります。このコミットは、特に「カンマの欠落」という特定のケースにおけるエラー同期を改善しています。
技術的詳細
このコミットの主要な変更点は、go/parser
パッケージ内のparser.go
ファイルに集中しており、主に以下の点が挙げられます。
-
expectClosing
関数の引数名変更:expectClosing
関数は、閉じ括弧や閉じ波括弧などを期待する際に使用され、その前にカンマが欠落している場合にエラーメッセージを生成します。この関数は元々construct
という引数を持っていましたが、より汎用的なcontext
という名前に変更されました。これにより、エラーメッセージが生成される文脈をより正確に表現できるようになります。変更前:
func (p *parser) expectClosing(tok token.Token, construct string) token.Pos
変更後:func (p *parser) expectClosing(tok token.Token, context string) token.Pos
これに伴い、エラーメッセージ内の文字列も
"missing ',' before newline in "+construct)
から"missing ',' before newline in "+context)
に更新されています。 -
seesComma
関数の新規導入: このコミットの最も重要な変更は、seesComma
という新しいヘルパー関数が導入されたことです。この関数は、パーサーが現在処理しているトークンがカンマであるかどうかを判断し、そうでない場合に特定の条件でエラーを報告しつつ、解析を継続するロジックを提供します。seesComma
関数の挙動は以下の通りです。- 現在のトークンが
token.COMMA
であれば、true
を返します。これは、カンマが期待通りに存在する場合です。 - 現在のトークンが
token.SEMICOLON
であり、かつそのリテラル値が改行文字(\n
)である場合、これは「改行の前にカンマが欠落している」という一般的なエラーパターンと見なされます。この場合、パーサーはp.error
メソッドを呼び出してエラーメッセージ("missing ',' before newline in "+context)
)を報告します。重要なのは、この関数がtrue
を返す点です。 これは、パーサーが「カンマがそこに挿入された」かのように振る舞い、解析を継続できるようにするためです。これにより、単一のカンマ欠落エラーが、後続の構文エラーの連鎖を引き起こすことを防ぎます。 - 上記以外のケースでは、
false
を返します。
- 現在のトークンが
-
既存の解析ロジックにおける
seesComma
の利用:seesComma
関数が導入された後、parser.go
内の複数の場所で、カンマの存在をチェックする既存のロジックがseesComma
の呼び出しに置き換えられました。これにより、カンマの欠落に対するエラーハンドリングが一元化され、改善されたseesComma
のロジックが適用されるようになりました。影響を受けた主な関数は以下の通りです。parseVarList
(変数リストの解析)parseParameterList
(パラメータリストの解析)parseCallOrConversion
(関数呼び出しや型変換の引数リストの解析)parseElementList
(複合リテラルの要素リストの解析)
これらの変更により、例えば
var a, b int
のような変数宣言でvar a b int
とカンマを忘れた場合や、f(a, b)
のような関数呼び出しでf(a b)
とカンマを忘れた場合でも、パーサーはより賢くエラーを報告し、可能であれば解析を継続できるようになります。
これらの変更は、Go言語のパーサーがより堅牢になり、開発者にとってより分かりやすいエラーメッセージを提供することで、開発効率の向上に貢献します。
コアとなるコードの変更箇所
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -362,9 +362,9 @@ func (p *parser) expect(tok token.Token) token.Pos {
// expectClosing is like expect but provides a better error message
// for the common case of a missing comma before a newline.
//
-func (p *parser) expectClosing(tok token.Token, construct string) token.Pos {
+func (p *parser) expectClosing(tok token.Token, context string) token.Pos {
if p.tok != tok && p.tok == token.SEMICOLON && p.lit == "\n" {
- p.error(p.pos, "missing ',' before newline in "+construct)
+ p.error(p.pos, "missing ',' before newline in "+context)
p.next()
}
return p.expect(tok)
@@ -376,6 +376,18 @@ func (p *parser) expectSemi() {
}
}
+func (p *parser) seesComma(context string) bool {
+ if p.tok == token.COMMA {
+ return true
+ }
+ if p.tok == token.SEMICOLON && p.lit == "\n" {
+ p.error(p.pos, "missing ',' before newline in "+context)
+ return true // "insert" the comma and continue
+
+ }
+ return false
+}
+
func assert(cond bool, msg string) {
if !cond {
panic("go/parser internal error: " + msg)
@@ -647,7 +659,7 @@ func (p *parser) parseVarList(isParam bool) (list []ast.Expr, typ ast.Expr) {
// accept them all for more robust parsing and complain later
for typ := p.parseVarType(isParam); typ != nil; {
list = append(list, typ)
- if p.tok != token.COMMA {
+ if !p.seesComma("variable list") {
break
}
p.next()
@@ -688,7 +700,7 @@ func (p *parser) parseParameterList(scope *ast.Scope, ellipsisOk bool) (params [
// Go spec: The scope of an identifier denoting a function
// parameter or result variable is the function body.
p.declare(field, nil, scope, ast.Var, idents...)
- if p.tok != token.COMMA {
+ if !p.seesComma("parameter list") {
break
}
p.next()
@@ -1078,7 +1090,7 @@ func (p *parser) parseCallOrConversion(fun ast.Expr) *ast.CallExpr {
ellipsis = p.pos
p.next()
}
- if p.tok != token.COMMA {
+ if !p.seesComma("argument list") {
break
}
p.next()
@@ -1118,7 +1130,7 @@ func (p *parser) parseElementList() (list []ast.Expr) {
for p.tok != token.RBRACE && p.tok != token.EOF {
list = append(list, p.parseElement(true))
- if p.tok != token.COMMA {
+ if !p.seesComma("composite literal") {
break
}
p.next()
コアとなるコードの解説
上記の差分は、go/parser
パッケージのparser.go
ファイルにおける主要な変更を示しています。
-
expectClosing
関数の変更:-func (p *parser) expectClosing(tok token.Token, construct string) token.Pos { +func (p *parser) expectClosing(tok token.Token, context string) token.Pos { if p.tok != tok && p.tok == token.SEMICOLON && p.lit == "\n" { - p.error(p.pos, "missing ',' before newline in "+construct) + p.error(p.pos, "missing ',' before newline in "+context) p.next() } return p.expect(tok)
この変更は、
expectClosing
関数の第2引数の名前をconstruct
からcontext
に変更したものです。これはセマンティックな改善であり、エラーメッセージ内で使用される文字列が、より一般的な「文脈」を示すように調整されました。機能的な変更はありませんが、コードの可読性と意図の明確化に貢献しています。 -
seesComma
関数の新規追加:func (p *parser) seesComma(context string) bool { if p.tok == token.COMMA { return true } if p.tok == token.SEMICOLON && p.lit == "\n" { p.error(p.pos, "missing ',' before newline in "+context) return true // "insert" the comma and continue } return false }
この新しい関数
seesComma
は、このコミットの核心です。- まず、現在のトークン
p.tok
がtoken.COMMA
(カンマ)であるかをチェックします。もしそうであれば、カンマが期待通りに存在するため、true
を返します。 - 次に、現在のトークンが
token.SEMICOLON
(セミコロン)であり、かつそのリテラル値p.lit
が改行文字"\n"
であるかをチェックします。Go言語では、改行がセミコロンとして扱われる自動セミコロン挿入のルールがあります。この条件が真である場合、それはプログラマが改行の前にカンマを書き忘れた可能性が高いことを示唆します。- この状況では、
p.error
を呼び出して「context
内で改行の前にカンマが欠落している」というエラーメッセージを報告します。 - そして、
true
を返します。 このtrue
の返却が重要です。これは、パーサーが「カンマがそこに存在すると仮定して」解析を継続することを意味します。これにより、パーサーはエラーを報告しつつも、その後の構文解析を続行でき、単一の欠落したカンマが原因で発生する可能性のある多数の連鎖的なエラーを回避します。
- この状況では、
- 上記のいずれの条件も満たさない場合、カンマは存在しないため
false
を返します。
- まず、現在のトークン
-
既存の解析ロジックにおける
seesComma
の利用:parseVarList
,parseParameterList
,parseCallOrConversion
,parseElementList
といった関数では、これまでif p.tok != token.COMMA { break }
のような形でカンマの存在を直接チェックしていました。このコミットでは、これらのチェックがif !p.seesComma(...) { break }
という形に置き換えられました。 例えば、parseVarList
の変更箇所は以下のようになります。- if p.tok != token.COMMA { + if !p.seesComma("variable list") { break }
これにより、これらの構文要素(変数リスト、パラメータリスト、引数リスト、複合リテラル要素リスト)の解析中にカンマが欠落し、かつそれが改行前のセミコロンとして解釈される場合に、
seesComma
関数が介入して適切なエラーメッセージを生成し、パーサーがより堅牢に動作するようになります。
これらの変更は、Go言語のパーサーがより「賢く」なり、一般的なコーディングミスに対してより有用なフィードバックを提供できるようになることを示しています。
関連リンク
- Go言語公式ドキュメント: https://go.dev/
go/parser
パッケージドキュメント: https://pkg.go.dev/go/parsergo/token
パッケージドキュメント: https://pkg.go.dev/go/token
参考にした情報源リンク
- Go言語の構文解析に関する一般的な情報源
- パーサーにおけるエラー回復戦略に関する一般的な情報源