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. - Creating a
Rewriter::RemoveAction
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... - 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!