diff --git a/.rubocop.yml b/.rubocop.yml index 9acc4e2af..bec0dfbba 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -171,6 +171,8 @@ RSpec/MatchArray: Enabled: true RSpec/MetadataStyle: Enabled: true +RSpec/NegatedExpectation: + Enabled: true RSpec/NoExpectationExample: Enabled: true RSpec/PendingWithoutReason: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c008527b..b4c308866 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Master (Unreleased) +- Add new `RSpec/NegatedExpectation` cop. ([@ydah]) + ## 2.27.1 (2024-03-03) - Fix a false positive for `RSpec/RepeatedSubjectCall` when `subject.method_call`. ([@ydah]) diff --git a/config/default.yml b/config/default.yml index 7a0fc924e..2f99243d5 100644 --- a/config/default.yml +++ b/config/default.yml @@ -692,6 +692,13 @@ RSpec/NamedSubject: StyleGuide: https://rspec.rubystyle.guide/#use-subject Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NamedSubject +RSpec/NegatedExpectation: + Description: Checks for expectations with `!`. + Enabled: pending + SafeAutoCorrect: false + VersionAdded: "<>" + Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NegatedExpectation + RSpec/NestedGroups: Description: Checks for nested example groups. Enabled: true diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 4dfed17f0..9e6ededaf 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -70,6 +70,7 @@ * xref:cops_rspec.adoc#rspecmultiplememoizedhelpers[RSpec/MultipleMemoizedHelpers] * xref:cops_rspec.adoc#rspecmultiplesubjects[RSpec/MultipleSubjects] * xref:cops_rspec.adoc#rspecnamedsubject[RSpec/NamedSubject] +* xref:cops_rspec.adoc#rspecnegatedexpectation[RSpec/NegatedExpectation] * xref:cops_rspec.adoc#rspecnestedgroups[RSpec/NestedGroups] * xref:cops_rspec.adoc#rspecnoexpectationexample[RSpec/NoExpectationExample] * xref:cops_rspec.adoc#rspecnottonot[RSpec/NotToNot] diff --git a/docs/modules/ROOT/pages/cops_rspec.adoc b/docs/modules/ROOT/pages/cops_rspec.adoc index d66acd99b..681b2bd16 100644 --- a/docs/modules/ROOT/pages/cops_rspec.adoc +++ b/docs/modules/ROOT/pages/cops_rspec.adoc @@ -3888,6 +3888,40 @@ end * https://rspec.rubystyle.guide/#use-subject * https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NamedSubject +== RSpec/NegatedExpectation + +|=== +| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed + +| Pending +| Yes +| Always (Unsafe) +| <> +| - +|=== + +Checks for expectations with `!`. + +=== Safety + +The autocorrection is marked as unsafe, because it may change the +expectation from a positive to a negative one, or vice versa. + +=== Examples + +[source,ruby] +---- +# bad +!expect(foo).to be_valid + +# good +expect(foo).not_to be_valid +---- + +=== References + +* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NegatedExpectation + == RSpec/NestedGroups |=== diff --git a/lib/rubocop/cop/rspec/negated_expectation.rb b/lib/rubocop/cop/rspec/negated_expectation.rb new file mode 100644 index 000000000..da86b54a6 --- /dev/null +++ b/lib/rubocop/cop/rspec/negated_expectation.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Checks for expectations with `!`. + # + # @safety + # The autocorrection is marked as unsafe, because it may change the + # expectation from a positive to a negative one, or vice versa. + # + # @example + # # bad + # !expect(foo).to be_valid + # + # # good + # expect(foo).not_to be_valid + # + class NegatedExpectation < Base + extend AutoCorrector + + MSG = 'Use `expect(...).%s` instead of ' \ + '`!expect(...)`.%s' + RESTRICT_ON_SEND = Runners.all + + def on_send(node) + return unless node.parent&.send_type? + return unless node.parent.method?(:!) + + replaced = replaced(node) + add_offense(node.parent, + message: message(node, replaced)) do |corrector| + corrector.remove(node.parent.loc.selector) + corrector.replace(node.loc.selector, replaced) + end + end + + private + + def message(node, replaced) + format(MSG, runner: node.loc.selector.source, replaced: replaced) + end + + def replaced(node) + runner = node.loc.selector.source + runner == 'to' ? 'not_to' : 'to' + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec_cops.rb b/lib/rubocop/cop/rspec_cops.rb index a1a4b45f9..4aa596256 100644 --- a/lib/rubocop/cop/rspec_cops.rb +++ b/lib/rubocop/cop/rspec_cops.rb @@ -96,6 +96,7 @@ require_relative 'rspec/multiple_memoized_helpers' require_relative 'rspec/multiple_subjects' require_relative 'rspec/named_subject' +require_relative 'rspec/negated_expectation' require_relative 'rspec/nested_groups' require_relative 'rspec/no_expectation_example' require_relative 'rspec/not_to_not' diff --git a/spec/rubocop/cop/rspec/negated_expectations_spec.rb b/spec/rubocop/cop/rspec/negated_expectations_spec.rb new file mode 100644 index 000000000..467d90bf8 --- /dev/null +++ b/spec/rubocop/cop/rspec/negated_expectations_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpec::NegatedExpectation, :config do + it 'registers an offense when using redundant negation with `.to`' do + expect_offense(<<~RUBY) + !expect(foo).to be_valid + ^^^^^^^^^^^^^^^^^^^^^^^^ Use `expect(...).not_to` instead of `!expect(...)`.to + RUBY + + expect_correction(<<~RUBY) + expect(foo).not_to be_valid + RUBY + end + + it 'registers an offense when using redundant negation with `.not_to`' do + expect_offense(<<~RUBY) + !expect(foo).not_to be_valid + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `expect(...).to` instead of `!expect(...)`.not_to + RUBY + + expect_correction(<<~RUBY) + expect(foo).to be_valid + RUBY + end + + it 'registers an offense when using redundant negation with `.to_not`' do + expect_offense(<<~RUBY) + !expect(foo).to_not be_valid + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use `expect(...).to` instead of `!expect(...)`.to_not + RUBY + + expect_correction(<<~RUBY) + expect(foo).to be_valid + RUBY + end + + it 'does not register an offense when without redundant negation' do + expect_no_offenses(<<~RUBY) + expect(foo).not_to be_valid + RUBY + end +end