How to Traverse Foreign Ruby Code
25 Oct 2015A Ruby on Rails project will most likely contain large amounts of third party software. Software written by other people can fluctuate greatly in terms of documentation. Even very well documented software might have pieces that are more shrouded than others.
When these opaque pieces of code start to cause issue or incite curiosity, spelunking through these libraries is easier with simple patterns and the right tools.
0. Get the Right Tools
An important piece of technology when reading code is a text editor. The right editor will make searching for methods fast and opening files painless.
Another vital component when traversing Ruby code is a runtime debugger. For this, the pry
gem is a personal favorite. Using pry
is as simple as requiring it, then adding binding.pry
at the desired stopping point. Alternatively, some people like to debug with output statements in code execution; but, this is 2015 and I like things that I can interact with.
1. Use source
and source_location
This example will use ActiveRecord
’s store_accessor
method as the subject of investigation.
The store_accessor
method is useful for storing data with a volatile structure. If an application wants to prototype a feature or is unsure about how useful a certain data structure might be, using store_accessor
is reasonable. In this case we can assume that the store_accessor
column is the json
type.
Given a User
class, with a column named settings
, we can define two store_accessors
: is_registered
and contact_method
.
class User < ActiveRecord::Base
store_accessor :settings, :is_registered, :contact_method
end
This model responds to contact_method=
and serializes the result into the json
column.
If we wanted to see how this was defined, we need to start a console and look at where the contact_method=
method exists.
To see the definition of a method, method
and source
are helpful.
user = User.new
puts user.method(:contact_method=).source
#=> define_method("#{key}=") do |value|
# write_store_attribute(store_attribute, key, value)
# end
It seems that the contact_method=
method is meta-programmed. This does not say much but gives a good starting point. Now, to find which file this method is defined in, source_location
is used.
user.method(:contact_method=).source_location
#=> ["$RVM_PATH/.rvm/gems/ruby-2.2.1@global/gems/activerecord-4.2.4/lib/active_record/store.rb", 85]
As assumed, the method which defines the store_accessor
methods is inside of ActiveRecord
, specifically on line 85
of store.rb
.
2. Place a Reasonable binding.pry
With the location of the method found, binding.pry
has a logical place to go. Since gems are not magic, the ActiveRecord
gem can be opened and its source easily read. Opening gems is fairly simple, set a desired EDITOR
and open via the bundle
command. For this example, subl
is mapped to Sublime Text.
EDITOR=subl bundle open activerecord
Then, navigating to line 85
of store.rb
, a binding.pry
can be added to stop code execution when the contact_method=
method is used.
# In activerecord-4.2.4/lib/active_record/store.rb
define_method("#{key}=") do |value|
require 'pry'
if key == :contact_method
binding.pry
end
write_store_attribute(store_attribute, key, value)
end
Note: adding require 'pry'
might not be necessary, depending on the bundle this code is running in.
With the binding.pry
in place, launching a new rails console
will use the modified code. When a user
’s contact_method=
method is called, pry will take over.
user = User.new
user.contact_method = { phone: true }
85: define_method("#{key}=") do |value|
86: require 'pry'
87: if key == :contact_method
=> 88: binding.pry
89: end
90: write_store_attribute(store_attribute, key, value)
91: end
We have liftoff! The code is halted in the meta programmed definition of this setter. The local variables here can be accessed to see what is actually going on:
[1] pry> store_attribute
# => :settings
[2] pry> value
# => {:phone=>true}
[3] pry> key
# => :contact_method
This tells us some, but it seems that the real logic is in the write_store_attribute
method. While the code execution is stopped, we are able to use the same method().source
and method().source_location
calls from before to gain even more insight.
[4] pry> puts method(:write_store_attribute).source
# => def write_store_attribute(store_attribute, key, value)
# => accessor = store_accessor_for(store_attribute)
# => accessor.write(self, store_attribute, key, value)
# => end
[5] pry> method(:write_store_attribute).source_location
#=> ["$RVM_PATH/.rvm/gems/ruby-2.2.1@global/gems/activerecord-4.2.4/lib/active_record/store.rb",
129]
3. Make an Exit
Pry comes with a number of helpful commands for code navigation. An important command is exit
, which will stop the at a specified break point and let the code continue to either the next break point or until completion.
Multiple binding.pry
lines may be added to different files in order to jump from break point to break point. If we wanted to dig deeper, to see what the accessor
variable is in write_store_attribute
, we could place a second binding.pry
.
# In activerecord-4.2.4/lib/active_record/store.rb
def write_store_attribute(store_attribute, key, value)
accessor = store_accessor_for(store_attribute)
binding.pry
accessor.write(self, store_attribute, key, value)
end
Now, exit
and re-running rails console
shows both break points in action.
> user = User.new
> user.contact_method = { phone: true }
85: define_method("#{key}=") do |value|
86: require 'pry'
87: if key == :contact_method
=> 88: binding.pry
89: end
90: write_store_attribute(store_attribute, key, value)
91: end
[1] pry> exit
129: def write_store_attribute(store_attribute, key, value)
130: accessor = store_accessor_for(store_attribute)
=> 131: binding.pry
132: accessor.write(self, store_attribute, key, value)
133: end
pry> accessor
#=> ActiveRecord::Store::StringKeyedHashAccessor
pry> puts accessor.method(:write).source
# => def self.write(object, attribute, key, value)
# => super object, attribute, key.to_s, value
# => end
More break points, more knowledge. Each new binding.pry
gives a new context which subsequently opens more avenues of exploration. This is obviously not the end of the store_accessor
logic, but a great first step has been made.
Note: The @
command can be used to show the current bound context. This is especially helpful if many different debug or output statements have been used in one bind point.
4. Rinse and Repeat
Following the same pattern, any depth of code can be reached by placing a binding.pry
, observing results and repeating. Traveling through code that is foreign will also help build confidence. When developers stop assuming that things are black boxes of magic, everyone benefits.
Using these simple techniques, code previously hidden or otherwise out of reach becomes accessible and easy to traverse. Finally, I would advise to keep trips down the code rabbit hole short, or you might shave one too many yaks.