Rewriting code with Rubocop
Many of you used Rubocop to enforce code style in your project.
But have you thought that it can be also used to rewrite the code?
Under the hood, Rubocop uses parser library to convert Ruby code into syntax tree (AST).
Within Rubocop cop (rule), you can manipulate with nodes of the syntax tree in any way you like. This gives us the power to write code that rewrites another code.
In my case, we did a huge refactoring and the project was full of blocks like:
# old code
# new code
We had hundreds of
ProjectName.support_legacy? statements all over the project. Sometimes it was
if, and sometimes
# do smth
At some point the refactoring was finished and it was time to get rid of all
if ProjectName.support_legacy? branches.
I’m not a big fan of writing complex regular expressions and I decided to give Rubocop a try with rewriting my code automatically.
Rubocop design provides you a way to add your own rules, which are called “cops”. Here is our cop that removes all
if branches with the legacy code:
class RewriteLegacyBranch < Cop
# Constant required for Rubocop
MSG = 'violation message'.freeze
# triggered on any `if` statement in the code
ifst = node.child_nodes
# if this is what we're looking for, mark it as an offence
if ifst.method_name == :support_legacy? && ifst.receiver.source == "ProjectName"
# for unless, completely remove the statement
loc = node.loc
loc.respond_to?(:keyword) && loc.keyword.is?('unless'.freeze)
def drop_if_block_and_leave_new_code(corrector, node)
# drop the `if` and just leave the new code
new_source = String.new
if_content = node.child_nodes
if_content.source.each_line do |line|
# for indentation
if line =~ /^( +)/
line = line[2..-1]
new_source << line
def drop_unless_block(corrector, node)
# indentation workarounds to not leave whitespaces after we remove the block of code
indent_found = node.source_range.source_line =~ /^( +)/
whitespaces = $1.size
r = node.source_range
line_range = r.class.new(r.source_buffer, r.begin_pos - whitespaces, r.end_pos + 1)
It turned out that he Rubocop API is not so well documented. I had to dig around the code of existing cops to see examples.
You’ll may need to do the same if you’re looking into creating rules that are more complex than mine.
Now it’s time to apply the cop to the code:
bundle exec rubocop --require /absolute/path/to/cop_we_wrote.rb --only CustomCops/RewriteLegacyBranch --autocorrect
We provide three arguments to rubocop:
- Require the custom cop that we wrote (the path should be absolute)
- Only apply the single cop (by default, Rubocop will also apply a list of default cops)
- Autocorrect the violations with the rule defined in
I was extremely happy with the fact that Rubocop saved me a couple of hours of cleaning up the legacy code myself.