Detecting common Ruby errors at definition time using ParseTree and method_added
Mark Aiken pointed out a really bad Ruby gotcha-- Ruby doesn't automatically call the base class initializer when you use inheritance, nor does it warn you if you forget to call super. This behavior is different from just about every other object oriented language, and can lead to partially initialized objects and really hard to diagnose errors. (In his case, he was subclassing ActiveRecord::Base.)
Perhaps future versions of Ruby will fix this misfeature, but in the meanwhile Ruby is dynamic enough that it's possible to detect and warn specifically about such errors as soon as classes are loaded.
The following approach is really nothing more than proof of concept, but hopefully could be the basis for a real verifier that could be loaded in development mode to spot common errors. The idea is to write a verify_class function that uses ParseTree to convert a class into a set of sexps and then looks for common errors statically, like forgetting to call super in the initialize function of a class that has a class that has a superclass. (In addition, the verifier could potentially wrap methods to add additional runtime checks.) The method_added method is wrapped to call the verify_class method whenever a new method is added to the class.
-
require 'ruby2ruby'
-
-
class Class
-
alias_method :method_added_orig, :method_added
-
-
def verify_class(clazz)
-
for node in ParseTree.new.parse_tree(clazz)
-
if node[0] == :class && node[2] != [:const, :Object]
-
for method in node[3..-1]
-
if method[0..1] == [:defn, :initialize]
-
statements = method[2][1]
-
if statements.is_a?(Array) &&
-
!statements.detect {|s| s == [:zsuper]}
-
raise "Error, initialize defined in subclass without call to super"
-
end
-
end
-
end
-
end
-
end
-
true
-
end
-
-
-
def method_added(p)
-
method_added_orig(p)
-
if p = "initialize"
-
puts "Verifying #{p}"
-
verify_class(self)
-
end
-
end
-
-
end
The following irb session shows the verifier in action:
-
irb(main):002:0> class Foo; def initialize; end; end
-
Verifying initialize
-
=> nil
-
irb(main):003:0> class Good < Foo; def initialize; super; end; end
-
Verifying initialize
-
=> nil
-
irb(main):004:0> class Bad < Foo; def initialize; end; end
-
Verifying initialize
-
RuntimeError: Error, initialize defined in subclass without call to super