Debugging Super Methods in Ruby 2.214 Jan 2015
Debugging a large codebase is hard. Ruby makes debugging easier by exposing method metadata and caller stack inside Ruby’s own process. Recently in Ruby 2.2.0 this meta inspection got another useful feature by exposing super method metadata. In this post we will look at how this information can be used to debug and why it needed to be added.
One of the first talks I ever wrote was “Dissecting Ruby With Ruby” all about inspecting and debugging Ruby processes using nothing but Ruby code. If you’ve never heard of the Method method it’s worth a watch.
In short, Ruby knows how to execute your code, as well as where your code was defined. For example, with this small class:
class Dog def bark puts "woof" end end
We can see exactly where
Dog#bark is defined:
puts Dog.new.bark # => "woof" puts Dog.new.method(:bark).source_location.inspect # => ["/tmp/dog.rb", 2]
Even if someone did some crazy metaprogramming or you accidentally over-wrote the method, Ruby will always tell you the location of the method it will call.
If you’ve seen the “Dissecting Ruby” talk, you’ll know that there is a big problem with the super method. It’s almost impossible to tell where the final method location being called is written.
class SchneemsDog < Dog def bark super end end
I ended up using some metaprogramming to figure this out:
cinco = SchneemsDog.new cinco.class.superclass.instance_method(:bark) # => ["/tmp/dog.rb", 6]
This works, but it wouldn’t if we did certain types of metaprogramming. For example, we would get the wrong answer if we did this:
module DoubleBark def bark super super end end cinco = SchneemsDog.new cinco.extend(Doublebark)
In this case,
cinco.bark will call the method defined in the
cinco.bark # => bark # => bark puts cinco.method(:bark) #<Method: SchneemsDog(DoubleBark)#bark>
The actual “super” being referred to is defined in the
SchneemsDog class. However, the code tells us that the method is in the
Dog class, which is incorrect.
puts cinco.class.superclass.instance_method(:bark) # => #<UnboundMethod: Dog#bark>
This is because our
Doublebark module isn’t an ancestor of the
cinco.class. How can we solve this issue?
In feature request #9781, I proposed adding a method to allow Ruby to give you this information directly. Shortly after, one of my co-workers, Nobuyoshi Nakada, A.K.A. “The Patch Monster”, attached a working patch, and it was accepted into the Ruby trunk (soon to become 2.2.0) around July.
If you are debugging in Ruby 2.2.0 you can now use Method#super_method. Using the same code we mentioned previously:
cinco = SchneemsDog.new cinco.method(:bark).super_method # => #<Method: Dog#bark>
You can see this returns the method on the
Dog class rather than the
SchneemsDog class. If we call
source_location in the output, we will get the correct value:
module DoubleBark def bark super super end end cinco = SchneemsDog.new cinco.extend(Doublebark) puts cinco.method(:bark) # => #<Method: SchneemsDog(DoubleBark)#bark> puts cinco.method(:bark).super_method # => #<Method: SchneemsDog#bark>
Not only is this simpler, it’s now correct. The return of
super_method will be the same method that Ruby will call when
super is invoked, regardless of whatever craziness is done with metaprogramming. Even though this is a simple example, I hope you’ll find this useful in the wild.
Follow @schneems for Ruby articles and pictures of his dogs. Note that Cinco was not harmed in the making of this blog post