[インデックス 14611] ファイルの概要
このコミットは、Go言語の標準ライブラリであるregexp
パッケージ内のテストファイルsrc/pkg/regexp/all_test.go
において、正規表現のコンパイルエラーを検証するTestBadCompile
テストを再有効化するものです。以前のregexp
パッケージのコードがコメントアウトされていた部分を、新しいregexp
パッケージの挙動に合わせて修正し、テストを再び実行可能にしています。
コミット
- コミットハッシュ:
3c6c88319eeb4b6fe0b599d894bbb0d8f50a116f
- Author: Russ Cox rsc@golang.org
- Date: Tue Dec 11 12:19:39 2012 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/3c6c88319eeb4b6fe0b599d894bbb0d8f50a116f
元コミット内容
regexp: re-enable TestBadCompile
The code that was commented out was for the old regexp package.
In the new one the errors and the space of valid regexps are different.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/6873063
変更の背景
このコミットの背景には、Go言語のregexp
パッケージが大幅に刷新されたという重要な経緯があります。初期のGo言語には独自の正規表現エンジンが搭載されていましたが、パフォーマンスや機能の面で課題がありました。そのため、Googleが開発した高性能な正規表現エンジンであるRE2ライブラリをベースにした新しいregexp
パッケージが導入されました。
この移行に伴い、正規表現の構文解析におけるエラーの種類や、有効な正規表現として認識される範囲が変更されました。古いregexp
パッケージ用に書かれていたTestBadCompile
テストは、これらの変更に対応していなかったため、一時的にコメントアウトされていました。
本コミットは、新しいregexp
パッケージの挙動に合わせてTestBadCompile
テストを更新し、再有効化することを目的としています。これにより、正規表現の不正なパターンに対するエラーハンドリングが正しく機能していることを保証し、パッケージの堅牢性を高めます。
前提知識の解説
正規表現 (Regular Expression)
正規表現は、文字列のパターンを記述するための強力なツールです。特定の文字の並び、繰り返し、選択、グループ化などを表現でき、テキスト検索、置換、検証などに広く利用されます。
正規表現エンジン
正規表現エンジンは、正規表現パターンを解釈し、入力文字列とのマッチングを行うソフトウェアコンポーネントです。正規表現エンジンには大きく分けて2つの主要なタイプがあります。
-
バックトラッキングエンジン (Backtracking Engine):
- Perl、Python、Java、Rubyなどの多くの言語で採用されています。
- 正規表現のパターンを左から右に評価し、マッチングが失敗した場合に以前の決定点に戻って(バックトラックして)別のパスを試みます。
- 非常に柔軟で強力な表現力を持つ反面、特定のパターン(例:
(a+)+b
)と入力文字列の組み合わせによっては、指数関数的な時間計算量(「破滅的バックトラッキング」)が発生し、パフォーマンスが著しく低下する可能性があります。
-
DFAベースのエンジン (Deterministic Finite Automaton-based Engine):
- RE2(Goの
regexp
パッケージが採用)、awk、grepなどが採用しています。 - 入力文字列を一度だけスキャンし、常に線形時間(入力文字列の長さに比例する時間)でマッチングを完了することを保証します。
- バックトラッキングエンジンに比べて表現力に一部制限がある場合があります(例: 後方参照(backreferences)をサポートしないことが多い)が、予測可能な高性能を提供します。
- RE2(Goの
Go言語のregexp
パッケージの進化
Go言語のregexp
パッケージは、当初は独自の正規表現エンジンを持っていましたが、後にGoogleが開発したRE2ライブラリをベースにした実装に置き換えられました。この変更は、Goのregexp
パッケージが常に線形時間で動作することを保証し、破滅的バックトラッキングの問題を回避することを目的としていました。
RE2は、正規表現を非決定性有限オートマトン(NFA)に変換し、それを決定性有限オートマトン(DFA)に変換してマッチングを行うことで、この線形時間保証を実現しています。この設計思想の違いにより、RE2ベースのregexp
パッケージは、古い実装とは異なるエラーを報告したり、一部の正規表現の解釈が異なる場合があります。
技術的詳細
このコミットの技術的詳細は、主にGoのregexp
パッケージの内部的な変更、特に正規表現のパースとコンパイルにおけるエラーハンドリングの変更に焦点を当てています。
古いregexp
パッケージでは、正規表現の構文エラーはerror
型の具体的な変数(例: ErrBareClosure
, ErrUnmatchedLpar
など)として定義されていました。これらのエラーは、regexp
パッケージの内部で定義された特定の条件に合致した場合に返されていました。
新しいRE2ベースのregexp
パッケージでは、エラーの報告方法がより汎用的になりました。具体的には、エラーは文字列として返され、その文字列がエラーの内容を詳細に記述するようになりました。これにより、より多様なエラーケースに対応できるようになり、またエラーメッセージがよりユーザーフレンドリーになりました。
この変更に伴い、TestBadCompile
テスト内のbad_re
スライス(不正な正規表現パターンとその期待されるエラーを定義する)の構造が変更されました。
- 古い定義:
type stringError struct { re string; err error }
err
フィールドはerror
インターフェース型で、具体的なエラー変数と比較していました。
- 新しい定義:
type stringError struct { re string; err string }
err
フィールドはstring
型になり、期待されるエラーメッセージの文字列と比較するようになりました。
また、compileTest
ヘルパー関数も、エラーの比較ロジックが変更されました。
- 古い
compileTest
:if err != error { ... }
- 返された
error
オブジェクトが期待されるerror
オブジェクトと同一であるかを直接比較していました。
- 返された
- 新しい
compileTest
:if error == "" && err != nil
: エラーが期待されていないのにエラーが返された場合。if error != "" && err == nil
: エラーが期待されているのにエラーが返されなかった場合。else if error != "" && !strings.Contains(err.Error(), error)
: エラーが期待されており、エラーが返されたが、そのエラーメッセージが期待される文字列を含んでいない場合。
この変更により、テストは新しいregexp
パッケージが返す文字列ベースのエラーメッセージを正確に検証できるようになりました。特に、strings.Contains
を使用することで、エラーメッセージが完全に一致する必要はなく、特定のキーワードやフレーズが含まれていれば良いという柔軟性を持たせています。これは、RE2エンジンが返すエラーメッセージが、Goの内部エラーコードよりも詳細で可変性があるためです。
コアとなるコードの変更箇所
変更はsrc/pkg/regexp/all_test.go
ファイルに集中しています。
--- a/src/pkg/regexp/all_test.go
+++ b/src/pkg/regexp/all_test.go
@@ -30,53 +30,52 @@ var good_re = []string{
`\!\\`,
}
-/*
type stringError struct {
re string
- err error
+ err string
}
var bad_re = []stringError{
- {`*`, ErrBareClosure},
- {`+`, ErrBareClosure},
- {`?`, ErrBareClosure},
- {`(abc`, ErrUnmatchedLpar},
- {`abc)`, ErrUnmatchedRpar},
- {`x[a-z`, ErrUnmatchedLbkt},
- {`abc]`, ErrUnmatchedRbkt},
- {`[z-a]`, ErrBadRange},
- {`abc\`, ErrExtraneousBackslash},
- {`a**`, ErrBadClosure},
- {`a*+`, ErrBadClosure},
- {`a??`, ErrBadClosure},
- {`\x`, ErrBadBackslash},
-}
-*/
-
-func compileTest(t *testing.T, expr string, error error) *Regexp {
+ {`*`, "missing argument to repetition operator: `*`"},
+ {`+`, "missing argument to repetition operator: `+`"},
+ {`?`, "missing argument to repetition operator: `?`"},
+ {`(abc`, "missing closing ): `(abc`"},
+ {`abc)`, "unexpected ): `abc)`"},
+ {`x[a-z`, "missing closing ]: `[a-z`"},
+ {`[z-a]`, "invalid character class range: `z-a`"},
+ {`abc\`, "trailing backslash at end of expression"},
+ {`a**`, "invalid nested repetition operator: `**`"},
+ {`a*+`, "invalid nested repetition operator: `*+`"},
+ {`\x`, "invalid escape sequence: `\\x`"},
+}
+
+func compileTest(t *testing.T, expr string, error string) *Regexp {
re, err := Compile(expr)
- if err != error {
+ if error == "" && err != nil {
t.Error("compiling `", expr, "`; unexpected error: ", err.Error())
}
+ if error != "" && err == nil {
+ t.Error("compiling `", expr, "`; missing error")
+ } else if error != "" && !strings.Contains(err.Error(), error) {
+ t.Error("compiling `", expr, "`; wrong error: ", err.Error(), "; want ", error)
+ }
return re
}
func TestGoodCompile(t *testing.T) {
for i := 0; i < len(good_re); i++ {
- compileTest(t, good_re[i], nil)
+ compileTest(t, good_re[i], "")
}
}
-/*
func TestBadCompile(t *testing.T) {
for i := 0; i < len(bad_re); i++ {
compileTest(t, bad_re[i].re, bad_re[i].err)
}
}
-*/
func matchTest(t *testing.T, test *FindTest) {
- re := compileTest(t, test.pat, nil)
+ re := compileTest(t, test.pat, "")
if re == nil {
return
}
主な変更点は以下の通りです。
- コメントアウトの解除:
bad_re
スライスの定義とTestBadCompile
関数の全体を囲んでいた/* ... */
コメントが削除され、コードが有効化されました。 stringError
構造体の変更:err
フィールドの型がerror
からstring
に変更されました。type stringError struct { re string err string // 変更点: error型からstring型へ }
bad_re
スライスの内容変更: 各要素のerr
フィールドが、具体的なerror
変数から、期待されるエラーメッセージの文字列リテラルに置き換えられました。- 例:
ErrBareClosure
->"missing argument to repetition operator:
*"
- 例:
compileTest
関数のシグネチャ変更:error
引数の型がerror
からstring
に変更されました。func compileTest(t *testing.T, expr string, error string) *Regexp { // 変更点: error型からstring型へ
compileTest
関数のエラーチェックロジック変更:if err != error
という直接比較の代わりに、以下の3つの条件でエラーをチェックするようになりました。if error == "" && err != nil
: エラーが期待されていないのにエラーが返された場合。if error != "" && err == nil
: エラーが期待されているのにエラーが返されなかった場合。else if error != "" && !strings.Contains(err.Error(), error)
: エラーが期待されており、エラーが返されたが、そのエラーメッセージが期待される文字列を含んでいない場合。
TestGoodCompile
とmatchTest
の呼び出し変更:compileTest
の第3引数にnil
を渡していた箇所が、空文字列""
に置き換えられました。これは、エラーが期待されないことを示す新しいcompileTest
のシグネチャに合わせたものです。
コアとなるコードの解説
このコミットの核心は、Goのregexp
パッケージが内部的に正規表現のパースエラーをどのように報告するかという変更に適応することです。
-
stringError
構造体とbad_re
スライスの変更: 古いregexp
パッケージは、特定の構文エラーに対して、ErrBareClosure
のような事前定義されたerror
変数を返していました。しかし、新しいRE2ベースのregexp
パッケージは、より詳細で動的なエラーメッセージを文字列として生成します。例えば、*
のような繰り返し演算子が引数を欠いている場合、以前はErrBareClosure
という一般的なエラーでしたが、新しいパッケージでは"missing argument to repetition operator:
*"
という具体的な文字列を返します。この変更に対応するため、bad_re
スライスは、期待されるエラーを具体的なerror
オブジェクトではなく、そのエラーメッセージの文字列で定義するように更新されました。 -
compileTest
関数のエラーチェックロジック:compileTest
関数は、正規表現のコンパイルを試み、その結果が期待通りであるかを検証するヘルパー関数です。if error == "" && err != nil
: これは、good_re
(有効な正規表現)のテストケースで使われます。error
引数が空文字列の場合、エラーは期待されていません。もしCompile
関数がnil
でないエラーを返した場合、それは予期せぬエラーであり、テストは失敗します。if error != "" && err == nil
: これは、bad_re
(不正な正規表現)のテストケースで使われます。error
引数が空文字列でない場合、特定のエラーが期待されています。もしCompile
関数がエラーを返さなかった(nil
を返した)場合、それは期待されるエラーが見つからなかったことを意味し、テストは失敗します。else if error != "" && !strings.Contains(err.Error(), error)
: これもbad_re
のテストケースで使われます。エラーが期待されており、実際にエラーが返された場合、そのエラーメッセージ(err.Error()
)が、error
引数で指定された文字列を含んでいるかをstrings.Contains
でチェックします。これにより、RE2エンジンが返す可能性のある、より詳細なエラーメッセージ(例: 行番号や列番号を含む場合など)にも対応できるようになります。完全に一致を求めるのではなく、部分文字列の一致で十分とすることで、テストの柔軟性と堅牢性を高めています。
これらの変更により、TestBadCompile
は新しいregexp
パッケージの挙動に完全に適合し、正規表現のコンパイルエラーが正しく報告されることを保証するようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/3c6c88319eeb4b6fe0b599d894bbb0d8f50a116f
- Go Code Review (CL): https://golang.org/cl/6873063
参考にした情報源リンク
- The Go
regexp
package (Go Documentation): https://go.dev/pkg/regexp/ - Regular Expression Matching in Go (Go Blog): https://go.dev/blog/regexp (RE2の導入に関する公式ブログ記事)
- RE2 (Google's Regular Expression Engine): https://github.com/google/re2
- Stack Overflow discussion on old vs new Go regex: https://stackoverflow.com/questions/10617955/what-is-the-difference-between-the-old-and-new-regexp-packages-in-go
- GitHub issue on regexp performance (illustrates ongoing improvements): https://github.com/golang/go/issues/22060
- Medium article on Go regex compilation: https://medium.com/@joshua.s.a.g/go-regexp-compilation-and-performance-101-2020-a72121212121