Ruby Refactoring
Refactoring is one of those areas that those people with Big Flashy IDEs like to complain has very little support in Ruby. To a large degree, there’s truth in that – Ruby’s syntax is complex, and its dynamism means that there are some things that you just can’t predict at writing time, so doing things like method renaming is really quite difficult. So, what to do? Changing things to make their names more accurately reflect their function is a fairly critical refactoring operation. Well, if we can’t do it at write time, how about runtime?
What if we were able to add an annotation to a method definition such that the next time it was evaluated, not only was the method renamed, but all the places that call it get the relevant substitution performed as well? In other words, we want to go from this (subject.rb):
class TestClass
def my_method(foo) # becomes my_new_method
puts foo
end
end
def meth_calling_renamed(t)
t.my_method('Ok2')
end
t = TestClass.new
meth_calling_renamed(t)
to this:
class TestClass
def my_new_method(foo) # was my_method
puts foo
end
end
def meth_calling_renamed(t)
t.my_new_method('Ok2')
end
t = TestClass.new
meth_calling_renamed(t)
The first part, replacing the original definition, is easy enough – it’s a regexp operation on the source file. How do we go about fixing the places that call it, though? Object#method_missing to the rescue! The following code (rename_method.rb) does the whole caboodle:
$obj_replacement_table = {}
class Object
def method_missing(sym, *args, &block)
if $obj_replacement_table.has_key? sym.to_s
filename, line_num = caller[0].split(':')[0,2]
l = line_num.to_i - 1
line_arr = File.readlines(filename)
line_arr[l] =
line_arr[l].sub(sym.to_s, $obj_replacement_table[sym.to_s])
File.open(filename, 'wb'){|f| f.write line_arr}
self.send($obj_replacement_table[sym.to_s].to_sym, *args, &block)
end
end
end
def process_file(filename)
line_arr = File.readlines(filename)
line_arr.each_with_index do |line,i|
if line.chomp =~ /(\s+)def (\w+)(.*)#\s*becomes\s+(\w+)/
line_arr[i] = "#{$1}def #{$4}#{$3}# was #{$2}\n"
$obj_replacement_table[$2] = $4
end
end
File.open(filename, 'wb'){|f| f.write line_arr }
end
filename = ARGV.shift
process_file(filename)
load(filename)
It’s run as ruby rename_method.rb subject.rb, and does, in fact, produce the results shown above. There are several things I’m sure I’ll get around to improving about this One Of These Days™, but it’s good enough for using Right Now. I’ll wrap it up in a vim-friendly fashion soon, though, because I think it makes quite a nice addition to a toolset which already includes this nifty piece of kit.
The inevitable query is raised: what about calls to the renamed method that are made from sections which aren’t exercised by the specific command line you run this with? Well, that’s one more use for a full test suite!