CurrentAttributes の属性名衝突を動的に検出し例外を発生させる
ActiveSupport::CurrentAttributes が保持する内部ハッシュ @attributes は、attributes アクセサと reset が正常に機能する前提で設計されています。属性名が内部メソッドと衝突すると、reset が状態をクリアできずリクエスト間でデータが漏れるという深刻なバグが発生します。今回の PR は、そのような衝突を宣言時に検出し ArgumentError を投げることで防止します。
背景
属性名が CurrentAttributes の内部メソッドと同名になると、生成されたアクセサがそれらを上書きし、コアロジックが期待通りに動作しなくなります。例えば attribute :attributes を定義すると reset が内部ハッシュをクリアできず、リクエスト間で状態が残ります。この振る舞いは属性名衝突による意図しない副作用です。
以前は INVALID_ATTRIBUTE_NAMES という定数に手動で禁止名リストを保持していましたが、メンテナンスが追い付かず :attributes、:defaults、:attribute が抜け落ちていました。一方で削除済みの :reset_all が残っているなど、リストの正確性が失われていました。
この状態では新しい衝突リスクを防げず、テストだけで検出できない実装上の欠陥が残るため、リストの自動更新が求められました。
技術的な変更
INVALID_ATTRIBUTE_NAMES 定数をハードコードから、クラス本体の全メソッドを走査して生成する動的計算へ置き換えました。これにより、今後 CurrentAttributes が追加・削除するメソッドは自動的に禁止名リストに反映されます。
- INVALID_ATTRIBUTE_NAMES = [:set, :reset, :resets, :instance, :before_reset, :after_reset, :reset_all, :clear_all].freeze # :nodoc:
+ # Declaring an attribute by one of these names would shadow the methods
+ # CurrentAttributes itself relies on. Computed at the very end of the
+ # class body so that all the methods defined above are captured.
+ INVALID_ATTRIBUTE_NAMES = (methods(false) + private_methods(false) + instance_methods(false) + private_instance_methods(false)).uniq.sort.freeze # :nodoc:
この計算は methods(false) などの Ruby リフレクション API を使用し、false 引数で Object や Kernel から継承されたメソッドを除外します。その結果、公開・非公開・クラス・インスタンスメソッドすべてが対象となり、内部実装と属性名の衝突を網羅的に検出できます。
さらに、activesupport/test/current_attributes_test.rb に2件の回帰テストを追加し、attribute :attributes や attribute :attribute, :defaults が ArgumentError を発生させることを明示的に検証しています。
これらの変更により、禁止名リストはコードベースと常に同期し、手動更新の負荷が排除されます。既存の許容属性名への影響はなく、エラーは属性宣言時に即座に報告されます。
設計判断
禁止名リストを クラス定義の最後に動的計算 する方針を採用した背景は、手動管理の脆弱性を根本的に解消したいという点にあります。リフレクションを用いることで、将来的なメソッド追加や削除が自動的に反映され、テスト以外のメンテナンスコストが不要になります。
このアプローチは実行時に若干の計算コストが発生しますが、クラスロード時に一度だけ評価されるためパフォーマンスへの影響は無視できるレベルです。また、属性名が禁止リストに含まれる場合は ArgumentError が即座に投げられるため、開発者はエラーを早期に把握でき、既存の API 互換性は保たれます。
まとめ
今回の PR は、CurrentAttributes が内部で使用するメソッド名と属性名の衝突を、動的に生成した INVALID_ATTRIBUTE_NAMES で防止する仕組みを導入しました。手動で管理していた denylist を自動化することで、将来のコード変更に対しても安全に制限が適用され、バグ潜在リスクが大幅に低減します。