skip to content

In Ruby, a method on a live object behaves unlike the code you read — how do you find the file and line that actually define it?

level: seniorimportance: should knowfreq 40%

answer

  1. method(:name) first
  2. source_location: file and line
  3. nil means native code
  4. super_method walks the chain
  5. (eval at ...) means generated

basics

~10 s

Call obj.method(:name).source_location. It returns the file and line of the definition that call would actually run, exposing a patch or prepended module; nil means the method is implemented in C.

solid answer

~40 s

Ask the object, not the source tree: `obj.method(:total).source_location` returns `[file, line]` for the definition a call to `total` would dispatch to right now, after every reopening, `prepend` and later redefinition has been applied. If a gem patched the method, the path points into that gem. `nil` means a method implemented in C, such as most core methods. For `attr_accessor` methods the location is the `attr_accessor` line; for `define_method`, the block; for string-evaluated code, whatever file the eval was given, or an `(eval at ...)` label. `super_method` returns the next definition up the chain, so you can walk from a patch to the original, and `Klass.instance_method(:total).source_location` asks the class without an instance.

code

ruby · 15 lines
ruby
class Invoice
  def total = 100
end

module Discounts
  def total = super * 0.9
end
Invoice.prepend(Discounts)

m = Invoice.new.method(:total)
m.source_location                  # => ["invoice.rb", 6]  (the prepended patch)
m.super_method.source_location     # => ["invoice.rb", 2]  (the original)
m.super_method.super_method        # => nil

1.method(:+).source_location       # => nil: implemented in C

go deeper

for a junior

Recall that obj.method(:name).source_location returns the file and line of a method's definition.

for a middle

Explain that the lookup follows the real ancestor chain, and what nil, attr and define_method locations mean.

for a senior

Diagnose a patched method in a live console: find the winning definition, walk super_method to the original, and name the gem responsible.

for a principal

Push for patches that are discoverable (named modules, real file locations) so the next incident can be traced from the process.

## Why reading the source is not enough Ruby classes are **open**. Any file loaded later can reopen a class and replace a method, a module can be `prepend`ed in front of it, and a gem can do either without your code mentioning it. The method a call runs is decided when the call happens, by walking the object's ancestors. So when behaviour and source disagree, the fastest check is to ask the running process which definition it will use. ## The tool: source_location 1. Get a `Method` object for the name: `obj.method(:total)`. This performs the same lookup a call would, so it finds the winning definition. 2. Call `source_location` on it. In Ruby 4.0 it returns a two-element array, `[path, line]`: the file and the line where that definition starts. ```ruby invoice.method(:total).source_location # => ["/gems/discounts-2.1.0/lib/discounts/invoice_patch.rb", 12] ``` A path inside a gem, or inside a file you did not expect, is the answer to "why does this behave differently". (Ruby 4.0's rdoc for this method describes a five-element array with columns, but the 4.0.7 implementation returns only the path and line.) ## Reading the result | Result | Meaning | |---|---| | `["app/models/invoice.rb", 40]` | defined in Ruby at that file and line | | a path under a gem directory | a gem defined or patched it | | `nil` | implemented in C: a core or extension method | | the line of an `attr_accessor` call | an accessor generated there | | the line of a `define_method` block | a method generated from that block | | `["(eval at lib/x.rb:3)", 1]` | compiled from a string without a file argument | The last row is why string-evaluated code should pass `__FILE__` and `__LINE__`: with them, `source_location` points at a real file. ## Walking to the next definition A patch usually calls `super` to reach the original. `Method#super_method` returns a `Method` for the next definition up the ancestor chain, or `nil` when there is none, so you can follow the layers: - `m = obj.method(:total)` — the winning definition, perhaps a prepended module; - `m.super_method.source_location` — the definition it wraps; - repeat until `super_method` returns `nil`. ## A worked diagnosis 1. Reproduce the surprise in a console against the same object, not a freshly built one: extensions applied to one object change its lookup. 2. Run `m = obj.method(:total)` and read `m.source_location`. 3. If the path is not the file you were reading, open it: that is the code that runs. 4. Walk `m.super_method` step by step to list every layer between the patch and the original, and note which layers call `super`. 5. Record which gem or file installed each layer, so the fix can target the right one instead of adding another patch on top. The whole check takes a minute and replaces guesswork about load order with a direct answer from the process. ## Asking the class instead of an instance `Invoice.instance_method(:total)` returns an `UnboundMethod` for what instances would run; its `source_location` works the same way. This is useful in a console when building an instance is expensive, or in tooling that walks classes. ## Limits - **Native methods** have no Ruby source; `nil` is the answer, and the C function is found in the interpreter or extension source. - **Ghost methods** handled by `method_missing` are not methods; `obj.method(:x)` finds one only if the class implements `respond_to_missing?`, and then the location is not the ghost's own. - The answer is a snapshot: a file loaded after you looked can redefine the method again. ## Mistakes to avoid - Grepping the codebase for `def total` and trusting the first hit. - Reading `nil` as "the method does not exist"; it means it is native. - Stopping at the first location when the winning definition is a wrapper that calls `super`. - Leaving string-generated methods without file and line, so their location is an eval label.

  • What does source_location return for a method created by attr_accessor or define_method?
    For `attr_accessor`, `attr_reader` and `attr_writer` methods, the file and line of the `attr_*` call that generated them. For `define_method`, the location of the block that became the method body. Both point at real code as long as it was not compiled from a string without a file argument.
  • Why can obj.method(:name) fail for a name that obj happily responds to?
    If the name is served by `method_missing`, there is no method to wrap, so `method` raises `NameError` unless the class also implements `respond_to_missing?` for that name. Even then, the resulting `Method` has no source of its own to report.

saying these in an interview costs you the question

  • Grepping for def name shows the definition that will run
  • source_location returning nil means the method does not exist
  • source_location always points at the original class, not a patch
  • Method#source_location in Ruby 4.0 returns line and column ranges
  • A prepended module cannot be discovered from the running object