`method_missing` が誤って公開されていたバグを修正
alias_method がアクセス修飾子を無視する仕様により、Jbuilderの method_missing が意図せず公開メソッドになっていた問題を修正しました。private :method_missing を明示的に追加することで、正しい可視性が保証されます。
背景
alias_method はアクセス修飾子の影響を受けず、コピー元のメソッドの可視性を引き継ぐというRubyの仕様が、このバグの根本原因です。lib/jbuilder.rb および lib/jbuilder/jbuilder_template.rb では、alias_method :method_missing, :set! が private ディレクティブの前に記述されていましたが、仮に後に書いたとしても結果は変わりません。#set! はパブリックメソッドであるため、そのエイリアスである method_missing も自動的にパブリックになり、BasicObject から継承されたプライベートな method_missing を上書きしていました。
この問題は、Railsアプリを Jbuilder に移行する際に、あるオブジェクトが method_missing をキーとして持っていたことで偶然発見されました。本来 {"method_missing": "hello"} と出力されるべきところが、予期しない結果になっていたのです。
パブリックな method_missing が直接呼び出された場合、set!("hello") という単一引数の呼び出しになっていました。_set_value は値が BLANK のときに短絡評価で処理を打ち切るため、キーが出力に現れないという無音の失敗(silent failure)が発生していました。
技術的な変更
修正は lib/jbuilder.rb と lib/jbuilder/jbuilder_template.rb の両ファイルに同一のパターンで適用されています。alias_method の直後に private :method_missing を明示的に追加することで、エイリアスの可視性を確実にプライベートに設定します。
変更前:
private
alias_method :method_missing, :set!
変更後:
alias_method :method_missing, :set!
private :method_missing
private
private :method_missing を private ディレクティブより上に配置しているのは、「このエイリアスの可視性を直後で明示的に制御している」という意図を明確にするためです。alias_method と private :method_missing を対にして読めるよう、視覚的な近接性を持たせた配置といえます。
この修正により、json.method_missing "hello" という DSL 呼び出しは、他のすべての DSL 呼び出しと同様に set!(:method_missing, "hello") というキーと値の両引数を持つ呼び出しとして処理されるようになり、{"method_missing": "hello"} を正しく出力します。テストは test/jbuilder_test.rb と test/jbuilder_template_test.rb の両方に追加され、この動作を保証しています。
設計判断
alias_method の可視性は呼び出し元ではなく定義元のメソッドに依存するというRubyの仕様を踏まえ、private :method_missing を明示的に指定する方式が採用されました。
わずかな行数の変更で修正できる現行の方式は、set! との同一性を保ちつつ、既存の DSL 動作への影響を最小限に抑えています。
まとめ
本PRは、alias_method とアクセス修飾子の相互作用というRubyの仕様上の落とし穴を修正した変更です。private :method_missing を明示的に付与するという追加が、無音の失敗を引き起こしていた長年の潜在バグを解消し、method_missing をキーとして使う DSL 呼び出しを正しく機能させます。