diff --git a/Gemfile b/Gemfile index f6df068e2a2..192f350a0f6 100644 --- a/Gemfile +++ b/Gemfile @@ -188,6 +188,7 @@ group :development do gem 'librarian', '>= 0.0.25', require: false gem 'annotate' gem 'foreman', require: false + gem 'metamorpher', '~> 0.1.0' end # Gem that enables support for plugins. It is required. diff --git a/refactorings/where_dot_first_to_find_by/app.rb b/refactorings/where_dot_first_to_find_by/app.rb new file mode 100644 index 00000000000..7645623eea6 --- /dev/null +++ b/refactorings/where_dot_first_to_find_by/app.rb @@ -0,0 +1,50 @@ +require "optparse" +require_relative "refactorers/refactor_where_first_to_find_by.rb" +require_relative "refactorers/refactor_where_first_not_called_expectations.rb" +require_relative "refactorers/refactor_where_first_mocks.rb" +require_relative "refactorers/refactor_where_first_strict_mocks.rb" + +options = { overwrite: true } +OptionParser.new do |opts| + opts.banner = "Usage: refactorer.rb [options]" + + opts.on("-d", "--dry-run", "Write changes to console, rather than to source files.") do |v| + options[:overwrite] = false + end +end.parse! + +source_dir = ARGV.first || "." +base = File.expand_path(File.join("**", "*.rb"), source_dir) +puts "Refactoring in source directory: #{base}" + +[ + # Refactor "where(...).first -> find_by(...)" + RefactorWhereFirstToFindBy, + + # Refactor ".expect(:where).never" to ".expect(:find_by).never" + RefactorWhereFirstNotCalledExpectations, + + # Refactor ".expect(:where).return([X])" to ".expect(:find_by).return(X)" + # and ".stubs(:where).return([X])" to ".stubs(:find_by).return(X)" + RefactorWhereFirstMocks, + + # Refactor ".expect(:where).with(...).return([X])" to ".expect(:find_by).with(...).return(X)" + RefactorWhereFirstStrictMocks + +].each do |refactorer| + refactorer.new.refactor_files(Dir.glob(base)) do |path, refactored, changes| + if changes.empty? + puts "No changes in #{path}" + + else + puts "In #{path}:" + + changes.each do |change| + puts "\tAt #{change.original_position}, inserting:\n\t\t#{change.refactored_code}" + puts "" + end + + File.open(path, "w") { |f| f.write(refactored) } if options[:overwrite] + end + end +end diff --git a/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_mocks.rb b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_mocks.rb new file mode 100644 index 00000000000..628fdaa3a30 --- /dev/null +++ b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_mocks.rb @@ -0,0 +1,31 @@ +require "metamorpher" + +class RefactorWhereFirstMocks + include Metamorpher::Refactorer + include Metamorpher::Builders::Ruby + + def pattern + builder + .build("TYPE.DOUBLE_METHOD(:where).returns(ARRAY_VALUE)") + .ensuring("DOUBLE_METHOD") { |m| m.name == :expects || m.name == :stubs } + .ensuring("ARRAY_VALUE") { |v| v.name == :array } + # Doesn't match non-array return types, such as Topic.stubs(:where).returns(Topic) + end + + def replacement + builder + .build("TYPE.DOUBLE_METHOD(:find_by).returns(SINGLE_VALUE)") + .deriving("SINGLE_VALUE", "ARRAY_VALUE") { |array_value| take_first(array_value) } + end + + private + + # Refactor the argument from [] to nil, or from [X] to X + def take_first(array_value) + if array_value.children.empty? + builder.build("nil") + else + array_value.children.first + end + end +end diff --git a/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_not_called_expectations.rb b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_not_called_expectations.rb new file mode 100644 index 00000000000..6edf5f4e8c7 --- /dev/null +++ b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_not_called_expectations.rb @@ -0,0 +1,14 @@ +require "metamorpher" + +class RefactorWhereFirstNotCalledExpectations + include Metamorpher::Refactorer + include Metamorpher::Builders::Ruby + + def pattern + builder.build("TYPE.expects(:where).never") + end + + def replacement + builder.build("TYPE.expects(:find_by).never") + end +end diff --git a/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_strict_mocks.rb b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_strict_mocks.rb new file mode 100644 index 00000000000..51470f2b2e8 --- /dev/null +++ b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_strict_mocks.rb @@ -0,0 +1,27 @@ +require "metamorpher" + +class RefactorWhereFirstStrictMocks + include Metamorpher::Refactorer + include Metamorpher::Builders::Ruby + + def pattern + builder.build("TYPE.expects(:where).with(PARAMS_).returns(ARRAY_VALUE)") + end + + def replacement + builder + .build("TYPE.expects(:find_by).with(PARAMS_).returns(SINGLE_VALUE)") + .deriving("SINGLE_VALUE", "ARRAY_VALUE") { |array_value| take_first(array_value) } + end + + private + + # Refactor the argument from [] to nil, or from [X] to X + def take_first(array_value) + if array_value.children.empty? + builder.build("nil") + else + array_value.children.first + end + end +end diff --git a/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_to_find_by.rb b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_to_find_by.rb new file mode 100644 index 00000000000..4632deeba0a --- /dev/null +++ b/refactorings/where_dot_first_to_find_by/refactorers/refactor_where_first_to_find_by.rb @@ -0,0 +1,14 @@ +require "metamorpher" + +class RefactorWhereFirstToFindBy + include Metamorpher::Refactorer + include Metamorpher::Builders::Ruby + + def pattern + builder.build("TYPE.where(PARAMS_).first") + end + + def replacement + builder.build("TYPE.find_by(PARAMS_)") + end +end