diff --git a/.simplecov b/.simplecov index 75f87802f..c09a9b47f 100644 --- a/.simplecov +++ b/.simplecov @@ -2,7 +2,7 @@ SimpleCov.start do enable_coverage :branch - minimum_coverage line: 99.60, branch: 94.84 + minimum_coverage line: 99.60, branch: 94.77 add_filter '/spec/' add_filter '/vendor/bundle/' end diff --git a/CHANGELOG.md b/CHANGELOG.md index a816a7f98..6b7a82e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Master (Unreleased) +- Add new `RSpec/Rails/NegationBeValid` cop. ([@ydah]) - Fix a false negative for `RSpec/ExcessiveDocstringSpacing` when finds description with em space. ([@ydah]) - Fix a false positive for `RSpec/EmptyExampleGroup` when example group with examples defined in `if` branch inside iterator. ([@ydah]) - Update the message output of `RSpec/ExpectActual` to include the word 'value'. ([@corydiamand]) diff --git a/config/default.yml b/config/default.yml index 24abac61b..10763e1e4 100644 --- a/config/default.yml +++ b/config/default.yml @@ -1104,6 +1104,16 @@ RSpec/Rails/MinitestAssertions: VersionAdded: '2.17' Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/MinitestAssertions +RSpec/Rails/NegationBeValid: + Description: Enforces use of `be_invalid` or `not_to` for negated be_valid. + EnforcedStyle: not_to + SupportedStyles: + - not_to + - be_invalid + Enabled: pending + VersionAdded: "<>" + Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/NegationBeValid + RSpec/Rails/TravelAround: Description: Prefer to travel in `before` rather than `around`. Enabled: pending diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 42b9fdd78..acdfa8a4c 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -128,6 +128,7 @@ * xref:cops_rspec_rails.adoc#rspecrails/httpstatus[RSpec/Rails/HttpStatus] * xref:cops_rspec_rails.adoc#rspecrails/inferredspectype[RSpec/Rails/InferredSpecType] * xref:cops_rspec_rails.adoc#rspecrails/minitestassertions[RSpec/Rails/MinitestAssertions] +* xref:cops_rspec_rails.adoc#rspecrails/negationbevalid[RSpec/Rails/NegationBeValid] * xref:cops_rspec_rails.adoc#rspecrails/travelaround[RSpec/Rails/TravelAround] // END_COP_LIST diff --git a/docs/modules/ROOT/pages/cops_rspec_rails.adoc b/docs/modules/ROOT/pages/cops_rspec_rails.adoc index 484aa8020..29d94410f 100644 --- a/docs/modules/ROOT/pages/cops_rspec_rails.adoc +++ b/docs/modules/ROOT/pages/cops_rspec_rails.adoc @@ -261,6 +261,58 @@ expect(b).not_to eq(a) * https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/MinitestAssertions +== RSpec/Rails/NegationBeValid + +|=== +| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed + +| Pending +| Yes +| Yes +| <> +| - +|=== + +Enforces use of `be_invalid` or `not_to` for negated be_valid. + +=== Examples + +==== EnforcedStyle: not_to (default) + +[source,ruby] +---- +# bad +expect(foo).to be_invalid + +# good +expect(foo).not_to be_valid +---- + +==== EnforcedStyle: be_invalid + +[source,ruby] +---- +# bad +expect(foo).not_to be_valid + +# good +expect(foo).to be_invalid +---- + +=== Configurable attributes + +|=== +| Name | Default value | Configurable values + +| EnforcedStyle +| `not_to` +| `not_to`, `be_invalid` +|=== + +=== References + +* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/Rails/NegationBeValid + == RSpec/Rails/TravelAround |=== diff --git a/lib/rubocop/cop/rspec/rails/negation_be_valid.rb b/lib/rubocop/cop/rspec/rails/negation_be_valid.rb new file mode 100644 index 000000000..21ef6495d --- /dev/null +++ b/lib/rubocop/cop/rspec/rails/negation_be_valid.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + module Rails + # Enforces use of `be_invalid` or `not_to` for negated be_valid. + # + # @example EnforcedStyle: not_to (default) + # # bad + # expect(foo).to be_invalid + # + # # good + # expect(foo).not_to be_valid + # + # @example EnforcedStyle: be_invalid + # # bad + # expect(foo).not_to be_valid + # + # # good + # expect(foo).to be_invalid + # + class NegationBeValid < Base + extend AutoCorrector + include ConfigurableEnforcedStyle + + MSG = 'Use `expect(...).%s %s`.' + RESTRICT_ON_SEND = %i[be_valid be_invalid].freeze + + # @!method not_to?(node) + def_node_matcher :not_to?, <<~PATTERN + (send ... :not_to (send nil? :be_valid ...)) + PATTERN + + # @!method be_invalid?(node) + def_node_matcher :be_invalid?, <<~PATTERN + (send ... :to (send nil? :be_invalid ...)) + PATTERN + + def on_send(node) + return unless offense?(node.parent) + + add_offense(offense_range(node), + message: message(node.method_name)) do |corrector| + corrector.replace(node.parent.loc.selector, replaced_runner) + corrector.replace(node.loc.selector, replaced_matcher) + end + end + + private + + def offense?(node) + case style + when :not_to + be_invalid?(node) + when :be_invalid + not_to?(node) + end + end + + def offense_range(node) + node.parent.loc.selector.with(end_pos: node.loc.selector.end_pos) + end + + def message(_matcher) + format(MSG, + runner: replaced_runner, + matcher: replaced_matcher) + end + + def replaced_runner + case style + when :not_to + 'not_to' + when :be_invalid + 'to' + end + end + + def replaced_matcher + case style + when :not_to + 'be_valid' + when :be_invalid + 'be_invalid' + end + end + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec_cops.rb b/lib/rubocop/cop/rspec_cops.rb index c3779e4a3..6edc2610e 100644 --- a/lib/rubocop/cop/rspec_cops.rb +++ b/lib/rubocop/cop/rspec_cops.rb @@ -18,6 +18,7 @@ require_relative 'rspec/rails/avoid_setup_hook' require_relative 'rspec/rails/have_http_status' +require_relative 'rspec/rails/negation_be_valid' begin require_relative 'rspec/rails/http_status' rescue LoadError diff --git a/spec/rubocop/cop/rspec/rails/negation_be_valid_spec.rb b/spec/rubocop/cop/rspec/rails/negation_be_valid_spec.rb new file mode 100644 index 000000000..e5ad05e8d --- /dev/null +++ b/spec/rubocop/cop/rspec/rails/negation_be_valid_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpec::Rails::NegationBeValid do + let(:cop_config) { { 'EnforcedStyle' => enforced_style } } + + context 'with EnforcedStyle `not_to`' do + let(:enforced_style) { 'not_to' } + + it 'registers an offense when using ' \ + '`expect(...).to be_invalid`' do + expect_offense(<<~RUBY) + expect(foo).to be_invalid + ^^^^^^^^^^^^^ Use `expect(...).not_to be_valid`. + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(...).not_to be_valid`' do + expect_no_offenses(<<~RUBY) + expect(foo).not_to be_valid + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(...).to be_valid`' do + expect_no_offenses(<<~RUBY) + expect(foo).to be_valid + RUBY + end + end + + context 'with EnforcedStyle `be_invalid`' do + let(:enforced_style) { 'be_invalid' } + + it 'registers an offense when using ' \ + '`expect(...).not_to be_valid`' do + expect_offense(<<~RUBY) + expect(foo).not_to be_valid + ^^^^^^^^^^^^^^^ Use `expect(...).to be_invalid`. + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(...).to be_invalid`' do + expect_no_offenses(<<~RUBY) + expect(foo).to be_invalid + RUBY + end + + it 'does not register an offense when using ' \ + '`expect(...).to be_valid`' do + expect_no_offenses(<<~RUBY) + expect(foo).to be_valid + RUBY + end + end +end