Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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つの主要なタイプがあります。

  1. バックトラッキングエンジン (Backtracking Engine):

    • Perl、Python、Java、Rubyなどの多くの言語で採用されています。
    • 正規表現のパターンを左から右に評価し、マッチングが失敗した場合に以前の決定点に戻って(バックトラックして)別のパスを試みます。
    • 非常に柔軟で強力な表現力を持つ反面、特定のパターン(例: (a+)+b)と入力文字列の組み合わせによっては、指数関数的な時間計算量(「破滅的バックトラッキング」)が発生し、パフォーマンスが著しく低下する可能性があります。
  2. DFAベースのエンジン (Deterministic Finite Automaton-based Engine):

    • RE2(Goのregexpパッケージが採用)、awk、grepなどが採用しています。
    • 入力文字列を一度だけスキャンし、常に線形時間(入力文字列の長さに比例する時間)でマッチングを完了することを保証します。
    • バックトラッキングエンジンに比べて表現力に一部制限がある場合があります(例: 後方参照(backreferences)をサポートしないことが多い)が、予測可能な高性能を提供します。

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
 	}

主な変更点は以下の通りです。

  1. コメントアウトの解除: bad_reスライスの定義とTestBadCompile関数の全体を囲んでいた/* ... */コメントが削除され、コードが有効化されました。
  2. stringError構造体の変更: errフィールドの型がerrorからstringに変更されました。
    type stringError struct {
    	re  string
    	err string // 変更点: error型からstring型へ
    }
    
  3. bad_reスライスの内容変更: 各要素のerrフィールドが、具体的なerror変数から、期待されるエラーメッセージの文字列リテラルに置き換えられました。
    • 例: ErrBareClosure -> "missing argument to repetition operator: *"
  4. compileTest関数のシグネチャ変更: error引数の型がerrorからstringに変更されました。
    func compileTest(t *testing.T, expr string, error string) *Regexp { // 変更点: error型からstring型へ
    
  5. compileTest関数のエラーチェックロジック変更:
    • if err != errorという直接比較の代わりに、以下の3つの条件でエラーをチェックするようになりました。
      • if error == "" && err != nil: エラーが期待されていないのにエラーが返された場合。
      • if error != "" && err == nil: エラーが期待されているのにエラーが返されなかった場合。
      • else if error != "" && !strings.Contains(err.Error(), error): エラーが期待されており、エラーが返されたが、そのエラーメッセージが期待される文字列を含んでいない場合。
  6. TestGoodCompilematchTestの呼び出し変更: 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パッケージの挙動に完全に適合し、正規表現のコンパイルエラーが正しく報告されることを保証するようになりました。

関連リンク

参考にした情報源リンク