HashWithIndifferentAccess が falsy なデフォルト値を保持するよう修正

rails/rails

Rails の HashWithIndifferentAccess が、Hash.new(false) のように falsy なデフォルト値を持つハッシュから生成されたときに、デフォルトが失われて nil になるバグを解消しました。これにより、意図した false のデフォルトが保持され、欠損キーの読み取り結果が期待通りになります。

背景

HashWithIndifferentAccessinitialize メソッドは、元ハッシュの default をコピーする処理を if hash.default でガードしていました。Ruby では falsenil はどちらも 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.rbinitialize 部分で、条件式を if hash.default から unless hash.default.nil? に変更しました。これにより、hash.defaultnil でない限りは必ずコピーされ、 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 行の置換とテスト追加のみで、ライブラリ全体の動作に広範な影響を与えることなく、期待通りのデフォルト保持という仕様を実現しています。

まとめ

HashWithIndifferentAccessfalse をはじめとする falsy なデフォルト値を正しくコピーできるようになり、欠損キーの読み取り結果が期待通りになると同時に、既存の nil デフォルト挙動は保持されたため、後方互換性を損なうことなくバグが解消されました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
8edcb88b

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断、まとめの5セクションが揃っており、総論→各論→結論の流れが明確です。

カスタムMarkdown構文 ⚠ WARNING

シンタックスハイライト・GitHubリンク記法の正確性

GitHubリンクは [PR #57633](URL) の形で記載されており、[#123](URL) という厳密な形式ではありません。コードブロックは正しい言語ハイライトが使用されていますが、ファイル名付きハイライトは使用していません。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的なRuby / Rails エンジニア向けの内容で、余計な基礎説明はなく適切です。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論パラグラフ、具体例・コード、まとめパラグラフで構成され、1段落1トピック、トピックセンテンスで始まり、長さも適切です。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事内の diff ブロックは PR の diff と完全に一致しており、変更行も正確に引用されています。テストコードも追加された内容と合致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

falsy、default、HashWithIndifferentAccess などの用語は PR と一致し、誤用はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

変更の理由、実装内容、後方互換性への配慮が PR 内容と合致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事の全ての主張は PR のタイトル・説明・diff で裏付けられており、捏造や推測はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR 番号(#57633)やテスト件数(96)などの数値は PR と一致しています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルは PR の要旨「falsy なデフォルト値を保持する」ことを正しく日本語化しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

Rails のバージョン情報や LTS 等、PR に記載のない外部知識は記事に含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現は使用されておらず、矛盾もありません。