From f88f5a457cb95dc22519aa33b527a73f198e92e8 Mon Sep 17 00:00:00 2001 From: Daniel Colson Date: Tue, 3 Apr 2018 20:50:00 -0400 Subject: [PATCH 1/2] Add custom RuboCop for `assert_not` over `refute` Since at least cf4afc4 we have preferred `assert_not` methods over `refute` methods. I have seen plenty of comments in PRs about this, and we have tried to fix it a few times (5294ad8, e45f176, 8910f12, 41f50be, d4cfd54, 48a183e, and 211adb4), but the `refute` methods keep sneaking back in. This custom RuboCop will take care of enforcing this preference, so we don't have to think about it again. I suspect there are other similar stylistic preferences that could be solved with some custom RuboCops, so I will definitely keep my eyes open. `assert_not` over `assert !` might be a good candidate, for example. I wasn't totally sure if `ci/custom_cops` was the best place to put this, but nothing else seemed quite right. At one point I had it set up as a gem, but I think custom cops like this would have limited value in another context. I want to see how code climate handles the new cops before autocorrecting the existing violations. If things go as expected, I will push another commit with those corrections. --- .rubocop.yml | 7 ++ ci/custom_cops/bin/test | 5 ++ ci/custom_cops/lib/custom_cops.rb | 3 + ci/custom_cops/lib/custom_cops/refute_not.rb | 71 ++++++++++++++++++ .../test/custom_cops/refute_not_test.rb | 75 +++++++++++++++++++ ci/custom_cops/test/support/cop_helper.rb | 35 +++++++++ 6 files changed, 196 insertions(+) create mode 100755 ci/custom_cops/bin/test create mode 100644 ci/custom_cops/lib/custom_cops.rb create mode 100644 ci/custom_cops/lib/custom_cops/refute_not.rb create mode 100644 ci/custom_cops/test/custom_cops/refute_not_test.rb create mode 100644 ci/custom_cops/test/support/cop_helper.rb diff --git a/.rubocop.yml b/.rubocop.yml index 3c765d5b1db..eb410376fe2 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,5 @@ +require: './ci/custom_cops/lib/custom_cops' + AllCops: TargetRubyVersion: 2.4 # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop @@ -8,6 +10,11 @@ AllCops: - '**/vendor/**/*' - 'actionpack/lib/action_dispatch/journey/parser.rb' +# Prefer assert_not_x over refute_x +CustomCops/RefuteNot: + Include: + - '**/*_test.rb' + # Prefer &&/|| over and/or. Style/AndOr: Enabled: true diff --git a/ci/custom_cops/bin/test b/ci/custom_cops/bin/test new file mode 100755 index 00000000000..495ffec83aa --- /dev/null +++ b/ci/custom_cops/bin/test @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require_relative "../../../tools/test" diff --git a/ci/custom_cops/lib/custom_cops.rb b/ci/custom_cops/lib/custom_cops.rb new file mode 100644 index 00000000000..d5d17f88568 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops.rb @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +require_relative "custom_cops/refute_not" diff --git a/ci/custom_cops/lib/custom_cops/refute_not.rb b/ci/custom_cops/lib/custom_cops/refute_not.rb new file mode 100644 index 00000000000..3e89e0fd328 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops/refute_not.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module CustomCops + # Enforces the use of `#assert_not` methods over `#refute` methods. + # + # @example + # # bad + # refute false + # refute_empty [1, 2, 3] + # refute_equal true, false + # + # # good + # assert_not false + # assert_not_empty [1, 2, 3] + # assert_not_equal true, false + # + class RefuteNot < RuboCop::Cop::Cop + MSG = "Prefer `%s` over `%s`" + + CORRECTIONS = { + refute: "assert_not", + refute_empty: "assert_not_empty", + refute_equal: "assert_not_equal", + refute_in_delta: "assert_not_in_delta", + refute_in_epsilon: "assert_not_in_epsilon", + refute_includes: "assert_not_includes", + refute_instance_of: "assert_not_instance_of", + refute_kind_of: "assert_not_kind_of", + refute_nil: "assert_not_nil", + refute_operator: "assert_not_operator", + refute_predicate: "assert_not_predicate", + refute_respond_to: "assert_not_respond_to", + refute_same: "assert_not_same", + refute_match: "assert_no_match" + }.freeze + + OFFENSIVE_METHODS = CORRECTIONS.keys.freeze + + def_node_matcher :offensive?, "(send nil? #offensive_method? ...)" + + def on_send(node) + return unless offensive?(node) + + message = offense_message(node.method_name) + add_offense(node, location: :selector, message: message) + end + + def autocorrect(node) + ->(corrector) do + corrector.replace( + node.loc.selector, + CORRECTIONS[node.method_name] + ) + end + end + + private + + def offensive_method?(method_name) + OFFENSIVE_METHODS.include?(method_name) + end + + def offense_message(method_name) + format( + MSG, + refute_method: method_name, + assert_method: CORRECTIONS[method_name] + ) + end + end +end diff --git a/ci/custom_cops/test/custom_cops/refute_not_test.rb b/ci/custom_cops/test/custom_cops/refute_not_test.rb new file mode 100644 index 00000000000..5dbd8bf32a3 --- /dev/null +++ b/ci/custom_cops/test/custom_cops/refute_not_test.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require "support/cop_helper" +require "./lib/custom_cops/refute_not" + +class RefuteNotTest < ActiveSupport::TestCase + include CopHelper + + setup do + @cop = CustomCops::RefuteNot.new + end + + { + refute: :assert_not, + refute_empty: :assert_not_empty, + refute_equal: :assert_not_equal, + refute_in_delta: :assert_not_in_delta, + refute_in_epsilon: :assert_not_in_epsilon, + refute_includes: :assert_not_includes, + refute_instance_of: :assert_not_instance_of, + refute_kind_of: :assert_not_kind_of, + refute_nil: :assert_not_nil, + refute_operator: :assert_not_operator, + refute_predicate: :assert_not_predicate, + refute_respond_to: :assert_not_respond_to, + refute_same: :assert_not_same, + refute_match: :assert_no_match + }.each do |refute_method, assert_method| + test "rejects `#{refute_method}` with a single argument" do + inspect_source(@cop, "#{refute_method} a") + assert_offense @cop, offense_message(refute_method, assert_method) + end + + test "rejects `#{refute_method}` with multiple arguments" do + inspect_source(@cop, "#{refute_method} a, b, c") + assert_offense @cop, offense_message(refute_method, assert_method) + end + + test "autocorrects `#{refute_method}` with a single argument" do + corrected = autocorrect_source(@cop, "#{refute_method} a") + assert_equal "#{assert_method} a", corrected + end + + test "autocorrects `#{refute_method}` with multiple arguments" do + corrected = autocorrect_source(@cop, "#{refute_method} a, b, c") + assert_equal "#{assert_method} a, b, c", corrected + end + + test "accepts `#{assert_method}` with a single argument" do + inspect_source(@cop, "#{assert_method} a") + assert_empty @cop.offenses + end + + test "accepts `#{assert_method}` with multiple arguments" do + inspect_source(@cop, "#{assert_method} a, b, c") + assert_empty @cop.offenses + end + end + + private + + def assert_offense(cop, expected_message) + assert_not_empty cop.offenses + + offense = cop.offenses.first + carets = "^" * offense.column_length + + assert_equal expected_message, "#{carets} #{offense.message}" + end + + def offense_message(refute_method, assert_method) + carets = "^" * refute_method.to_s.length + "#{carets} Prefer `#{assert_method}` over `#{refute_method}`" + end +end diff --git a/ci/custom_cops/test/support/cop_helper.rb b/ci/custom_cops/test/support/cop_helper.rb new file mode 100644 index 00000000000..d259154df56 --- /dev/null +++ b/ci/custom_cops/test/support/cop_helper.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "rubocop" + +module CopHelper + def inspect_source(cop, source) + processed_source = parse_source(source) + raise "Error parsing example code" unless processed_source.valid_syntax? + investigate(cop, processed_source) + processed_source + end + + def autocorrect_source(cop, source) + cop.instance_variable_get(:@options)[:auto_correct] = true + processed_source = inspect_source(cop, source) + rewrite(cop, processed_source) + end + + private + TARGET_RUBY_VERSION = 2.4 + + def parse_source(source) + RuboCop::ProcessedSource.new(source, TARGET_RUBY_VERSION) + end + + def rewrite(cop, processed_source) + RuboCop::Cop::Corrector.new(processed_source.buffer, cop.corrections) + .rewrite + end + + def investigate(cop, processed_source) + RuboCop::Cop::Commissioner.new([cop], [], raise_error: true) + .investigate(processed_source) + end +end From c1ceafc9d128b430f27d43d98369683c59c33021 Mon Sep 17 00:00:00 2001 From: Daniel Colson Date: Tue, 3 Apr 2018 21:34:51 -0400 Subject: [PATCH 2/2] Autocorrect `refute` RuboCop violations 73e7aab behaved as expected on codeship, failing the build with exactly these RuboCop violations. Hopefully `rubocop -a` will have been enough to get a passing build! --- actionpack/test/controller/integration_test.rb | 4 ++-- .../parameters/parameters_permit_test.rb | 10 +++++----- .../test/dispatch/system_testing/server_test.rb | 2 +- .../associations/has_one_associations_test.rb | 2 +- .../test/cases/associations/join_model_test.rb | 2 +- activerecord/test/cases/base_test.rb | 2 +- .../connection_adapters/connection_handler_test.rb | 4 ++-- activerecord/test/cases/migration_test.rb | 2 +- .../test/cases/multiparameter_attributes_test.rb | 2 +- activerecord/test/cases/schema_dumper_test.rb | 2 +- activerecord/test/cases/statement_cache_test.rb | 2 +- activerecord/test/cases/transactions_test.rb | 14 +++++++------- activesupport/test/dependencies_test.rb | 4 ++-- railties/test/application/rake_test.rb | 2 +- 14 files changed, 27 insertions(+), 27 deletions(-) diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index a685f5868eb..8a49d7822ee 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -345,7 +345,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest follow_redirect! assert_response :ok - refute_same previous_html_document, html_document + assert_not_same previous_html_document, html_document end end @@ -375,7 +375,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest a = open_session b = open_session - refute_same(a.integration_session, b.integration_session) + assert_not_same(a.integration_session, b.integration_session) end def test_get_with_query_string diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 295f3a03ef9..e60003dc641 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -309,7 +309,7 @@ class ParametersPermitTest < ActiveSupport::TestCase merged_params = @params.reverse_merge(default_params) assert_equal "1234", merged_params[:id] - refute_predicate merged_params[:person], :empty? + assert_not_predicate merged_params[:person], :empty? end test "#with_defaults is an alias of reverse_merge" do @@ -317,11 +317,11 @@ class ParametersPermitTest < ActiveSupport::TestCase merged_params = @params.with_defaults(default_params) assert_equal "1234", merged_params[:id] - refute_predicate merged_params[:person], :empty? + assert_not_predicate merged_params[:person], :empty? end test "not permitted is sticky beyond reverse_merge" do - refute_predicate @params.reverse_merge(a: "b"), :permitted? + assert_not_predicate @params.reverse_merge(a: "b"), :permitted? end test "permitted is sticky beyond reverse_merge" do @@ -334,7 +334,7 @@ class ParametersPermitTest < ActiveSupport::TestCase @params.reverse_merge!(default_params) assert_equal "1234", @params[:id] - refute_predicate @params[:person], :empty? + assert_not_predicate @params[:person], :empty? end test "#with_defaults! is an alias of reverse_merge!" do @@ -342,7 +342,7 @@ class ParametersPermitTest < ActiveSupport::TestCase @params.with_defaults!(default_params) assert_equal "1234", @params[:id] - refute_predicate @params[:person], :empty? + assert_not_predicate @params[:person], :empty? end test "modifying the parameters" do diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb index 95e411faf4f..740e90a4dad 100644 --- a/actionpack/test/dispatch/system_testing/server_test.rb +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -17,7 +17,7 @@ class ServerTest < ActiveSupport::TestCase test "server is changed from `default` to `puma`" do Capybara.server = :default ActionDispatch::SystemTesting::Server.new.run - refute_equal Capybara.server, Capybara.servers[:default] + assert_not_equal Capybara.server, Capybara.servers[:default] end test "server is not changed to `puma` when is different than default" do diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 602fe527017..b90edf9b130 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -678,7 +678,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase book = SpecialBook.create!(status: "published") author.book = book - refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count + assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end def test_association_enum_works_properly_with_nested_join diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index f19a9f5f7a4..57ebc1e3e00 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -369,7 +369,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase Tag.has_many :null_taggings, -> { none }, class_name: :Tagging Tag.has_many :null_tagged_posts, through: :null_taggings, source: "taggable", source_type: "Post" assert_equal [], tags(:general).null_tagged_posts - refute_equal [], tags(:general).tagged_posts + assert_not_equal [], tags(:general).tagged_posts end def test_eager_has_many_polymorphic_with_source_type diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7dfb05a6a5f..d5e1c2feb7d 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1497,7 +1497,7 @@ class BasicsTest < ActiveRecord::TestCase end test "column names are quoted when using #from clause and model has ignored columns" do - refute_empty Developer.ignored_columns + assert_not_empty Developer.ignored_columns query = Developer.from("developers").to_sql quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index f67c679faec..57b3a5844eb 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -328,7 +328,7 @@ module ActiveRecord pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) assert_same klass2.connection, pool.connection - refute_same klass2.connection, ActiveRecord::Base.connection + assert_not_same klass2.connection, ActiveRecord::Base.connection klass2.remove_connection @@ -347,7 +347,7 @@ module ActiveRecord def test_remove_connection_should_not_remove_parent klass2 = Class.new(Base) { def self.name; "klass2"; end } klass2.remove_connection - refute_nil ActiveRecord::Base.connection + assert_not_nil ActiveRecord::Base.connection assert_same klass2.connection, ActiveRecord::Base.connection end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f77a2755441..e3fd7d1a7b5 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -404,7 +404,7 @@ class MigrationTest < ActiveRecord::TestCase ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - refute_equal current_env, new_env + assert_not_equal current_env, new_env sleep 1 # mysql by default does not store fractional seconds in the database migrator.up diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index a24b173cf5f..6f3903eed4f 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -394,6 +394,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase "written_on(4i)" => "13", "written_on(5i)" => "55", ) - refute_predicate topic, :written_on_came_from_user? + assert_not_predicate topic, :written_on_came_from_user? end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 50d766a99ed..31bdf3f3571 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -469,7 +469,7 @@ class SchemaDumperTest < ActiveRecord::TestCase output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string assert_match %r{create_table "omg_cats"}, output - refute_match %r{create_table "cats"}, output + assert_no_match %r{create_table "cats"}, output ensure migration.migrate(:down) ActiveRecord::Base.table_name_prefix = original_table_name_prefix diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index ad6cd198e20..e3c12f68fda 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -102,7 +102,7 @@ module ActiveRecord Book.find_by(name: "my other book") end - refute_equal book, other_book + assert_not_equal book, other_book end def test_find_by_does_not_use_statement_cache_if_table_name_is_changed diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c70286d52a3..1c144a781d6 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -323,8 +323,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback @@ -344,8 +344,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_double_nested_transaction_applies_parent_state_on_rollback @@ -371,9 +371,9 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? - refute_predicate topic_three, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? + assert_not_predicate topic_three, :persisted? end def test_manually_rolling_back_a_transaction diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index c0c4c4cac52..2ca21f215e0 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -223,7 +223,7 @@ class DependenciesTest < ActiveSupport::TestCase Timeout.timeout(0.1) do # Remove the constant, as if Rails development middleware is reloading changed files: ActiveSupport::Dependencies.remove_unloadable_constants! - refute defined?(AnotherConstant::ReloadError) + assert_not defined?(AnotherConstant::ReloadError) end # Change the file, so that it is **correct** this time: @@ -231,7 +231,7 @@ class DependenciesTest < ActiveSupport::TestCase # Again: Remove the constant, as if Rails development middleware is reloading changed files: ActiveSupport::Dependencies.remove_unloadable_constants! - refute defined?(AnotherConstant::ReloadError) + assert_not defined?(AnotherConstant::ReloadError) # Now, reload the _fixed_ constant: assert ConstantReloadError diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 9683230d072..d94caf5fe0f 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -41,7 +41,7 @@ module ApplicationTests rails "db:create", "db:migrate" output = rails("db:test:prepare", "test") - refute_match(/ActiveRecord::ProtectedEnvironmentError/, output) + assert_no_match(/ActiveRecord::ProtectedEnvironmentError/, output) end end