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:

Setting local variables feels clumsy. When the rule encounters a flash setting it needs to shift into a different state where now it's checking the next line for a redirect_to invocation. I'm not sure of a nicer way to express this though.

invocation. I'm not sure of a nicer way to express this though. Creating a Rewriter::RemoveAction and then maybe discarding it feels similarly clumsy.

and then maybe discarding it feels similarly clumsy. As Richard initially pointed out on the PR it needed to check more types of flash; I fixed that up.

This misses cases where the flash setting and the redirect_to are not on adjacent lines. To sort through that we would need to do data flow analysis. This is challenging in a dynamically typed language, but perhaps we could do something on the basic block level. Perhaps a future effort...

are not on adjacent lines. To sort through that we would need to do data flow analysis. This is challenging in a dynamically typed language, but perhaps we could do something on the basic block level. Perhaps a future effort... Having done a rule in the past and knowing the mechanics of iterating on it helped a lot. No mystery there, except that it was nice to refer back to my own blog post for how to do that.

I didn't realize how long this Action Pack feature has been available. It was committed by DHH in 2009! Way back when...

Edit: This rule is in synvert-snippets now, so if you have synvert installed and do a synvert --sync && synvert -r rails/redirect_with_flash it'll fix up your code.

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!