def complete(invoice)
invoice.settle!
@notifier.deliver(invoice)
end deliver. At this call site only two ever received it, and only one of
those in production: those are the two a signature change has to reach.
The problem
Duck typing is Ruby’s best trick and its hardest thing to navigate. This call could go anywhere:
class Checkout
def initialize(notifier:) = @notifier = notifier
def complete(invoice)
invoice.settle!
@notifier.deliver(invoice)
end
end
“Go to definition” on deliver offers five methods: EmailNotifier, SmsNotifier, SlackNotifier,
NullNotifier and Mailer::Legacy. Grep finds the same five. Now you need deliver to take a locale.
Which of them does this call actually reach? Changing all five is wasted work if two are dead, and
changing the wrong three is a production bug.
Static tools can’t narrow it down. notifier is whatever someone passed to Checkout.new, and that’s
decided in a container, a factory or a config file somewhere else.
The fix
Ask the call site:
$ ra who_actually_answered app/models/checkout.rb:6
@notifier.deliver(invoice)
EmailNotifier#deliver 2,904 production runs · 6 spec examples
NullNotifier#deliver 211 spec examples
never answered here SmsNotifier, SlackNotifier, Mailer::Legacy
Two classes answer this call. EmailNotifier in production, and NullNotifier in the specs that
don’t care about notifications. Those are the two that need the new keyword:
class EmailNotifier
def deliver(invoice, locale: I18n.default_locale)
InvoiceMailer.with(invoice:, locale:).receipt.deliver_later
end
end
class NullNotifier
def deliver(_invoice, locale: nil) = nil
end
The other three can wait until something that calls them needs a locale. Mailer::Legacy may be a
candidate for dead_api.
Where it shows up
- Your editor (LSP). “Go to implementation” on a duck-typed call lists the classes that answered it, busiest first, instead of every method with that name.
- Your agent (MCP). Before changing a method, the agent asks which implementations a call site really reaches, and edits those.
- The command line.
ra who_actually_answeredon afile:line, or on a method to list its call sites and the receivers at each.
How it works
Every recorded call is an edge: the call site in the caller (file and line), the method that ran, and
the class of the object it ran on. who_actually_answered groups those edges by call site and counts
the receiving classes.
Each call tree is stamped with its entry point, so the counts split cleanly into spec examples and
production runs. NullNotifier answering only in specs is a different fact from NullNotifier
answering in production, and it’s reported as one.
Limits
- Answered, not could answer. A class that never received the message in a recorded run doesn’t appear, even if some path you didn’t record would send it there. Record the jobs and tasks too, or treat “never answered here” as a question.
- App classes only. A call answered by a core or gem class, like a
Hashpassed in as a notifier, isn’t wrapped, so it doesn’t show up as a receiver.
Related tools
what_calls
Who can call this method. Every caller of a method from real runs, with where and how often. The calls that actually happened, not grep. what_it_calls does the reverse.
See more →what_it_receives
What a method receives. The observed shape of each argument at a call site. The messages the method actually sends to what it's passed, not only the class it was given.
See more →what_a_change_touches
What a change touches. Every caller that relies on a method's observed shape, the argument classes it passes and the messages it sends to what comes back, so you know what a signature change reaches before you make it.
See more →