[インデックス 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
ステートメント(ゴルーチン)に関する公式ブログ記事やチュートリアル - 正規表現に関する一般的な情報源