InheritableOptions の比較演算子が非 Hash 値で NoMethodError を起こすバグを修正
ActiveSupport::InheritableOptions#== がハッシュでないオブジェクトと比較した際に NoMethodError を発生させていた問題を解消し、== が常に真偽値を返すようにしました。
背景
InheritableOptions#== の実装が to_h == other.to_h に固定されていた ため、String や Integer など #to_h を持たないオブジェクトと比較すると例外がスローされました。この実装は 81d0a29 コミットで導入されたもので、親オプションのマージ結果をハッシュ比較で評価する意図でしたが、非ハッシュ引数を想定していませんでした。
例外はArray#include?やArray#uniq、case式など、#== を内部で呼び出す多数のコア機能で顕在化 します。たとえば [options].include?("x") が NoMethodError になるケースが報告され、開発者が意図しないクラッシュに直面していました。
さらに nil との比較で true が返っていた ことも問題でした。nil.to_h が空ハッシュを返すため、空の InheritableOptions が nil と等価と判定され、本来期待すべき false ではなく true になる不整合が生じていました。
技術的な変更
InheritableOptions#== の実装を other.is_a?(Hash) && to_h == other.to_h に変更し、ハッシュ以外のオブジェクトに対しては即座に false を返すようにしました。この guard は Hash#== の振る舞いを忠実に再現し、nil など #to_h を持つがハッシュではないオブジェクトとの比較で誤った true を防ぎます。
@@ -105,7 +105,7 @@ def to_h
end
def ==(other)
- to_h == other.to_h
+ other.is_a?(Hash) && to_h == other.to_h
end
def inspect
テストスイートにも test_inheritable_options_equality と test_inheritable_options_equality_with_non_hash_returns_false を追加し、ハッシュ比較と非ハッシュ比較の両方で期待通りの結果が得られることを確認しました。これにより ordered_options_test.rb 全体が 30 件のテストで 0 失敗となり、修正の正当性が自動的に保証されます。
設計判断
is_a?(Hash) を用いた型チェックは respond_to?(:to_h) よりも限定的 です。nil は #to_h を実装しますがハッシュではないため、respond_to? 判定だけでは空 InheritableOptions が nil と等価になる問題が残ります。is_a?(Hash) に絞ることで、Hash、OrderedOptions、InheritableOptions(マージ結果はハッシュ)との比較は許容しつつ、その他すべての型に対しては false を返す明確な境界を設定しました。
この変更は 後方互換性を維持 しながら、OrderedOptions が継承する Hash#== と同一の契約を InheritableOptions にも適用するという設計方針を示しています。既存コードが options == other_options の形で使用されていても、非ハッシュ引数に対して例外が発生しなくなるため、実運用上の安全性が大幅に向上します。
まとめ
InheritableOptions#== が非ハッシュオブジェクトとの比較で例外を投げていたバグを、ハッシュ型チェックに置き換えることで修正しました。これにより Array#include? などの標準的なコレクション操作が安全に動作し、nil との不適切な等価判定も解消されます。テスト追加により変更の正当性が検証され、Rails の設定系オブジェクトの比較契約が一貫しました。