[インデックス 15246] ファイルの概要
このコミットは、Go言語のパーサー(go/parser
パッケージ)におけるエラー処理の挙動を変更するものです。具体的には、ParseFile
関数が遭遇するエラーの数が10個を超えた場合に、それ以上のエラー処理を停止し、パニックを発生させるように修正されています。このパニックはParseFile
内で捕捉され、既存のエラーリストが返されることで、パーサーの堅牢性とパフォーマンスのバランスが改善されています。
コミット
commit 2cd96806f4b04545b056ddaa53234fd15e821a1f
Author: Michael Matloob <matloob@google.com>
Date: Thu Feb 14 11:26:21 2013 -0800
go/parser: stop ParseFile after ten errors.
There wil be a panic if more than ten errors are encountered. ParseFile
will recover and return the ErrorList.
Fixes #3943.
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/7307085
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/2cd96806f4b04545b056ddaa53234fd15e821a1f
元コミット内容
go/parser: stop ParseFile after ten errors.
There wil be a panic if more than ten errors are encountered. ParseFile will recover and return the ErrorList.
Fixes #3943.
変更の背景
この変更は、Go言語のIssue #3943に対応するものです。元のIssueでは、go/parser
パッケージが非常に多くの構文エラーを含むファイルをパースしようとした際に、パフォーマンスが著しく低下するという問題が報告されていました。特に、数千行にわたる単一の行に構文エラーが集中しているような極端なケースでは、パーサーが無限ループに近い状態に陥り、CPUを大量に消費し続ける可能性がありました。
このような状況は、不正な入力ファイルや悪意のあるコードが与えられた場合に、パーサーがリソースを過剰に消費し、サービス拒否(DoS)攻撃のような状態を引き起こすリスクをはらんでいました。パーサーは通常、構文エラーを検出して報告する役割を担いますが、エラーが多すぎる場合には、すべてのエラーを詳細に報告するよりも、早期に処理を打ち切ってリソースの枯渇を防ぐことが重要になります。
このコミットは、エラーが一定数(この場合は10個)を超えた場合に、それ以上のエラー検出と処理を停止し、リソースの消費を抑えることを目的としています。これにより、パーサーの堅牢性が向上し、不正な入力に対する耐性が高まります。
前提知識の解説
Go言語のgo/parser
パッケージ
go/parser
パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。Goコンパイラや各種ツール(go fmt
, go vet
など)の基盤として利用されています。
このパッケージは、字句解析(lexing)と構文解析(parsing)の2つの主要なフェーズで動作します。字句解析器(scanner)がソースコードをトークンに分解し、パーサーがそれらのトークンから構文木を構築します。
エラー処理とErrorList
go/parser
は、構文エラーを検出すると、それをscanner.Error
型として記録し、複数のエラーをscanner.ErrorList
にまとめて管理します。ParseFile
関数は、パースが完了した後にこのErrorList
を返します。通常、パーサーは可能な限り多くのエラーを報告しようとしますが、エラーが非常に多い場合には、その処理自体がボトルネックとなることがあります。
パニックとリカバリー(panic
and recover
)
Go言語には、プログラムの異常終了を示すpanic
と、そのパニックを捕捉してプログラムの実行を継続させるrecover
というメカニズムがあります。panic
は、通常、回復不可能なエラーやプログラマーの想定外の状況が発生した場合に用いられます。recover
は、defer
関数内で呼び出され、パニックが発生したスタックフレームから制御を奪い、プログラムを正常な状態に戻すことを試みます。
このコミットでは、エラー数が閾値を超えた場合に意図的にパニックを発生させ、それをParseFile
のdefer
ブロックで捕捉することで、通常の制御フローでは難しい早期終了を実現しています。
ast
パッケージ
ast
(Abstract Syntax Tree)パッケージは、Go言語のソースコードの抽象構文木を表現するためのデータ構造を提供します。go/parser
パッケージがソースコードを解析して生成する結果は、このast
パッケージで定義された構造体(例: ast.File
, ast.Decl
, ast.Expr
など)のインスタンスとして表現されます。
技術的詳細
このコミットの主要な変更点は、go/parser
パッケージのエラー処理ロジックに、エラー数の上限を設けたことです。
- エラー数の閾値設定:
parser.error
メソッド内で、p.errors.Len()
(現在記録されているエラーの数)が10以上になった場合、かつSpuriousErrors
モード(後述)が有効でない場合に、panic(bailout{})
が呼び出されるようになりました。 bailout
型: 新たにbailout
という空の構造体型が定義されています。これは、パーサーの早期終了を示すためのマーカーとして使用されます。特定の型のパニックを発生させることで、recover
時にそのパニックがパーサーの意図した早期終了であるかどうかを識別できます。ParseFile
でのパニック捕捉:ParseFile
関数は、defer
ステートメントを使用してパニックを捕捉するようになりました。defer
関数内でrecover()
が呼び出され、パニックの値がe
に格納されます。_ = e.(bailout)
という型アサーションが行われます。これは、捕捉されたパニックがbailout
型でなければ、再度パニックを発生させる(つまり、意図しないパニックは捕捉しない)ためのガードです。- パニックが捕捉された後、
f
(*ast.File
)がnil
の場合には、APIの要件を満たすために空のast.File
インスタンスが作成されます。 - 最後に、
p.errors.Sort()
が呼び出され、エラーリストがソートされた後、p.errors.Err()
が返されます。これにより、パニックによって中断された場合でも、それまでに収集されたエラーが適切に返されることが保証されます。
AllErrors
モードの導入:Mode
型にAllErrors
という新しいフラグが追加されました。これはSpuriousErrors
と同じ値を持つエイリアスであり、後方互換性を保ちつつ、より明確な名前を提供します。SpuriousErrors
またはAllErrors
モードが有効な場合、エラー数の上限チェックは行われず、すべてのエラーが報告されます。これは、デバッグ時や、すべてのエラーを網羅的に取得したい場合に利用されます。error_test.go
の変更: テストコードにおいて、found.RemoveMultiples()
がcheckErrors
関数に追加されています。これは、このコミットの変更とは直接関係ありませんが、エラーリストから重複するエラーを削除する処理がテストの早い段階で行われるようになったことを示しています。
このメカニズムにより、パーサーは大量のエラーに遭遇した場合でも、リソースを無駄に消費することなく、迅速に処理を終了し、それまでに検出したエラーを報告できるようになります。
コアとなるコードの変更箇所
src/pkg/go/parser/interface.go
--- a/src/pkg/go/parser/interface.go
+++ b/src/pkg/go/parser/interface.go
@@ -52,12 +52,13 @@ func readSource(filename string, src interface{}) ([]byte, error) {
type Mode uint
const (
- PackageClauseOnly Mode = 1 << iota // parsing stops after package clause
- ImportsOnly // parsing stops after import declarations
- ParseComments // parse comments and add them to AST
- Trace // print a trace of parsed productions
- DeclarationErrors // report declaration errors
- SpuriousErrors // report all (not just the first) errors per line
+ PackageClauseOnly Mode = 1 << iota // parsing stops after package clause
+ ImportsOnly // parsing stops after import declarations
+ ParseComments // parse comments and add them to AST
+ Trace // print a trace of parsed productions
+ DeclarationErrors // report declaration errors
+ SpuriousErrors // same as AllErrors, for backward-compatibility
+ AllErrors = SpuriousErrors // report all (not just the first 10) errors per file
)
// ParseFile parses the source code of a single Go source file and returns
@@ -79,35 +80,39 @@ const (\n // representing the fragments of erroneous source code). Multiple errors
// are returned via a scanner.ErrorList which is sorted by file position.\n //
-func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (*ast.File, error) {\n+func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error) {\n // get source\n text, err := readSource(filename, src)\n if err != nil {\n return nil, err\n }\n \n-\t// parse source\n \tvar p parser\n-\tp.init(fset, filename, text, mode)\n-\tf := p.parseFile()\n-\tif f == nil {\n-\t\t// source is not a valid Go source file - satisfy\n-\t\t// ParseFile API and return a valid (but) empty\n-\t\t// *ast.File\n-\t\tf = &ast.File{\n-\t\t\tName: new(ast.Ident),\n-\t\t\tScope: ast.NewScope(nil),\n+\tdefer func() {\n+\t\tif e := recover(); e != nil {\n+\t\t\t_ = e.(bailout) // re-panics if it\'s not a bailout\n+\t\t}\n+\n+\t\t// set result values\n+\t\tif f == nil {\n+\t\t\t// source is not a valid Go source file - satisfy\n+\t\t\t// ParseFile API and return a valid (but) empty\n+\t\t\t// *ast.File\n+\t\t\tf = &ast.File{\n+\t\t\t\tName: new(ast.Ident),\n+\t\t\t\tScope: ast.NewScope(nil),\n+\t\t\t}\n \t\t}\n-\t}\n \n-\t// sort errors\n-\tif p.mode&SpuriousErrors == 0 {\n-\t\tp.errors.RemoveMultiples()\n-\t} else {\n \t\tp.errors.Sort()\n-\t}\n+\t\terr = p.errors.Err()\n+\t}()\n \n-\treturn f, p.errors.Err()\n+\t// parse source\n+\tp.init(fset, filename, text, mode)\n+\tf = p.parseFile()\n+\n+\treturn\n }\n \n // ParseDir calls ParseFile for the files in the directory specified by path and
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -340,7 +340,13 @@ func (p *parser) next() {\n }\n }\n \n+// A bailout panic is raised to indicate early termination.\n+type bailout struct{}\n+\n func (p *parser) error(pos token.Pos, msg string) {\n+\tif p.mode&SpuriousErrors == 0 && p.errors.Len() >= 10 {\n+\t\tpanic(bailout{})\n+\t}\n \tp.errors.Add(p.file.Position(pos), msg)\n }\n \n```
### `src/pkg/go/parser/error_test.go`
```diff
--- a/src/pkg/go/parser/error_test.go
+++ b/src/pkg/go/parser/error_test.go
@@ -145,6 +145,7 @@ func checkErrors(t *testing.T, filename string, input interface{}) {\n \t\tt.Error(err)\n \t\treturn\n \t}\n+\tfound.RemoveMultiples()\n \n \t// we are expecting the following errors\n \t// (collect these after parsing a file so that it is found in the file set)\n```
## コアとなるコードの解説
### `src/pkg/go/parser/interface.go`の変更
* **`Mode`定数の追加**:
```go
+ AllErrors = SpuriousErrors // report all (not just the first 10) errors per file
```
`AllErrors`という新しい`Mode`フラグが追加されました。これは`SpuriousErrors`と同じ値を持つエイリアスであり、パーサーがすべてのエラーを報告するように指示します。このフラグが設定されている場合、後述のエラー数によるパニックは発生しません。
* **`ParseFile`関数の変更**:
```go
-func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (*ast.File, error) {
+func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error) {
```
関数のシグネチャが変更され、戻り値`f`と`err`が名前付き戻り値として宣言されました。これにより、`defer`関数内でこれらの値を直接設定できるようになります。
```go
+ defer func() {
+ if e := recover(); e != nil {
+ _ = e.(bailout) // re-panics if it's not a bailout
+ }
+
+ // set result values
+ if f == nil {
+ // source is not a valid Go source file - satisfy
+ // ParseFile API and return a valid (but) empty
+ // *ast.File
+ f = &ast.File{
+ Name: new(ast.Ident),\n+\t\t\t\tScope: ast.NewScope(nil),
+ }
+ }
+
+ p.errors.Sort()
+ err = p.errors.Err()
+ }()
```
この`defer`ブロックが、このコミットの核心部分です。
* `recover()`が呼び出され、パニックが発生した場合にその値`e`を捕捉します。
* `_ = e.(bailout)`は、捕捉されたパニックが`bailout`型であることを確認します。もし`bailout`型でなければ、それはパーサーが意図しないパニックであるため、再度パニックを発生させます。これにより、パーサーの内部的な早期終了と、予期せぬランタイムエラーによるパニックを区別しています。
* `f == nil`の場合、つまりパースが途中で中断され`ast.File`が生成されなかった場合に、APIの要件を満たすために空の`ast.File`が作成されます。
* `p.errors.Sort()`は、収集されたエラーをファイル位置に基づいてソートします。
* `err = p.errors.Err()`は、`ErrorList`からエラーオブジェクトを取得し、`ParseFile`の戻り値`err`に設定します。これにより、パニックによって中断された場合でも、それまでに検出されたエラーが呼び出し元に返されます。
```go
- // parse source
- var p parser
- p.init(fset, filename, text, mode)
- f := p.parseFile()
- if f == nil {
- // source is not a valid Go source file - satisfy
- // ParseFile API and return a valid (but) empty
- // *ast.File
- f = &ast.File{
- Name: new(ast.Ident),
- Scope: ast.NewScope(nil),
- }
- }
-
- // sort errors
- if p.mode&SpuriousErrors == 0 {
- p.errors.RemoveMultiples()
- } else {
- p.errors.Sort()
- }
-
- return f, p.errors.Err()
+ // parse source
+ p.init(fset, filename, text, mode)
+ f = p.parseFile()
+
+ return
```
元のエラーソートロジックと`f`が`nil`の場合の処理が`defer`ブロック内に移動され、`ParseFile`の本体はよりシンプルになりました。
### `src/pkg/go/parser/parser.go`の変更
* **`bailout`型の定義**:
```go
+// A bailout panic is raised to indicate early termination.
+type bailout struct{}
```
パーサーの早期終了を示すための空の構造体`bailout`が定義されました。
* **`error`メソッドの変更**:
```go
func (p *parser) error(pos token.Pos, msg string) {
+ if p.mode&SpuriousErrors == 0 && p.errors.Len() >= 10 {
+ panic(bailout{})
+ }
p.errors.Add(p.file.Position(pos), msg)
}
```
`parser`構造体の`error`メソッドは、パーサーがエラーを記録する際に呼び出されます。このメソッド内に、エラー数のチェックが追加されました。
* `p.mode&SpuriousErrors == 0`: `SpuriousErrors`(または`AllErrors`)モードが有効でない場合、つまりすべてのエラーを報告する必要がない場合にのみ、このチェックが実行されます。
* `p.errors.Len() >= 10`: 記録されているエラーの数が10個以上になった場合。
* 上記の条件が両方とも真の場合、`panic(bailout{})`が呼び出され、パーサーの実行が強制的に中断されます。このパニックは`ParseFile`関数内の`defer`ブロックで捕捉されます。
### `src/pkg/go/parser/error_test.go`の変更
```diff
--- a/src/pkg/go/parser/error_test.go
+++ b/src/pkg/go/parser/error_test.go
@@ -145,6 +145,7 @@ func checkErrors(t *testing.T, filename string, input interface{}) {\n \t\tt.Error(err)\n \t\treturn\n \t}\n+\tfound.RemoveMultiples()\n \n \t// we are expecting the following errors\n \t// (collect these after parsing a file so that it is found in the file set)\n```
この変更は、テストヘルパー関数`checkErrors`内で、エラーリストから重複を削除する`RemoveMultiples()`が、エラーのソート前に行われるように修正されたものです。これは、このコミットの主要なロジック変更(パニックによる早期終了)とは直接的な関連はありませんが、エラー処理のテストにおけるクリーンアップの一環として行われたと考えられます。
## 関連リンク
* Go Issue #3943: [https://github.com/golang/go/issues/3943](https://github.com/golang/go/issues/3943)
* Go CL 7307085: [https://golang.org/cl/7307085](https://golang.org/cl/7307085)
## 参考にした情報源リンク
* Go言語の公式ドキュメント(`go/parser`, `go/ast`, `go/token`パッケージ)
* Go言語の`panic`と`recover`に関する公式ドキュメントやチュートリアル
* Go言語のIssueトラッカー(GitHub)
* Go言語のコードレビューシステム(Gerrit)# [インデックス 15246] ファイルの概要
このコミットは、Go言語のパーサー(`go/parser`パッケージ)におけるエラー処理の挙動を変更するものです。具体的には、`ParseFile`関数が遭遇するエラーの数が10個を超えた場合に、それ以上のエラー処理を停止し、パニックを発生させるように修正されています。このパニックは`ParseFile`内で捕捉され、既存のエラーリストが返されることで、パーサーの堅牢性とパフォーマンスのバランスが改善されています。
## コミット
commit 2cd96806f4b04545b056ddaa53234fd15e821a1f Author: Michael Matloob matloob@google.com Date: Thu Feb 14 11:26:21 2013 -0800
go/parser: stop ParseFile after ten errors.
There wil be a panic if more than ten errors are encountered. ParseFile
will recover and return the ErrorList.
Fixes #3943.
R=golang-dev, gri
CC=golang-dev
https://golang.org/cl/7307085
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/2cd96806f4b04545b056ddaa53234fd15e821a1f](https://github.com/golang/go/commit/2cd96806f4b04545b056ddaa53234fd15e821a1f)
## 元コミット内容
`go/parser: stop ParseFile after ten errors.`
`There wil be a panic if more than ten errors are encountered. ParseFile will recover and return the ErrorList.`
`Fixes #3943.`
## 変更の背景
この変更は、Go言語のIssue #3943に対応するものです。元のIssueでは、`go/parser`パッケージが非常に多くの構文エラーを含むファイルをパースしようとした際に、パフォーマンスが著しく低下するという問題が報告されていました。特に、数千行にわたる単一の行に構文エラーが集中しているような極端なケースでは、パーサーが無限ループに近い状態に陥り、CPUを大量に消費し続ける可能性がありました。
このような状況は、不正な入力ファイルや悪意のあるコードが与えられた場合に、パーサーがリソースを過剰に消費し、サービス拒否(DoS)攻撃のような状態を引き起こすリスクをはらんでいました。パーサーは通常、構文エラーを検出して報告する役割を担いますが、エラーが多すぎる場合には、そのすべてのエラーを詳細に報告するよりも、早期に処理を打ち切ってリソースの枯渇を防ぐことが重要になります。
このコミットは、エラーが一定数(この場合は10個)を超えた場合に、それ以上のエラー検出と処理を停止し、リソースの消費を抑えることを目的としています。これにより、パーサーの堅牢性が向上し、不正な入力に対する耐性が高まります。
## 前提知識の解説
### Go言語の`go/parser`パッケージ
`go/parser`パッケージは、Go言語のソースコードを解析し、抽象構文木(AST: Abstract Syntax Tree)を生成するための標準ライブラリです。Goコンパイラや各種ツール(`go fmt`, `go vet`など)の基盤として利用されています。
このパッケージは、字句解析(lexing)と構文解析(parsing)の2つの主要なフェーズで動作します。字句解析器(scanner)がソースコードをトークンに分解し、パーサーがそれらのトークンから構文木を構築します。
### エラー処理と`ErrorList`
`go/parser`は、構文エラーを検出すると、それを`scanner.Error`型として記録し、複数のエラーを`scanner.ErrorList`にまとめて管理します。`ParseFile`関数は、パースが完了した後にこの`ErrorList`を返します。通常、パーサーは可能な限り多くのエラーを報告しようとしますが、エラーが非常に多い場合には、その処理自体がボトルネックとなることがあります。
### パニックとリカバリー(`panic` and `recover`)
Go言語には、プログラムの異常終了を示す`panic`と、そのパニックを捕捉してプログラムの実行を継続させる`recover`というメカニズムがあります。`panic`は、通常、回復不可能なエラーやプログラマーの想定外の状況が発生した場合に用いられます。`recover`は、`defer`関数内で呼び出され、パニックが発生したスタックフレームから制御を奪い、プログラムを正常な状態に戻すことを試みます。
このコミットでは、エラー数が閾値を超えた場合に意図的にパニックを発生させ、それを`ParseFile`の`defer`ブロックで捕捉することで、通常の制御フローでは難しい早期終了を実現しています。
### `ast`パッケージ
`ast`(Abstract Syntax Tree)パッケージは、Go言語のソースコードの抽象構文木を表現するためのデータ構造を提供します。`go/parser`パッケージがソースコードを解析して生成する結果は、この`ast`パッケージで定義された構造体(例: `ast.File`, `ast.Decl`, `ast.Expr`など)のインスタンスとして表現されます。
## 技術的詳細
このコミットの主要な変更点は、`go/parser`パッケージのエラー処理ロジックに、エラー数の上限を設けたことです。
1. **エラー数の閾値設定**: `parser.error`メソッド内で、`p.errors.Len()`(現在記録されているエラーの数)が10以上になった場合、かつ`SpuriousErrors`モード(後述)が有効でない場合に、`panic(bailout{})`が呼び出されるようになりました。
2. **`bailout`型**: 新たに`bailout`という空の構造体型が定義されています。これは、パーサーの早期終了を示すためのマーカーとして使用されます。特定の型のパニックを発生させることで、`recover`時にそのパニックがパーサーの意図した早期終了であるかどうかを識別できます。
3. **`ParseFile`でのパニック捕捉**: `ParseFile`関数は、`defer`ステートメントを使用してパニックを捕捉するようになりました。
* `defer`関数内で`recover()`が呼び出され、パニックの値が`e`に格納されます。
* `_ = e.(bailout)`という型アサーションが行われます。これは、捕捉されたパニックが`bailout`型でなければ、再度パニックを発生させる(つまり、意図しないパニックは捕捉しない)ためのガードです。
* パニックが捕捉された後、`f`(`*ast.File`)が`nil`の場合には、APIの要件を満たすために空の`ast.File`インスタンスが作成されます。
* 最後に、`p.errors.Sort()`が呼び出され、エラーリストがソートされた後、`p.errors.Err()`が返されます。これにより、パニックによって中断された場合でも、それまでに収集されたエラーが適切に返されることが保証されます。
4. **`AllErrors`モードの導入**: `Mode`型に`AllErrors`という新しいフラグが追加されました。これは`SpuriousErrors`と同じ値を持つエイリアスであり、後方互換性を保ちつつ、より明確な名前を提供します。`SpuriousErrors`または`AllErrors`モードが有効な場合、エラー数の上限チェックは行われず、すべてのエラーが報告されます。これは、デバッグ時や、すべてのエラーを網羅的に取得したい場合に利用されます。
5. **`error_test.go`の変更**: テストコードにおいて、`found.RemoveMultiples()`が`checkErrors`関数に追加されています。これは、このコミットの変更とは直接関係ありませんが、エラーリストから重複するエラーを削除する処理がテストの早い段階で行われるようになったことを示しています。
このメカニズムにより、パーサーは大量のエラーに遭遇した場合でも、リソースを無駄に消費することなく、迅速に処理を終了し、それまでに検出したエラーを報告できるようになります。
## コアとなるコードの変更箇所
### `src/pkg/go/parser/interface.go`
```diff
--- a/src/pkg/go/parser/interface.go
+++ b/src/pkg/go/parser/interface.go
@@ -52,12 +52,13 @@ func readSource(filename string, src interface{}) ([]byte, error) {
type Mode uint
const (
- PackageClauseOnly Mode = 1 << iota // parsing stops after package clause
- ImportsOnly // parsing stops after import declarations
- ParseComments // parse comments and add them to AST
- Trace // print a trace of parsed productions
- DeclarationErrors // report declaration errors
- SpuriousErrors // report all (not just the first) errors per line
+ PackageClauseOnly Mode = 1 << iota // parsing stops after package clause
+ ImportsOnly // parsing stops after import declarations
+ ParseComments // parse comments and add them to AST
+ Trace // print a trace of parsed productions
+ DeclarationErrors // report declaration errors
+ SpuriousErrors // same as AllErrors, for backward-compatibility
+ AllErrors = SpuriousErrors // report all (not just the first 10) errors per file
)
// ParseFile parses the source code of a single Go source file and returns
@@ -79,35 +80,39 @@ const (\n // representing the fragments of erroneous source code). Multiple errors
// are returned via a scanner.ErrorList which is sorted by file position.\n //
-func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (*ast.File, error) {\n+func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error) {\n // get source\n text, err := readSource(filename, src)\n if err != nil {\n return nil, err\n }\n \n-\t// parse source\n \tvar p parser\n-\tp.init(fset, filename, text, mode)\n-\tf := p.parseFile()\n-\tif f == nil {\n-\t\t// source is not a valid Go source file - satisfy\n-\t\t// ParseFile API and return a valid (but) empty\n-\t\t// *ast.File\n-\t\tf = &ast.File{\n-\t\t\tName: new(ast.Ident),\n-\t\t\tScope: ast.NewScope(nil),\n+\tdefer func() {\n+\t\tif e := recover(); e != nil {\n+\t\t\t_ = e.(bailout) // re-panics if it's not a bailout\n+\t\t}\n+\n+\t\t// set result values\n+\t\tif f == nil {\n+\t\t\t// source is not a valid Go source file - satisfy\n+\t\t\t// ParseFile API and return a valid (but) empty\n+\t\t\t// *ast.File\n+\t\t\tf = &ast.File{\n+\t\t\t\tName: new(ast.Ident),\n+\t\t\t\tScope: ast.NewScope(nil),\n+\t\t\t}\n \t\t}\n-\t}\n \n-\t// sort errors\n-\tif p.mode&SpuriousErrors == 0 {\n-\t\tp.errors.RemoveMultiples()\n-\t} else {\n \t\tp.errors.Sort()\n-\t}\n+\t\terr = p.errors.Err()\n+\t}()\n \n-\treturn f, p.errors.Err()\n+\t// parse source\n+\tp.init(fset, filename, text, mode)\n+\tf = p.parseFile()\n+\n+\treturn\n }\n \n // ParseDir calls ParseFile for the files in the directory specified by path and
src/pkg/go/parser/parser.go
--- a/src/pkg/go/parser/parser.go
+++ b/src/pkg/go/parser/parser.go
@@ -340,7 +340,13 @@ func (p *parser) next() {\n }\n }\n \n+// A bailout panic is raised to indicate early termination.\n+type bailout struct{}\n+\n func (p *parser) error(pos token.Pos, msg string) {\n+\tif p.mode&SpuriousErrors == 0 && p.errors.Len() >= 10 {\n+\t\tpanic(bailout{})\n+\t}\n \tp.errors.Add(p.file.Position(pos), msg)\n }\n \n```
### `src/pkg/go/parser/error_test.go`
```diff
--- a/src/pkg/go/parser/error_test.go
+++ b/src/pkg/go/parser/error_test.go
@@ -145,6 +145,7 @@ func checkErrors(t *testing.T, filename string, input interface{}) {\n \t\tt.Error(err)\n \t\treturn\n \t}\n+\tfound.RemoveMultiples()\n \n \t// we are expecting the following errors\n \t// (collect these after parsing a file so that it is found in the file set)\n```
## コアとなるコードの解説
### `src/pkg/go/parser/interface.go`の変更
* **`Mode`定数の追加**:
```go
+ AllErrors = SpuriousErrors // report all (not just the first 10) errors per file
```
`AllErrors`という新しい`Mode`フラグが追加されました。これは`SpuriousErrors`と同じ値を持つエイリアスであり、パーサーがすべてのエラーを報告するように指示します。このフラグが設定されている場合、後述のエラー数によるパニックは発生しません。
* **`ParseFile`関数の変更**:
```go
-func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (*ast.File, error) {
+func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error) {
```
関数のシグネチャが変更され、戻り値`f`と`err`が名前付き戻り値として宣言されました。これにより、`defer`関数内でこれらの値を直接設定できるようになります。
```go
+ defer func() {
+ if e := recover(); e != nil {
+ _ = e.(bailout) // re-panics if it's not a bailout
+ }
+
+ // set result values
+ if f == nil {
+ // source is not a valid Go source file - satisfy
+ // ParseFile API and return a valid (but) empty
+ // *ast.File
+ f = &ast.File{
+ Name: new(ast.Ident),\n+\t\t\t\tScope: ast.NewScope(nil),
+ }
+ }
+
+ p.errors.Sort()
+ err = p.errors.Err()
+ }()
```
この`defer`ブロックが、このコミットの核心部分です。
* `recover()`が呼び出され、パニックが発生した場合にその値`e`を捕捉します。
* `_ = e.(bailout)`は、捕捉されたパニックが`bailout`型であることを確認します。もし`bailout`型でなければ、それはパーサーが意図しないパニックであるため、再度パニックを発生させます。これにより、パーサーの内部的な早期終了と、予期せぬランタイムエラーによるパニックを区別しています。
* `f == nil`の場合、つまりパースが途中で中断され`ast.File`が生成されなかった場合に、APIの要件を満たすために空の`ast.File`が作成されます。
* `p.errors.Sort()`は、収集されたエラーをファイル位置に基づいてソートします。
* `err = p.errors.Err()`は、`ErrorList`からエラーオブジェクトを取得し、`ParseFile`の戻り値`err`に設定します。これにより、パニックによって中断された場合でも、それまでに検出されたエラーが呼び出し元に返されます。
```go
- // parse source
- var p parser
- p.init(fset, filename, text, mode)
- f := p.parseFile()
- if f == nil {
- // source is not a valid Go source file - satisfy
- // ParseFile API and return a valid (but) empty
- // *ast.File
- f = &ast.File{
- Name: new(ast.Ident),
- Scope: ast.NewScope(nil),
- }
- }
-
- // sort errors
- if p.mode&SpuriousErrors == 0 {
- p.errors.RemoveMultiples()
- } else {
- p.errors.Sort()
- }
-
- return f, p.errors.Err()
+ // parse source
+ p.init(fset, filename, text, mode)
+ f = p.parseFile()
+
+ return
```
元のエラーソートロジックと`f`が`nil`の場合の処理が`defer`ブロック内に移動され、`ParseFile`の本体はよりシンプルになりました。
### `src/pkg/go/parser/parser.go`の変更
* **`bailout`型の定義**:
```go
+// A bailout panic is raised to indicate early termination.
+type bailout struct{}
```
パーサーの早期終了を示すための空の構造体`bailout`が定義されました。
* **`error`メソッドの変更**:
```go
func (p *parser) error(pos token.Pos, msg string) {
+ if p.mode&SpuriousErrors == 0 && p.errors.Len() >= 10 {
+ panic(bailout{})
+ }
p.errors.Add(p.file.Position(pos), msg)
}
```
`parser`構造体の`error`メソッドは、パーサーがエラーを記録する際に呼び出されます。このメソッド内に、エラー数のチェックが追加されました。
* `p.mode&SpuriousErrors == 0`: `SpuriousErrors`(または`AllErrors`)モードが有効でない場合、つまりすべてのエラーを報告する必要がない場合にのみ、このチェックが実行されます。
* `p.errors.Len() >= 10`: 記録されているエラーの数が10個以上になった場合。
* 上記の条件が両方とも真の場合、`panic(bailout{})`が呼び出され、パーサーの実行が強制的に中断されます。このパニックは`ParseFile`関数内の`defer`ブロックで捕捉されます。
### `src/pkg/go/parser/error_test.go`の変更
```diff
--- a/src/pkg/go/parser/error_test.go
+++ b/src/pkg/go/parser/error_test.go
@@ -145,6 +145,7 @@ func checkErrors(t *testing.T, filename string, input interface{}) {\n \t\tt.Error(err)\n \t\treturn\n \t}\n+\tfound.RemoveMultiples()\n \n \t// we are expecting the following errors\n \t// (collect these after parsing a file so that it is found in the file set)\n```
この変更は、テストヘルパー関数`checkErrors`内で、エラーリストから重複を削除する`RemoveMultiples()`が、エラーのソート前に行われるように修正されたものです。これは、このコミットの主要なロジック変更(パニックによる早期終了)とは直接的な関連はありませんが、エラー処理のテストにおけるクリーンアップの一環として行われたと考えられます。
## 関連リンク
* Go Issue #3943: [https://github.com/golang/go/issues/3943](https://github.com/golang/go/issues/3943)
* Go CL 7307085: [https://golang.org/cl/7307085](https://golang.org/cl/7307085)
## 参考にした情報源リンク
* Go言語の公式ドキュメント(`go/parser`, `go/ast`, `go/token`パッケージ)
* Go言語の`panic`と`recover`に関する公式ドキュメントやチュートリアル
* Go言語のIssueトラッカー(GitHub)
* Go言語のコードレビューシステム(Gerrit)