ParameterFilterの正規表現最適化における行アンカーの意味的差異を修正
/^token$/ と /\Atoken\z/ を同等に扱っていた ParameterFilter のハッシュルックアップ最適化を修正し、行アンカー(^$)と文字列アンカー(\A\z)の意味的差異を正しく考慮した実装に改めました。
背景
#57166 で導入されたハッシュルックアップ最適化に、正規表現のアンカーの違いを無視するバグが含まれていました。元のPRは、/^email$/ や /\Atoken\z/ のような完全一致の正規表現フィルターを文字列として抽出し、.any? によるループではなくO(1)のハッシュ参照に置き換えるものでした。このアプローチはフィルタリング処理を最大4.5倍高速化するとされており、実用上のメリットは大きいものでした。
しかし、^ と $ はRubyの正規表現において 行アンカー(line anchor)であり、文字列全体の先頭・末尾ではなく各行の先頭・末尾にマッチします。つまり /^token$/ は "token\nfoo" というキーにもマッチしますが、文字列アンカー \A と \z を使った /\Atoken\z/ は文字列全体の先頭・末尾にのみマッチするため、"token\nfoo" にはマッチしません。この意味的差異を元の実装は無視していました。
技術的な変更
最適化を @exact_string_keys と @exact_line_keys の2つのハッシュに分割することで、両アンカーの意味的差異を保持しつつ高速なルックアップを実現しています。
変更前: 単一の @exact_keys ハッシュに、^$ と \A\z の両方のアンカーを持つ正規表現を格納していました。
# 変更前(#57166の実装)
@exact_keys = nil
# ...
elsif (literal = extract_exact_key(item))
(@exact_keys ||= {})[literal] = true
変更後: アンカーの種類に応じて2種類のハッシュに振り分けます。
@exact_string_keys = nil
@exact_line_keys = nil
# ...
elsif (literal = extract_exact_string_key(item))
(@exact_string_keys ||= {})[literal] = true
elif (literal = extract_exact_line_key(item))
(@exact_line_keys ||= {})[literal] = true
else
@regexps << item
end
@exact_string_keys(\A...\z アンカー)はキー全体との完全一致照合に使用されます。一方、@exact_line_keys(^...$ アンカー)は、まずキー全体と照合し、キーに "\n" が含まれる場合は各行を分割してそれぞれ照合します。これにより "token\nfoo" のような改行を含むキーへのマッチが正しく処理されます。
また、/\Atoken$/ や /^token\z/ のように 混合アンカー(mixed anchors)を持つ正規表現は、ハッシュで表現できない非対称な意味を持つため、最適化の対象外として従来の正規表現マッチングにフォールバックします。テストでは混合アンカーの動作も明示的に検証されており、/\Atoken$/ が "token\nfoo" にマッチし "foo\ntoken" にはマッチしないこと、/^token\z/ がその逆になることが確認されています。
設計判断
最適化の適用範囲を意味的な正確さで分類する 設計が採用されました。
^$ アンカーの正規表現をすべて @regexps に戻す(最適化を取りやめる)という選択肢も考えられますが、このPRは行ごとの照合ロジックを追加することで @exact_line_keys として最適化を継続しています。改行を含むパラメータキーは実際には非常にまれではあるものの、/^token$/ という書き方をする開発者が意図した挙動を正確に再現するという点で、仕様の正確性を優先した判断といえます。
一方、混合アンカーはハッシュによる表現が不可能なため、正規表現マッチングへのフォールバックを選んでいます。これはパフォーマンスより正確性を優先する一貫した方針です。
まとめ
本PRは、正規表現のアンカーの意味的差異(行アンカー vs 文字列アンカー)を正確にモデル化することで、#57166 の最適化の正確性を回復した修正です。@exact_string_keys と @exact_line_keys への分割という設計は、「最適化できる場合は最大限に最適化しつつ、意味的に曖昧なケースは安全側に倒す」という原則を体現しています。