テンプレート検索を例外が出ない非例外系メソッドへ分割

rails/rails

非例外系の #find を導入し、find_all(...).first のパターンを排除しました。これにより、テンプレート探索時に例外を捕捉する必要がなくなり、将来的な高速検索実装への足掛かりができます。

背景

従来、ActionView のテンプレート探索は lookup_context.find_all(...).first に依存しており、結果が無い場合は nil が返り、呼び出し側で rescue して例外処理を行うケースが散在していました。find_all は全候補を取得してから最初の要素を選ぶため、不要なソートや配列生成が発生し、パフォーマンスに余計なコストが掛かっていました。この PR は 例外を投げない検索 (#find) と、例外を必ず投げる検索 (#find!) を明確に分離することで、コードベースをシンプルにしつつ将来の最適化を容易にする狙いです。

技術的な変更

LookupContext に非例外系 find と例外系 find! を追加

lookup_context.rb では既存の find非例外系 として残し、新たに find! を実装しました。find! は内部で normalize_namedetail_args_for を呼び出し、@view_paths.find! に委譲し、見つからない場合は MissingTemplate を即座に送出します。これにより、呼び出し側は成功時はテンプレートオブジェクトを受け取り、失敗時は例外で即座に制御フローが抜けます。

@@
-      def find(name, prefixes = [], partial = false, keys = [], options = {})
+      def find(name, prefixes = [], partial = false, keys = [], options = {})
@@
-      alias :find_template :find
+      # non‑raising lookup used by most rendering paths
+      alias :find_template :find
+
+      def find!(name, prefixes = [], partial = false, keys = [], options = {})
+        name, prefixes = normalize_name(name, prefixes)
+        details, details_key = detail_args_for(options)
+        @view_paths.find!(name, prefixes, partial, details, details_key, keys)
+      end

PathSetfind! を実装し、エラーメッセージを統一

path_set.rb では find! メソッドを追加し、find の結果が nil の場合に MissingTemplate を発生させます。エラーメッセージは従来と同等の情報量を保持し、テストでも同様の出力が検証されています。

@@
-      def find(path, prefixes, partial, details, details_key, locals)
-        find_all(path, prefixes, partial, details, details_key, locals).first ||
-          raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
+      def find(path, prefixes, partial, details, details_key, locals)
+        find_all(path, prefixes, partial, details, details_key, locals).first
       end
+
+      def find!(path, prefixes, partial, details, details_key, locals)
+        find(path, prefixes, partial, details, details_key, locals) ||
+          raise(MissingTemplate.new(self, path, prefixes, partial, details, details_key, locals))
+      end

呼び出し側のコードを find / find! に置換

  • etag_with_template_digest.rbpick_template_for_etaglookup_context.find に変更し、例外が不要なケースで非例外系検索を利用。
  • digestor.rbfind_templatefinder.find に置き換え、シンプル化。
  • layouts.rb のデフォルトレイアウト検索ロジックは lookup_context.find に統一。
  • partial_renderer.rbtemplate_renderer.rb では、必ずテンプレートが存在すべき場面で find! を使用し、失敗時は即座に例外を上位へ伝搬させます。
@@
-            @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
+            @lookup_context.find!(options[:template], options[:prefixes], false, keys, @details)
@@
-          @lookup_context.find_template(layout, nil, false, keys, details)
+          @lookup_context.find!(layout, nil, false, keys, @details.merge(formats: formats))

新しいフォーマット判定メソッド any_formats?

LookupContextany_formats? が追加され、指定フォーマットが一つでも存在すれば true を返すようになりました。template_renderer.rb のレイアウト解決ロジックで、フォーマットが見つからないケースを明示的に判定できるようになり、冗長な例外処理が削減されています。

@@
-      def any?(name, prefixes = [], partial = false, keys = [])
+      def any?(name, prefixes = [], partial = false, keys = [])
         exists?(name, prefixes, partial, keys)
       end
       alias :any_templates? :any?
+
+      def any_formats?(name, prefixes = [], partial = false, keys = [], options = {})
+        exists?(name, prefixes, partial, keys, **options, formats: default_formats)
+      end

テストの更新

lookup_context_test.rb では find! を用いた例外メッセージ検証に変更し、期待されるエラーテキストが変わらないことを確認しています。また、PathSetfind! に対するテストも追加され、例外発生時のメッセージが正確であることが保証されています。

設計判断

例外系と非例外系の明示的分離

findfind! の二分割は、検索結果の有無に応じた制御フローを呼び出し側で明示的に選択できるようにする設計です。従来は find_all(...).first の結果が nil になるか、MissingTemplate がスローされるかで判定していましたが、ロジックが散在しやすく保守性が低下していました。この分割により、成功時は静かにオブジェクトを取得し、失敗時は即座に例外で通知という責務が明確化され、コードベース全体で一貫したエラーハンドリングが可能になります。

後方互換性の維持

find のシグネチャは変更せず、既存の呼び出しは全て非例外系へ流れるようリファクタリングしました。find! は新規メソッドとして追加されたため、既存コードが影響を受けることはありません。テストスイートの全パスが通過していることから、既存機能の動作は保持されていることが確認できます。

将来の最適化への土台

非例外系 find「ベストマッチ」だけを返す実装に置き換えられる余地が生まれました。現在は内部で find_all の結果の先頭要素を取得していますが、将来的にインデックス検索やキャッシュ戦略を導入すれば、余計なソートや配列生成を回避できるようになります。今回の分割は、そのような最適化を導入しやすくするための設計的な布石となります。

まとめ

この PR はテンプレート検索ロジックを 例外系 (find!) と非例外系 (find) に分割し、呼び出し側が意図に沿った検索手段を選択できるようにしました。any_formats? の追加でフォーマット判定が簡潔になり、テストも更新されたことで既存の挙動は保持されています。結果としてコードの可読性と保守性が向上し、将来的な検索最適化への道が開かれました。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
442d167f

この記事は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リンク記法の正確性

コードブロックのファイル名付きシンタックスハイライトは正しく使用されていますが、PRリンクが要求された `[ #123 ](URL)` 形式ではなく `PR #57783` と記載されているため警告としました。

対象読者への適合性 ✓ PASS

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

Rails の内部構成に関する専門的な記述が中心で、対象読者であるエンジニアに適したレベルです。

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

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

各セクションは総論・各論・結論の段落構成が取られ、段落はトピックセンテンスで始まり、長さも適切です。

Diff内容との照合 ✓ PASS

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

記事内のコードブロックは提供された Diff と一致しており、追加・削除箇所・ファイル名すべてが正しく反映されています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

用語(LookupContext、MissingTemplate、any_formats? など)は正しく使用され、誤用は見られません。

説明の技術的正確性 ✓ PASS

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

技術的説明は PR の目的と Diff の変更点に基づいており、根拠のない主張はありません。

事実の突合 ✓ PASS

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

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

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

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

PR 番号 #57783 など数値は正確です。

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

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

記事タイトルは PR の「Split template lookup into raising and non-raising」の意味を日本語で正確に表現しています。

外部知識の正確性 ✓ PASS

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

LTS やリリース日程等、PR に記載されていない外部情報は含まれていません。

時間表現の正確性 ✓ PASS

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

時間表現の記載はなく、矛盾もありません。