Automating a redirect_to / flash cleanup

15 Aug 2017

If you're like me and have an application with some pockets of very outdated Rails idioms, you may have code like this in your controllers:

class PostsController < ApplicationController
  def publish
    Post.find(params[:id]).publish!
    flash[:notice] = "Post published!"
    redirect_to root_path
  end
end

This can of course be rewritten to use a Hash as the second argument to redirect_to:

class PostsController < ApplicationController
  def publish
    Post.find(params[:id]).publish!
    redirect_to root_path, notice: "Post published!"
  end
end

And there's a similar, but slightly different, transformation that can be made for setting error flash messages:

# before
flash[:error] = "Could not approve comment!"
redirect_to root_path
# after
redirect_to root_path, flash: {error: "Could not approve comment!"}

I had a bunch of these changes that needed fixing up, and it would have taken around 15 minutes to search around the codebase and fix all the references. But why do in 15 minutes what you can automate in a weekend? I've written about synvert before and it's the perfect tool for automating this transformation. Thus, a new synvert rule:

Synvert::Rewriter.new 'rails', 'redirect_with_flash' do
  description <<-EOS

  Fold flash setting into redirect_to.

  flash[:notice] = "huzzah"
  redirect_to root_path
  =>
  redirect_to root_path, notice: "huzzah"

  and

  flash[:error] = "booo"
  redirect_to root_path
  =>
  redirect_to root_path, flash: {error: "huzzah"}

  EOS
  within_file 'app/controllers/**/*rb' do
    within_node type: 'def' do
      line = nil
      msg = nil
      remover_action = nil
      flash_type = nil
      with_node type: 'send', receiver: 'flash', arguments: { size: 2, last: { type: :str } } do
        line = node.line
        flash_type = node.arguments.first.to_source
        msg = node.arguments.last.to_source
        remover_action = Synvert::Rewriter::RemoveAction.new(self)
      end
      with_node type: 'send', receiver: nil, message: :redirect_to do
        if line.present? && node.line == line + 1
          @actions << remover_action
          if [':notice', ':alert'].include?(flash_type)
            replace_with " , #{flash_type[1..-1]}: #{msg}"
          else
            replace_with " , flash: {#{flash_type[1..-1]}: #{msg}}"
          end
        end
      end
    end
  end
end

This whipped right through the codebase I was working on and fixed up most occurrences in a jiffy. A few notes:

Takeaways are that synvert continues to be a very handy tool, that it's worth rescanning the Rails API docs occasionally to see what you've missed, and that automating tedious things is good fun!