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!