[インデックス 18746] ファイルの概要
このコミットは、Go言語のパーサー(go/parserパッケージ)におけるエラー報告の精度向上に関するものです。具体的には、goステートメントやdeferステートメントで関数が正しく呼び出されていない場合に報告されるエラーの位置が不正確であった問題を修正しています。また、パースエラー時にトークンの終了位置が有効な範囲外になる可能性があった問題も同時に解決しています。
コミット
commit 1624c73c9d98ad3466db0648a8462e8720cfa4aa
Author: Robert Griesemer <gri@golang.org>
Date: Tue Mar 4 14:10:30 2014 -0800
go/parser: better error position for non-invoked gp/defer functions
Added test cases and expanded test harness to handle token end
positions.
Also: Make sure token end positions are never outside the valid
position range, as was possible in case of parse errors.
Fixes #7458.
LGTM=bradfitz
R=bradfitz
CC=golang-codereviews
https://golang.org/cl/70190046
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/1624c73c9d98ad3466db0648a8462e8720cfa4aa
元コミット内容
Go言語のパーサーにおいて、goおよびdeferステートメント内で関数が呼び出されていない場合の、エラー位置の報告を改善する。テストケースを追加し、トークンの終了位置を扱うためのテストハーネスを拡張した。また、パースエラーの場合にトークンの終了位置が有効な位置範囲外になる可能性があった問題を修正した。
変更の背景
Go言語のコンパイラやツールチェーンにおいて、コードの構文解析(パース)は非常に重要なステップです。パーサーはソースコードを読み込み、抽象構文木(AST)を構築します。この過程で構文エラーが検出された場合、ユーザーに正確なエラーメッセージとエラーが発生したコード上の位置(行番号、列番号など)を報告する必要があります。
このコミット以前は、goステートメントやdeferステートメントにおいて、関数呼び出しが期待される場所で実際には関数が呼び出されていない(例: go fではなくgo f()とすべきところをgo fと記述した場合)場合に、エラーメッセージは出力されるものの、そのエラーが指し示す位置が不正確であるという問題がありました。具体的には、エラーが報告されるべき場所(関数名の直後)ではなく、ステートメントの開始位置など、より広範な範囲を指してしまうことがありました。
また、パーサーがASTノードの終了位置を計算する際に、トークンの開始位置に長さを加算することで終了位置を導出します。しかし、パースエラーが発生した場合など、トークンの開始位置自体が不正な値であると、計算された終了位置がファイルの有効な範囲(EOF以降)を超えてしまう可能性がありました。このような無効な位置情報が後続の処理で利用されると、パニック(プログラムの異常終了)を引き起こす原因となり得ました。
これらの問題を解決し、よりユーザーフレンドリーなエラー報告と、パーサーの堅牢性を向上させることがこのコミットの目的です。
前提知識の解説
goステートメント: Go言語の並行処理を記述するためのキーワードです。goキーワードの後に続く関数呼び出しを新しいゴルーチン(軽量スレッド)として実行します。例:go myFunction()deferステートメント: Go言語の遅延実行を記述するためのキーワードです。deferキーワードの後に続く関数呼び出しを、その関数が属する関数の終了時(return時、パニック時など)に実行するようにスケジュールします。例:defer file.Close()token.Pos: Go言語のgo/tokenパッケージで定義されている型で、ソースコード内の位置を表します。通常、ファイル内のバイトオフセットとして表現されます。ast.BadExpr/ast.BadStmt: Go言語のgo/astパッケージで定義されているASTノードです。パーサーが構文エラーを検出した際に、不正な式やステートメントの代わりにこれらのノードをASTに挿入します。これにより、エラーが発生してもパーサーが処理を続行し、可能な限り多くのエラーを一度に報告できるようになります。FromフィールドとToフィールドを持ち、不正なコードの範囲を示します。scanner.ScanComments:go/scannerパッケージのオプションで、コメントもスキャン対象に含めることを指定します。これにより、テストハーネスがソースコード内の特別なコメント(例:/* ERROR "rx" */)を解析して、期待されるエラーメッセージと位置を抽出できます。- 正規表現 (Regular Expression): テキスト内のパターンを記述するための強力なツールです。このコミットでは、テストハーネスがエラーコメントから期待されるエラーメッセージを抽出するために使用しています。
技術的詳細
このコミットは、主に以下の2つの技術的改善を導入しています。
-
safePos関数の導入:parser.goにsafePosという新しいメソッドが追加されました。この関数は、与えられたtoken.Posが有効なファイル位置であるかを検証し、もし無効であればファイルのEOF(End Of File)位置を返します。- 動作原理:
p.file.Offset(pos)を呼び出すことで、posがp.file(現在のファイル)の有効な範囲内にあるかを間接的にチェックします。Offsetメソッドは、無効な位置が与えられた場合にパニックを引き起こす可能性があります。safePosはdeferとrecoverを使ってこのパニックを捕捉し、パニックが発生した場合はp.file.Base() + p.file.Size()(ファイルのEOF位置)を返します。これにより、ASTノードのToフィールドなどに設定される位置情報が常に有効な範囲内に収まることが保証されます。 - 適用箇所:
ast.BadExprやast.BadStmtのToフィールドを設定する際に、既存のx.End()やlist[n-1].End()の代わりにp.safePos(...)が使用されるようになりました。これにより、パースエラーによって生成される不正なASTノードの範囲が、ファイルの有効な範囲を超えないようになります。
- 動作原理:
-
error_test.goのテストハーネスの拡張: エラー位置のテストをより正確に行うために、error_test.goのテストハーネスが拡張されました。/* ERROR HERE "rx" */コメントの導入: 新しいコメント形式/* ERROR HERE "rx" */が導入されました。これは、エラーメッセージが特定のトークンの直後に発生する場合(例:go fのfの直後)に、そのトークンの終了位置をエラー位置として指定するために使用されます。here変数の追加:expectedErrors関数内にhereという新しいtoken.Pos変数が追加されました。これは、直前の非コメント・非セミコロンのトークンの直後の位置(つまり、そのトークンの終了位置)を追跡します。- トークン長の計算:
scanner.Scan()で取得したトークン(tok)とリテラル(lit)から、そのトークンの長さを計算し、prev(トークンの開始位置)に加算することでhereの位置を更新します。これにより、ERROR HEREコメントが正確なエラー位置を指し示すことができるようになりました。 - 正規表現の更新:
errRx正規表現が更新され、HEREキーワードをオプションで捕捉できるようになりました。
これらの変更により、パーサーはより正確なエラー位置を報告できるようになり、特にgoやdeferステートメントにおける関数呼び出しの欠落といった一般的な構文エラーに対して、開発者が問題を特定しやすくなりました。また、無効な位置情報によるパニックの可能性も排除され、パーサー全体の堅牢性が向上しています。
コアとなるコードの変更箇所
src/pkg/go/parser/error_test.go
errRx正規表現の変更:^/\* *ERROR *(HERE)? *\"([^\"]*)\" *\*/$expectedErrors関数内の変更:var here token.Posの追加。if len(s) == 3ブロック内でs[1] == "HERE"の場合にpos = hereを設定するロジックの追加。defaultケースでhereの位置を計算するロジックの追加:var l int // token length if tok.IsLiteral() { l = len(lit) } else { l = len(tok.String()) } here = prev + token.Pos(l)
src/pkg/go/parser/parser.go
safePosメソッドの追加:func (p *parser) safePos(pos token.Pos) (res token.Pos) { defer func() { if recover() != nil { res = token.Pos(p.file.Base() + p.file.Size()) // EOF position } }() _ = p.file.Offset(pos) // trigger a panic if position is out-of-range return pos }ast.BadExprやast.BadStmtのToフィールド設定箇所でsafePosの利用:parseFieldDecl:typ = &ast.BadExpr{From: pos, To: p.safePos(list[n-1].End())}checkExpr:x = &ast.BadExpr{From: x.Pos(), To: p.safePos(x.End())}checkExprOrType:x = &ast.BadExpr{From: x.Pos(), To: p.safePos(x.End())}makeExpr:return &ast.BadExpr{From: s.Pos(), To: p.safePos(s.End())}parseForStmt:return &ast.BadStmt{From: pos, To: p.safePos(body.End())}parseReceiver:{Type: &ast.BadExpr{From: recv.Pos(), To: p.safePos(recv.End())}},
parseCallExprのシグネチャ変更とエラーメッセージの改善:func (p *parser) parseCallExpr() *ast.CallExprからfunc (p *parser) parseCallExpr(callType string) *ast.CallExprへ変更。- エラーメッセージを
p.errorExpected(x.Pos(), "function/method call")からp.error(p.safePos(x.End()), fmt.Sprintf("function must be invoked in %s statement", callType))へ変更。エラー位置にp.safePos(x.End())を使用し、エラーメッセージにcallType("go"または"defer")を含めるようにした。
parseGoStmtとparseDeferStmtでのparseCallExprの呼び出し変更:call := p.parseCallExpr()からcall := p.parseCallExpr("go")call := p.parseCallExpr()からcall := p.parseCallExpr("defer")
src/pkg/go/parser/short_test.go
- 新しいテストケースの追加:
`package p; func f() { go f /* ERROR HERE "function must be invoked" */ }`, `package p; func f() { defer func() {} /* ERROR HERE "function must be invoked" */ }`, `package p; func f() { go func() { func() { f(x func /* ERROR "expected ')'" */ (){}) } } }`,
コアとなるコードの解説
このコミットの核心は、パーサーがエラーを報告する際の「位置」の正確性を向上させることにあります。
-
safePos関数: この関数は、GoのパーサーがASTノードの終了位置を計算する際に、その位置がファイルの有効な範囲内にあることを保証するための安全装置として機能します。パーサーは通常、トークンの開始位置にそのトークンの長さを加算することで終了位置を導出しますが、パースエラーによって開始位置自体が不正な場合、計算結果の終了位置がファイルの実際の終端を超えてしまうことがあります。safePosはこのような「人工的な」無効な位置を検出し、代わりにファイルのEOF位置を返すことで、後続の処理でのパニックを防ぎます。これは、deferとrecoverを用いたGo特有のエラーハンドリングパターンを効果的に利用しています。 -
parseCallExprの変更とgo/deferステートメントのエラー報告: 以前は、go fやdefer fのように関数が呼び出されていない場合、エラーメッセージは「function/method callが期待される」という一般的なもので、エラー位置もステートメントの開始位置を指すことがありました。 この変更により、parseCallExprはcallType("go"または"defer")を受け取るようになり、エラーメッセージが「goステートメントでは関数が呼び出されなければならない」のように、より具体的になりました。 さらに重要なのは、エラー位置がp.safePos(x.End())、つまり関数名(f)の直後を指すようになったことです。これにより、開発者はコードのどこを修正すべきか、より直感的に理解できるようになります。 -
error_test.goのテストハーネスの拡張:/* ERROR HERE "rx" */コメントとhere変数の導入は、エラー位置のテストを非常に強力にしました。これにより、エラーが特定のトークンの「直後」に発生する場合でも、その正確な位置をテストケースで指定できるようになりました。これは、パーサーが報告するエラー位置の粒度を細かくし、テストの信頼性を高める上で不可欠な変更です。
これらの変更は、Go言語のツールチェーンが提供するエラーメッセージの品質を向上させ、開発者がより効率的にコードの誤りを修正できるようにするための重要な改善です。
関連リンク
- Go Issue #7458: go/parser: better error position for non-invoked gp/defer functions
- Go CL 70190046: go/parser: better error position for non-invoked gp/defer functions
参考にした情報源リンク
- Go言語の公式ドキュメント (go.dev)
- Go言語のソースコード (github.com/golang/go)
- Go言語の
go/parserパッケージのドキュメント - Go言語の
go/tokenパッケージのドキュメント - Go言語の
go/astパッケージのドキュメント - Go言語の
go/scannerパッケージのドキュメント - Go言語の
deferステートメントに関する公式ブログ記事やチュートリアル - Go言語の
goステートメント(ゴルーチン)に関する公式ブログ記事やチュートリアル - 正規表現に関する一般的な情報源