Ruby 3.2系バグ回避コードの削除とTimeパース処理の簡素化
Railsの最低サポートRubyバージョンが3.3.1に引き上げられたことにより、ActiveModel::Type::Helpers::TimeValueに存在していたRuby 3.2系バグ向けのワークアラウンドコードが削除されました。
背景
Ruby 3.2.0には、Time.new(string, in: "UTC") が無効な Time オブジェクトを返す場合があるバグが存在していました。このバグは bugs.ruby-lang.org/issues/19292 として報告されており、Railsはこれに対応するため、ランタイムで実行環境のRubyバージョンを検査するコードを導入していました。
具体的には、Time.new(2000, 1, 1, 0, 0, 0, "-00:00").yday != 1 という式を評価し、バグの影響を受けるRubyバージョンかどうかをロード時に判定していました。影響を受けるバージョンでは Time.at(Time.new(string, in: "UTC")) というラッパーを経由することで不正なオブジェクトの生成を回避していました。
Railsの最低サポートRubyバージョンが3.3.1となった現在、このランタイムプローブが常に false を返すデッドコードとなったため、削除の条件が整いました。
技術的な変更
activemodel/lib/active_model/type/helpers/time_value.rb から、ランタイムプローブによる条件分岐とバグ回避用の fast_string_to_time 実装が削除され、コードが一本化されました。
変更前:
if Time.new(2000, 1, 1, 0, 0, 0, "-00:00").yday != 1 # Early 3.2.x had a bug
# BUG: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0
# used to return an invalid Time object
# see: https://bugs.ruby-lang.org/issues/19292
def fast_string_to_time(string)
return unless string.include?("-")
if is_utc?
::Time.at(::Time.new(string, in: "UTC"))
else
::Time.new(string)
end
rescue ArgumentError
nil
end
else
def fast_string_to_time(string)
return unless string.include?("-")
if is_utc?
::Time.new(string, in: "UTC")
else
::Time.new(string)
end
rescue ArgumentError
nil
end
end
変更後:
def fast_string_to_time(string)
return unless string.include?("-")
if is_utc?
::Time.new(string, in: "UTC")
else
::Time.new(string)
end
rescue ArgumentError
nil
end
変更量は25行削除・8行追加で、実質的なロジックの変化はなく、バグの影響を受けないRuby 3.3.1以降では常に採用されていた正常系の実装のみが残ります。
設計判断
ランタイムプローブによる動的なメソッド定義という手法が今回の削除対象の核心です。
このパターンは、def をクラスボディで直接呼び出しているのではなく、モジュールのロード時に if 式を評価してどちらの def を実行するかを決定するもので、実行時のオーバーヘッドをメソッド呼び出しごとに発生させないよう工夫されています。バグを持つRubyバージョンが現役だった時代においては、コード品質を保ちつつ互換性を維持するための合理的な手法でした。
サポートするRubyバージョンの最低ラインを引き上げることで、こうした「生きた化石」的なコードを安全に取り除けるのは、バージョンポリシー管理がもたらす直接的な恩恵です。
まとめ
この変更は機能追加ではなく、最低サポートRubyバージョンの引き上げによって初めて可能になったデッドコードの除去です。25行が削除されることで、fast_string_to_time の実装は単一の明快なパスに統一され、将来的なメンテナンスコストが低減されます。