Upgrade to Pro — share decks privately, control downloads, hide ads and more …

PrismでRailsアプリケーションの呼び出しツリーを出すgem “call_map” を作...

Avatar for Yo Yo
July 13, 2026
53

PrismでRailsアプリケーションの呼び出しツリーを出すgem “call_map” を作ってみた

Avatar for Yo

Yo

July 13, 2026

Transcript

  1. 処理の流れを追うのが少しつらい audit_log を探しているころには…… 「あとで読む」に積んだ authorize_order! と validate_deletable! は忘れている 😇 #

    orders_controller.rb def destroy @order = Order.find(params[:id]) authorize_order! # ← あとで読む(1 件目) OrderDeleteService.execute(@order) # ← 潜る redirect_to orders_path end # 別ファイル: order_delete_service.rb def execute validate_deletable! # ← あとで読む(2 件目) destroy_order! # ← 潜る(2 段目) notify_deleted end # さらに別ファイル: concerns/order_deletion.rb def destroy_order! @order.destroy! audit_log(:order_destroyed) # ← これはどこで定義…? さらに掘る end ファイルを移るたびに小さなコンテキストスイッチが起きて、 頭の中の浅い「あとで読む」スタックから、少しずつこぼれ落ちていく
  2. 出力はこんな感じ OrdersController#destroy ├─ before_action authenticate_user! ├─ before_action set_order │ └─

    current_user.orders.find [framework] ├─ authorize_order! │ └─ OrderPolicy#destroy? ├─ OrderDeleteService.execute │ └─ OrderDeleteService#execute │ ├─ validate_deletable! │ │ └─ OrderDeletionPolicy#validate! │ ├─ destroy_order! │ │ └─ @order.destroy! [framework] │ └─ notify_deleted │ └─ OrderDeletedNotifier.call └─ redirect_to [framework]
  3. 仕組みはざっくり2 つ: 「索引を作る」と「掘る」 見つけた先も掘る $ call_map OrdersController#destroy ① 索引を作る app/**/*.rb

    を全部parse し て クラス・メソッド → 場所 を記録 ② 起点から掘る 本文の呼び出しを索引で解 決 ③ ツリーを出力 やっていることは「地図づくり」と「地図を使って探索」
  4. つらみその1: その User.all は、どっちの User ? ところが、このアプリにはモデルの User が2 つある:

    どっちに潜る? 間違えたら、嘘のツリーを出してしまう 管理画面のアクションを起点に実行して、 User.all の定義をアプリケーションコードから探したい: $ bundle exec call_map Admin::UsersController#index # 管理画面のコントローラ (Admin::UsersController) def index @users = User.all # ← この User の定義に潜りたい end # app/models/user.rb class User < ApplicationRecord # ① 一般ユーザー end # app/models/admin/user.rb module Admin class User < ApplicationRecord # ② 管理者 end end
  5. 答え: コントローラの「書き方」で変わる 囲んでいる Admin の中を先に探すので、 中の User は ②( 管理者)

    に解決される コード上に module Admin のブロックが無いので、 Admin の中は探索されない → ①( 一般ユーザー) に解決される Ruby は定数を、ソース上で囲んでいるブロックを内から外へ探す。 つまり解決先は、呼び出しの中身ではなく周りの書き方で決まる コントローラは、どちらも同じ Admin::UsersController 。でも、書き方が2 通りある: # module Admin のブロックで書かれていたら module Admin class UsersController def index @users = User.all # → ② Admin::User end end end # 「1 行で」書かれていたら class Admin::UsersController def index @users = User.all # → ① 一般ユーザー! end end
  6. call_map は「囲まれ方」ごと索引に登録する 呼び出しに出会ったら、この記録を内側から外側へ辿って候補を探す = Ruby の定数解決と同じ手順を、索引の上で再現する namespace の書き方がどちらでも、Ruby と同じ解決結果を出せるようにしたい。 でも最初の実装はクラス名(

    文字列1 個) だけの記録で、2 つの書き方を区別できなかった。作り直し: Definition( owner: "Admin::UsersController", # クラス名は同じでも…… lexical_nesting: ["Admin", "Admin::UsersController"], # 囲まれ方は違う。これも保存 )
  7. ③ 親クラスの callback も動く OrdersController#destroy は (1) → (2) →

    destroy の順で実行。 コントローラを1 ファイル読むだけでは全貌が分からない class ApplicationController < ActionController::Base before_action :authenticate_user! # (1) 親が先 end class OrdersController < ApplicationController before_action :set_order # (2) 子は後 end
  8. ④ skip_before_action class ApplicationController < ActionController::Base before_action :authenticate_user! # 親:

    全ページでログインチェックする end class LpController < ApplicationController skip_before_action :authenticate_user! # 子: このコントローラだけチェックを外す end
  9. ⑤ skip しても、再追加されたら動く class ApplicationController < ActionController::Base before_action :authenticate_user! #

    全ページログイン必須 end class LpController < ApplicationController skip_before_action :authenticate_user! # LP は公開ページなので外す end class MemberLpController < LpController before_action :authenticate_user! # 会員限定LP だけ、再びログイン必須 → 動く! end
  10. 対応していません 現状、静的解析で追う方法が全く思いつかない。一旦素直に諦めました: send / public_send → [dynamic] タグ付きの葉として表示 解決できない呼び出し →

    それ以上潜らず、葉として打ち止め user.send(dynamic_method) # 実行するまで、何を呼ぶか分からない eval(code_string) # 文字列の中身は解析できない def method_missing(name, ...) # 「定義が存在しない」メソッドの呼び出し
  11. これから concern (include / extend )のメソッド解決 effect map — 葉を

    [db-write] [enqueue] [mail] で意味づけ 逆方向トレース(このメソッドを変えたらどのaction に影響する?) MCP server 化