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!

Leave a Reply

Entries (RSS)