Richard Huang
Xinmin Labs
@flyerhzm
post = FactoryGirl.create(:post)
to
post = create(:post)
1.should == 1
to
expect(1).to eq 1
user = User.find_last_by_email_and_active('flyerhzm@xinminlabs.com', true)
to
user = User.where(email: 'flyerhzm@xinminlabs.com', active: true).last
Convert syntax for project 1
......
Convert syntax for project N
Convert syntax for their projects
Synvert + Snippets
AST
Ruby code
post = FactoryGirl.create(:post)
AST
(lvasgn :post
(send
(const nil :FactoryGirl) :create
(sym :post)))
Synvert::Rewriter.new 'synvert_name' do # Rewriter
if_gem 'gem_name', {gte: '1.0.0'} # GemSpec
within_files '**/*.rb' do # Instance
within_node rules_to_find_node do # Scope
unless_exist_node rules_to_check_node do # Condition
insert "what code to insert" # Action
end
end
end
end
Adds
include FactoryGirl::Syntax::Methods
to
class Test::Unit::TestCase
in file
test/test_helper.rb
Synvert::Rewriter.new 'factory_girl_short_syntax' do
within_file 'test/test_helper.rb' do
with_node type: 'class', name: 'Test::Unit::TestCase' do
insert 'include FactoryGirl::Syntax::Methods'
end
end
end
From factory_girl 2.0.0, so let's check the gem version.
Synvert::Rewriter.new 'factory_girl_short_syntax' do
# check if factory_girl gem greater than or equal to 2.0.0 in Gemfile.lock
if_gem 'factory_girl', {gte: '2.0.0'}
within_file 'test/test_helper.rb' do
with_node type: 'class', name: 'Test::Unit::TestCase' do
insert 'include FactoryGirl::Syntax::Methods'
end
end
end
It works, but it inserts code every time even when
include FactoryGirl::Syntax::Methods
already exist in the class, Synvert::Rewriter.new 'factory_girl_short_syntax' do
if_gem 'factory_girl', {gte: '2.0.0'}
within_file 'test/test_helper.rb' do
with_node type: 'class', name: 'Test::Unit::TestCase' do
# unless include FactoryGirl::Syntax::Methods exists
unless_exist_node type: 'send', message: 'include',
arguments: ['FactoryGirl::Syntax::Methods'] do
insert 'include FactoryGirl::Syntax::Methods'
end
end
end
end
It should also work in minitest and activesupport testcase
Synvert::Rewriter.new 'factory_girl_short_syntax' do
if_gem 'factory_girl', {gte: '2.0.0'}
within_file 'test/test_helper.rb' do
%w(Test::Unit::TestCase ActiveSupport::TestCase
MiniTest::Unit::TestCase MiniTest::Spec
MiniTest::Rails::ActiveSupport::TestCase).each do |class_name|
with_node type: 'class', name: class_name do
unless_exist_node type: 'send', message: 'include',
arguments: ['FactoryGirl::Syntax::Methods'] do
insert 'include FactoryGirl::Syntax::Methods'
end
end
end
end
end
For RSpec, it does a bit different
RSpec.configure do |config|
config.incldue FactoryGirl::Syntax::Methods
end
Synvert::Rewriter.new 'factory_girl_short_syntax' do
if_gem 'factory_girl', {gte: '2.0.0'}
within_file 'spec/spec_helper.rb' do
# match code RSpec.configure do |config|; ... end
within_node type: 'block', caller: {receiver: 'RSpec',
message: 'configure'} do
unless_exist_node type: 'send', message: 'include',
arguments: ['FactoryGirl::Syntax::Methods'] do
# {{ }} is executed on current node, so we can add or
# replace with some old code,
# arguments are [config], arguments.first is config,
insert "{{arguments.first}}.include FactoryGirl::Syntax::Methods"
end
end
end
end
Finally, apply it for cucumber as well.
World(FactoryGirl::Syntax::Methods)
Synvert::Rewriter.new 'factory_girl_short_syntax' do
if_gem 'factory_girl', {gte: '2.0.0'}
within_file 'features/support/env.rb' do
# current node is the root node of env.rb file
# we don't need with_node / within_node here
unless_exist_node type: 'send', message: 'World',
arguments: ['FactoryGirl::Syntax::Methods'] do
insert "World(FactoryGirl::Syntax::Methods)"
end
end
end
Now we can replace
FactoryGirl.create(:post)
with
create(:post)
Synvert::Rewriter.new 'factory_girl_short_syntax' do
if_gem 'factory_girl', {gte: '2.0.0'}
%w(test/**/*.rb spec/**/*.rb features/**/*.rb).each do |file_pattern|
# find all files in test/, spec/ and features/
within_files file_pattern do
with_node type: 'send', receiver: 'FactoryGirl', message: 'create' do
# for FactoryGirl.create(:post, title: 'post'),
# arguments are :post, title: 'post'
replace_with "create({{arguments}})"
end
end
end
end
There are other short syntaxes,
e.g. build, create_list, build_list, etc.
Synvert::Rewriter.new 'factory_girl_short_syntax' do
if_gem 'factory_girl', {gte: '2.0.0'}
%w(test/**/*.rb spec/**/*.rb features/**/*.rb).each do |file_pattern|
within_files file_pattern do
%w(create build attributes_for build_stubbed create_list build_list
create_pair build_pair).each do |message|
with_node type: 'send', receiver: 'FactoryGirl', message: message do
replace_with "#{message}({{arguments}})"
end
end
end
end
end
Finally write description for the rewriter
Synvert::Rewriter.new 'factory_girl_short_syntax' do
description <<-EOF
1. it adds FactoryGirl::Syntax::methods module to RSpec, Test::Unit,
Cucumber, Spainach, MiniTest, MiniTest::Spec, minitest-rails.
2. it converts to short syntax.
FactoryGirl.create(...) => create(...)
FactoryGirl.build(...) => build(...)
......
EOF
if_gem 'factory_girl', {gte: '2.0.0'}
......
end
From rails 4, dynamic finders are deprecated
User.find_last_by_email_and_active('flyerhzm@xinminlabs.com', true)
to
User.where(email: 'flyerhzm@xinminlabs.com', active: true).last
Synvert::Rewriter.new "convert_dynamic_finders" do
within_files '**/*.rb' do
with_node type: 'send', message: /^find_last_by_(.*)/ do
# node is current matching ast node
# you can add any ruby code in the block
# here we convert dynamic finder message to hash params, e.g.
# email_and_active('email', active) to email: 'email', active: active
#
# node.source(self) is used to get original ruby source code
# {{receiver}} gets the receiver of current ast node
fields = node.message.to_s["find_last_by_".length..-1].split("_and_")
hash_params = fields.length.times.map { |i|
fields[i] + ": " + node.arguments[i].source(self)
}.join(", ")
replace_with "{{receiver}}.where(#{hash_params}).last"
end
end
end
Let's use helper_method for other dynamic finders.
Synvert::Rewriter.new "convert_dynamic_finders" do
helper_method 'dynamic_finder_to_hash' do |prefix|
fields = node.message.to_s[prefix.length..-1].split("_and_")
fields.length.times.map { |i|
fields[i] + ": " + node.arguments[i].source(self)
}.join(", ")
end
within_files '**/*.rb' do
with_node type: 'send', message: /^find_all_by_(.*)/ do
hash_params = dynamic_finder_to_hash("find_all_by_")
replace_with "{{receiver}}.where(#{hash_params})"
end
with_node type: 'send', message: /^find_last_by_(.*)/ do
hash_params = dynamic_finder_to_hash("find_last_by_")
replace_with "{{receiver}}.where(#{hash_params}).last"
end
......
end
end
Any issue and pull request are welcome