The Pragmatic Studio

View Source On Ruby Methods

February 13, 2013

It’s second nature to view the source of a web page from your browser, but what about viewing the source of any Ruby method from an irb session or the Rails console?

For example, suppose you have a User ActiveRecord model and in the Rails console you’ve created a new object like so:

>> user = User.new

To update that object’s attributes and save it to the database, you can call the update method, like so:

>> user.update(name: "Fred", email: "fred@example.com")

So, how is that update method implemented? Going directly to the source is the best way to answer these types of questions. Tracking down the definition of the update method seems easy enough. Given that you have all the Rails source files at your disposal, you can simply run a quick search for occurrences of def update.

Trouble is, with that broad of a search you end up sifting through around 50 matches across 37 files. But you’re only interested in the update method of an ActiveRecord model. Ideally you could go directly to the source of that method and pop it open in your favorite editor. Thankfully, Ruby lets us do exactly that!

The first step is to get ahold of the method itself. We do that by calling the method method on the user object, passing in the name of the method we want:

>> method = user.method(:update)
=> #<Method: User(...)#update>

Notice that this returns a Method object that represents the update method. Methods aren’t natively represented as objects in Ruby, but you can objectify a method using this technique.

Here’s the cool part: Given a method object, we can ask it for its source location:

>> location = method.source_location
=> ["/Users/mike/.rvm/gems/ruby-2.5.1@studio/gems/activerecord-5.2.0/lib/active_record/persistence.rb", 423]

The source_location method returns an array with two elements: the name of the source file that defines the method and the line number where it’s defined. Pretty cool indeed!

It sure would be convenient if we could automatically open that file in our favorite editor and position the cursor on the exact line where the method is defined. Indeed, we can!

With VS Code, for example, you just run the code command with the --goto option to pass in file name and line number, separated with a colon. So from inside of the Rails console, you can launch VS Code and go directly to the update method using:

>> `code --goto #{location[0]}:#{location[1]}`

Using backquotes executes the enclosed string as an operating-system command.

Similarly, if you’re using SublimeText, you just run the subl command and separate the file name and the line number with a colon:

>> `subl #{location[0]}:#{location[1]}`

If you’re a TextMate fan, the mate command takes an -l option to indicate the line number:

>> `mate #{location[0]} -l #{location[1]}`

And for the die-hard MacVim users:

>> `mvim #{location[0]} +#{location[1]}`

Taking that a step further, you can bottle up these steps in a method so it’s easy to hop straight into the source of any method. Just toss this method into your .irbrc file (substituting in the command for your favorite editor):

def source_for(object, method)
  location = object.method(method).source_location
  `code --goto #{location[0]}:#{location[1]}` if location
  location
end

Then from your Rails console you can use:

>> source_for(User.new, :update)

The User class also defines an update class method, and to see the source for it you’d use:

>> source_for(User, :update)

We’ve been using source_for from within the Rails console, but you can use this method from within any irb session to view the source for methods in any gems, external libraries, or even your own code!

Building on that foundation, you can make the source_for method a bit more flexible. For example, suppose we wanted to look up the source for instance methods defined in Ruby modules, such as a Rails helper. We’d like to be able to use this

>> source_for(ApplicationHelper, :some_helper)

But that won’t work because the some_helper method isn’t defined on the ApplicationHelper module. It’s an instance method, not a class method. And modules can’t be instantiated, so we can’t pass in ApplicationHelper.new.

Based on Jim Weirich’s suggestion, here’s a slightly more involved version of the source_for method that handles both cases:

def source_for(object, method_sym)
  if object.respond_to?(method_sym, true)
    method = object.method(method_sym)
  elsif object.is_a?(Module)
    method = object.instance_method(method_sym)
  end
  location = method.source_location
  `code --goto #{location[0]}:#{location[1]}` if location
  location
rescue
  nil
end

First it checks if the method is defined on the object. If so, we go ahead and call the method method to get ahold of the method just like before. Otherwise, we check to see if the object is a module. In that case, we then use the instance_method method to get ahold of the instance method defined on that module. Problem solved!

Supporting modules has an interesting side effect. Suppose we want to look up a method on an object that requires a bunch of initialization parameters. In the original version of the source_for method, we’d need to call source_for something like this

>> source_for(MotherOfAllClasses.new(a, b, c, d), :some_method)

It’s inconvenient to have to supply all those initialization parameters just to get an object for the sake of finding the intended method. But given the revised source_for we can instead use

>> source_for(MotherOfAllClasses, :some_method)

This works simply because classes in Ruby are also modules. Let’s trace through the source_for method for this case. First, we check whether MotherOfAllClasses responds to the some_method method. It doesn’t because some_method is an instance method, not a class method. So then we check if MotherOfAllClasses is a module, which it is because all classes in Ruby are also modules. That being the case, the instance_method method returns the some_method method we’re interested in.

Thanks to Jim Weirich and Ernie Miller for their suggestions!

Learn Ruby Today!

Check out our popular Ruby course to learn how to design object-oriented programs the Ruby way, deepen your understanding of the Ruby programming language, and demystify the "magic" of Ruby on Rails!