How to Traverse Foreign Ruby Code

来源:互联网 时间:1970-01-01

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 prygem is a personal favorite. Using pryis as simple as requiring it, then adding binding.pryat 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 sourceand source_location

This example will use ActiveRecord's store_accessormethod as the subject of investigation.

The store_accessormethod 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_accessoris reasonable. In this case we can assume that the store_accessorcolumn is the jsontype.

Given a Userclass, with a column named settings, we can define two store_accessors: is_registeredand contact_method.

class User < ActiveRecord::Base store_accessor :settings, :is_registered, :contact_methodend

This model responds to contact_method=and serializes the result into the jsoncolumn.

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, methodand sourceare helpful.

user = User.newputs 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_locationis used.

user.method(:contact_method=).source_location#=> ["$RVM_PATH/.rvm/gems/[email protected]/gems/activerecord-4.2.4/lib/active_record/store.rb", 85]

As assumed, the method which defines the store_accessormethods is inside of ActiveRecord, specifically on line 85of store.rb.

2. Place a Reasonable binding.pry

With the location of the method found, binding.pryhas a logical place to go. Sincegems are not magic, the ActiveRecordgem can be opened and its source easily read. Opening gems is fairly simple, set a desired EDITORand open via the bundlecommand. For this example, sublime is mapped to Sublime Text.

EDITOR=sublime bundle open activerecord

Then, navigating to line 85of store.rb, a binding.prycan be added to stop code execution when the contact_method=method is used.

# In activerecord-4.2.4/lib/active_record/store.rbdefine_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 in place, launching a new rails consolewill use the modified code. When a user's contact_method=method is called, the binding will take over.

user = User.newuser.contact_method = { phone: true }85: define_method("#{key}=") do |value|86: require 'pry'87: if key == :contact_method => 88: binding.pry89: end90: 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_attributemethod. While in the binding, we are able to use the same method().sourceand method().source_locationcalls 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/[email protected]/gems/activerecord-4.2.4/lib/active_record/store.rb", 129] 3. Make an Exit

Pry comes with a number of helpful commandsfor code navigation. An important command is exit, which will stop the binding and let the code continue to either the next binding or until completion.

Multiple binding.prylines 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 accessorvariable is in write_store_attribute, we could place a second binding.

# In activerecord-4.2.4/lib/active_record/store.rbdef write_store_attribute(store_attribute, key, value) accessor = store_accessor_for(store_attribute) binding.pry accessor.write(self, store_attribute, key, value)end

Now, exitand re-running rails consoleshows both break points in action.

> user => 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: endpry> accessor#=> ActiveRecord::Store::StringKeyedHashAccessorpry> puts accessor.method(:write).source# => def self.write(object, attribute, key, value)# =>super object, attribute, key.to_s, value# => end

More bindings, more knowledge. Each new binding.prygives a new context which subsequently opens more avenues of exploration. This is obviously not the end of the store_accessorlogic, 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.