Ruby 4.0の`instance_variables_to_inspect`フックを活用した`inspect`メソッドの統一
Railsの56個のクラスで個別に実装されていたinspectメソッドが、Ruby 4.0で導入されたinstance_variables_to_inspectフックを使用する統一的な実装に置き換えられました。これにより、Ruby 4.0でのネイティブ動作を維持しながら、それ以前のバージョンでも一貫したinspect出力を実現しています。
背景
Ruby 4.0では、クラスがinspect出力に表示するインスタンス変数を制御するためのinstance_variables_to_inspectフックが追加されました。このフックにより、inspectメソッド全体をオーバーライドすることなく、表示する変数を選択できます。
Rails内部では、秘密鍵を含むクラスや巨大なオブジェクトなど、様々な理由でinspect出力をカスタマイズする必要がありました。#56782の調査により、56個のクラスで個別にdef inspectが実装されていることが判明しています。これらは大きく3つのパターンに分類されます:
-
全変数を隠す(8個): 秘密鍵の漏洩防止やオブジェクトサイズの問題から、
#<ClassName:0xADDRESS>のみを表示 - 一部の変数のみ表示(44個): 重要な変数だけを選択的に表示
- カスタム表現(4個): 特殊なフォーマットでの表示
個別実装の問題点は、Ruby 4.0の新しいフックを活用できず、各クラスで同じようなコードが重複していることでした。本PRはこの状況を改善するため、統一的なバックポート機構を導入しています。
技術的な変更
ActiveSupport::InspectBackportモジュールが新規に追加され、Ruby 4.0のinstance_variables_to_inspectフックを旧バージョンでも利用可能にします。
module ActiveSupport
module InspectBackport
class << self
if RUBY_VERSION < "4"
def apply(klass)
klass.define_method(:inspect, instance_method(:inspect))
end
else
def apply(_)
# noop
end
end
end
def inspect
ivars = instance_variables_to_inspect
klass = self.class.name || self.class.inspect
addr = "0x%x" % object_id
if ivars.empty?
"#<#{klass}:#{addr}>"
else
pairs = ivars.filter_map do |ivar|
if instance_variable_defined?(ivar)
"#{ivar}=#{instance_variable_get(ivar).inspect}"
end
end
"#<#{klass}:#{addr} #{pairs.join(", ")}>"
end
end
end
end
Ruby 4.0以降ではapplyメソッドが何もせず、ネイティブのinstance_variables_to_inspectが使用されます。Ruby 4.0未満では、バックポート実装のinspectメソッドを動的に定義します。
各クラスでは、従来のdef inspectを削除し、以下のパターンで置き換えます:
変更前(ActiveSupport::MessageVerifierの例):
def inspect
"#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
end
変更後:
ActiveSup...::InspectBackport.apply(self)
private
def instance_variables_to_inspect
[].freeze
end
秘密鍵を保持するMessageVerifier、MessageEncryptor、KeyGenerator、Aes256Gcmなどのクラスでは、instance_variables_to_inspectが空配列を返すことで、変数を一切表示しません。
キャッシュストアクラスでは、特定の変数のみを表示します:
ActiveSup...::InspectBackport.apply(self)
private
def instance_variables_to_inspect
[:@cache_path, :@options].freeze
end
FileStoreは@cache_pathと@optionsを、NullStoreとRedisCacheStoreは設定に関連する変数のみを表示するようになりました。
設計判断
Ruby 4.0の新機能を先取りしつつ、後方互換性を維持するアプローチが採用されました。
InspectBackport.applyの条件分岐により、Ruby 4.0以降では完全にネイティブ実装に委譲し、追加のオーバーヘッドがありません。Ruby 4.0未満でのみバックポート実装が使用されるため、将来的なRubyバージョンアップ時の移行コストがゼロになります。
instance_variables_to_inspectが返す配列をfreezeすることで、意図しない変更を防止しています。また、filter_mapを使用して未定義の変数を自動的にスキップする実装により、条件付きで変数を表示するケースにも対応しています。
テストでは、各クラスのinspect出力が期待通りの形式(#<ClassName:0xADDRESS>または#<ClassName:0xADDRESS @var=value>)であることを正規表現で検証しています。これにより、Rubyバージョン間でのアドレス表記の違いやハッシュ表現の違いを吸収しています。
まとめ
本PRは、Rails全体で56個のクラスに散在していたinspect実装を、Ruby 4.0の標準機能を活用する統一的な方式に移行しました。ActiveSupport::InspectBackportモジュールにより、Ruby 4.0未満でも同じインターフェースが使用でき、将来的なRubyバージョンアップ時には自動的にネイティブ実装に切り替わる設計です。秘密鍵の保護やデバッグ情報の制御といった既存のinspectカスタマイズの意図を保ちながら、コードの重複を排除し、保守性を向上させています。