HashWithIndifferentAccess が falsy なデフォルト値を保持するよう修正
Rails の HashWithIndifferentAccess が、Hash.new(false) のように falsy なデフォルト値を持つハッシュから生成されたときに、デフォルトが失われて nil になるバグを解消しました。これにより、意図した false のデフォルトが保持され、欠損キーの読み取り結果が期待通りになります。
背景
HashWithIndifferentAccess の initialize メソッドは、元ハッシュの default をコピーする処理を if hash.default でガードしていました。Ruby では false と nil はどちらも falsy であるため、元ハッシュが Hash.new(false) のように false をデフォルトに設定していても条件が偽となり、デフォルトがコピーされませんでした。
その結果、以下のように false が期待される場面で nil が返り、"unknown keys are off" パターンのフラグマップが正しく機能しなくなっていました。
source = Hash.new(false)
source["a"] = 1
hwia = ActiveSupport::HashWithIndifferentAccess.new(source)
hwia.default # => nil (期待: false)
hwia["missing"] # => nil (期待: false)
この挙動は、デフォルト値が意味を持つハッシュを HashWithIndifferentAccess に変換するユースケースで潜在的なバグとなっていました。
技術的な変更
該当コードは activesupport/lib/active_support/hash_with_indifferent_access.rb の initialize 部分で、条件式を if hash.default から unless hash.default.nil? に変更しました。これにより、hash.default が nil でない限りは必ずコピーされ、 false のような falsy でも保持されます。
@@ -75,7 +75,7 @@ def initialize(constructor = nil)
update(constructor)
hash = constructor.is_a?(Hash) ? constructor : constructor.to_hash
- self.default = hash.default if hash.default
+ self.default = hash.default unless hash.default.nil?
self.default_proc = hash.default_proc if hash.default_proc
else
super(constructor)
同時に activesupport/test/hash_with_indifferent_access_test.rb にテスト test_indifferent_preserves_falsy_default_from_source_hash を追加し、 Hash.new(false) から生成したインスタンスが default と欠損キーで false を返すことを検証しています。テストは既存の HWIA スイート全体(96 件)を通過し、回帰が防止されています。
def test_indifferent_preserves_falsy_default_from_source_hash
source = Hash.new(false)
source["a"] = 1
hash = HashWithIndifferentAccess.new(source)
assert_equal false, hash.default
assert_equal false, hash[:missing]
assert_equal 1, hash[:a]
end
設計判断
今回の修正は、既存コードへの影響を最小限に抑えることを優先し、条件式を unless hash.default.nil? に変更するシンプルなアプローチを採用しました。 nil がハッシュのデフォルトとして最も一般的であるため、この変更は「デフォルトが nil のケースでは何も変わらず、 nil 以外の値はそのままコピー」になるという後方互換性を保ちます。
また、新たに追加されたテストにより、今回のバグ修正が将来的に再度回帰しないことが保証されます。コード変更は 1 行の置換とテスト追加のみで、ライブラリ全体の動作に広範な影響を与えることなく、期待通りのデフォルト保持という仕様を実現しています。
まとめ
HashWithIndifferentAccess が false をはじめとする falsy なデフォルト値を正しくコピーできるようになり、欠損キーの読み取り結果が期待通りになると同時に、既存の nil デフォルト挙動は保持されたため、後方互換性を損なうことなくバグが解消されました。