How to Traverse Foreign Ruby Code

A 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.