From 1432383ab465fac36ffb4ba222a578b4d9751154 Mon Sep 17 00:00:00 2001 From: Ryo Nakamura Date: Fri, 8 Jul 2022 07:16:35 +0900 Subject: [PATCH] Add `RSpec/NoExpectationExample` In response to the review comments, I added the following changes later: - Prefer x_type? to is_a? - Use RSpec.Language.Examples - Add are_expected to expectation method names - Prefer on_block to on_send - Add more methods to RSpec.Language.Expectations - Use RSpec.Language.Expectations - Replace with clearer custom method names - Add some missing \@return comments --- .rubocop.yml | 13 ++ CHANGELOG.md | 1 + config/default.yml | 14 +- docs/modules/ROOT/pages/cops.adoc | 1 + docs/modules/ROOT/pages/cops_rspec.adoc | 45 +++++++ .../cop/rspec/no_expectation_example.rb | 90 +++++++++++++ lib/rubocop/cop/rspec_cops.rb | 1 + .../cop/rspec/no_expectation_example_spec.rb | 120 ++++++++++++++++++ 8 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 lib/rubocop/cop/rspec/no_expectation_example.rb create mode 100644 spec/rubocop/cop/rspec/no_expectation_example_spec.rb diff --git a/.rubocop.yml b/.rubocop.yml index d2feab940..a10b823c7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -72,6 +72,13 @@ Naming/InclusiveLanguage: Suggestions: - offense +RSpec: + Language: + Expectations: + - expect_correction + - expect_no_offenses + - expect_offense + RSpec/ExampleLength: CountAsOne: - heredoc @@ -81,6 +88,12 @@ RSpec/DescribeClass: Exclude: - spec/project/**/*.rb +RSpec/MultipleExpectations: + Max: 2 + +RSpec/NoExpectationExample: + Enabled: true + Style/FormatStringToken: Exclude: - spec/rubocop/**/*.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a16128d..c18539ee2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Add new `AllowConsecutiveOneLiners` (default true) option for `Rspec/EmptyLineAfterHook` cop. ([@ngouy][]) * Add autocorrect support for `RSpec/EmptyExampleGroup`. ([@r7kamura][]) +* Add `RSpec/NoExpectationExample`. ([@r7kamura][]) ## 2.12.1 (2022-07-03) diff --git a/config/default.yml b/config/default.yml index ce98cce21..937265b19 100644 --- a/config/default.yml +++ b/config/default.yml @@ -61,9 +61,14 @@ RSpec: Pending: - pending Expectations: + - are_expected - expect - - is_expected - expect_any_instance_of + - is_expected + - should + - should_not + - should_not_receive + - should_receive Helpers: - let - let! @@ -614,6 +619,13 @@ RSpec/NestedGroups: VersionChanged: '1.10' Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NestedGroups +RSpec/NoExpectationExample: + Description: Checks if an example includes any expectation. + Enabled: pending + Safe: false + VersionAdded: '2.13' + Reference: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NoExpectationExample + RSpec/NotToNot: Description: Checks for consistent method usage for negating expectations. Enabled: true diff --git a/docs/modules/ROOT/pages/cops.adoc b/docs/modules/ROOT/pages/cops.adoc index 120de28e8..d197f1e61 100644 --- a/docs/modules/ROOT/pages/cops.adoc +++ b/docs/modules/ROOT/pages/cops.adoc @@ -61,6 +61,7 @@ * xref:cops_rspec.adoc#rspecmultiplesubjects[RSpec/MultipleSubjects] * xref:cops_rspec.adoc#rspecnamedsubject[RSpec/NamedSubject] * xref:cops_rspec.adoc#rspecnestedgroups[RSpec/NestedGroups] +* xref:cops_rspec.adoc#rspecnoexpectationexample[RSpec/NoExpectationExample] * xref:cops_rspec.adoc#rspecnottonot[RSpec/NotToNot] * xref:cops_rspec.adoc#rspecoverwritingsetup[RSpec/OverwritingSetup] * xref:cops_rspec.adoc#rspecpending[RSpec/Pending] diff --git a/docs/modules/ROOT/pages/cops_rspec.adoc b/docs/modules/ROOT/pages/cops_rspec.adoc index ce6b17f51..f011099c8 100644 --- a/docs/modules/ROOT/pages/cops_rspec.adoc +++ b/docs/modules/ROOT/pages/cops_rspec.adoc @@ -3284,6 +3284,51 @@ end * https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NestedGroups +== RSpec/NoExpectationExample + +|=== +| Enabled by default | Safe | Supports autocorrection | Version Added | Version Changed + +| Pending +| No +| Yes (Unsafe) +| 2.13 +| - +|=== + +Checks if an example includes any expectation. + +All RSpec's example and expectation methods are covered by default. +If you are using your own custom methods, +add the following configuration: + + RSpec: + Language: + Examples: + Regular: + - custom_it + Expectations: + - custom_expect + +=== Examples + +[source,ruby] +---- +# bad +it do + a? +end + +# good +it do + expect(a?).to be_truthy +end +---- + +=== References + +* https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/NoExpectationExample + == RSpec/NotToNot |=== diff --git a/lib/rubocop/cop/rspec/no_expectation_example.rb b/lib/rubocop/cop/rspec/no_expectation_example.rb new file mode 100644 index 000000000..a416b7693 --- /dev/null +++ b/lib/rubocop/cop/rspec/no_expectation_example.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Checks if an example includes any expectation. + # + # All RSpec's example and expectation methods are covered by default. + # If you are using your own custom methods, + # add the following configuration: + # + # RSpec: + # Language: + # Examples: + # Regular: + # - custom_it + # Expectations: + # - custom_expect + # + # @example + # + # # bad + # it do + # a? + # end + # + # # good + # it do + # expect(a?).to be_truthy + # end + class NoExpectationExample < Base + extend AutoCorrector + + include RangeHelp + + MSG = 'No expectation found in this example.' + + # @!method expectation_method_call?(node) + # @param [RuboCop::AST::Node] node + # @return [Boolean] + def_node_matcher( + :expectation_method_call?, + send_pattern('#Expectations.all') + ) + + # @param [RuboCop::AST::BlockNode] node + def on_block(node) + return unless example_method_call?(node) + return if including_any_expectation?(node) + + add_offense(node) do |corrector| + corrector.remove(removed_range(node)) + end + end + + private + + # @param [RuboCop::AST::BlockNode] node + # @return [Boolean] + def example_method_call?(node) + Examples.all(node.method_name) + end + + # Recursively checks if the given node includes any expectation. + # @param [RuboCop::AST::Node] node + # @return [Boolean] + def including_any_expectation?(node) + if !node.is_a?(::RuboCop::AST::Node) + false + elsif expectation_method_call?(node) + true + else + node.children.any? do |child| + including_any_expectation?(child) + end + end + end + + # @param [RuboCop::AST::Node] node + # @return [Parser::Source::Range] + def removed_range(node) + range_by_whole_lines( + node.location.expression, + include_final_newline: true + ) + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec_cops.rb b/lib/rubocop/cop/rspec_cops.rb index 3ce86db8b..dd28df88e 100644 --- a/lib/rubocop/cop/rspec_cops.rb +++ b/lib/rubocop/cop/rspec_cops.rb @@ -77,6 +77,7 @@ require_relative 'rspec/multiple_subjects' require_relative 'rspec/named_subject' require_relative 'rspec/nested_groups' +require_relative 'rspec/no_expectation_example' require_relative 'rspec/not_to_not' require_relative 'rspec/overwriting_setup' require_relative 'rspec/pending' diff --git a/spec/rubocop/cop/rspec/no_expectation_example_spec.rb b/spec/rubocop/cop/rspec/no_expectation_example_spec.rb new file mode 100644 index 000000000..5e156d602 --- /dev/null +++ b/spec/rubocop/cop/rspec/no_expectation_example_spec.rb @@ -0,0 +1,120 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpec::NoExpectationExample do + context 'with empty example with it' do + it 'registers an offense' do + expect_offense(<<~RUBY) + RSpec.describe Foo do + it { bar } + ^^^^^^^^^^ No expectation found in this example. + + it { expect(baz).to be_truthy } + end + RUBY + + expect_correction(<<~RUBY) + RSpec.describe Foo do + + it { expect(baz).to be_truthy } + end + RUBY + end + end + + context 'with empty example with specify' do + it 'registers an offense' do + expect_offense(<<~RUBY) + specify { bar } + ^^^^^^^^^^^^^^^ No expectation found in this example. + RUBY + + expect_correction(<<~RUBY) + RUBY + end + end + + context 'with non-empty example with should' do + it 'registers no offenses' do + expect_no_offenses(<<~RUBY) + it { should be_truthy } + RUBY + end + end + + context 'with empty examples' do + it 'registers offenses' do + expect_offense(<<~RUBY) + it { bar } + ^^^^^^^^^^ No expectation found in this example. + it { baz } + ^^^^^^^^^^ No expectation found in this example. + RUBY + + expect_correction(<<~RUBY) + RUBY + end + end + + context 'with non-empty example with custom expectation' do + it 'registers an offense' do + expect_offense(<<~RUBY) + it { custom_expect(bar) } + ^^^^^^^^^^^^^^^^^^^^^^^^^ No expectation found in this example. + RUBY + + expect_correction(<<~RUBY) + RUBY + end + end + + context 'with non-empty example with configured custom expectation' do + before do + other_cops.dig('RSpec', 'Language', 'Expectations').push('custom_expect') + end + + it 'registers no offenses' do + expect_no_offenses(<<~RUBY) + it { custom_expect(bar) } + RUBY + end + end + + context 'with empty example with custom example method' do + it 'registers no offenses' do + expect_no_offenses(<<~RUBY) + custom_it { foo } + RUBY + end + end + + context 'with empty example with configured custom example method' do + before do + other_cops.dig( + 'RSpec', + 'Language', + 'Examples', + 'Regular' + ).push('custom_it') + end + + it 'registers an offense' do + expect_offense(<<~RUBY) + custom_it { foo } + ^^^^^^^^^^^^^^^^^ No expectation found in this example. + RUBY + + expect_correction(<<~RUBY) + RUBY + end + end + + context 'with block-less example in block' do + it 'registers no offenses' do + expect_no_offenses(<<~RUBY) + RSpec.describe Foo do + pending 'not implemented' + end + RUBY + end + end +end