From 32d2e2fc71dd730349cc6d0c8c3d7081f2f37bef Mon Sep 17 00:00:00 2001 From: Ulugbek Tuychiev Date: Thu, 16 Jan 2020 13:33:51 +0500 Subject: [PATCH] implement example group repeated description\body cops --- CHANGELOG.md | 3 + config/default.yml | 10 + .../cop/rspec/repeated_example_group_body.rb | 87 ++++++ .../repeated_example_group_description.rb | 96 +++++++ lib/rubocop/cop/rspec_cops.rb | 2 + manual/cops.md | 2 + manual/cops_rspec.md | 95 +++++++ .../rspec/repeated_example_group_body_spec.rb | 254 ++++++++++++++++++ ...repeated_example_group_description_spec.rb | 244 +++++++++++++++++ 9 files changed, 793 insertions(+) create mode 100644 lib/rubocop/cop/rspec/repeated_example_group_body.rb create mode 100644 lib/rubocop/cop/rspec/repeated_example_group_description.rb create mode 100644 spec/rubocop/cop/rspec/repeated_example_group_body_spec.rb create mode 100644 spec/rubocop/cop/rspec/repeated_example_group_description_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index aa827e6c5..a8d0a0438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * Fix `RSpec/InstanceVariable` detection inside custom matchers. ([@pirj][]) * Fix `RSpec/ScatteredSetup` to distinguish hooks with different metadata. ([@pirj][]) * Add autocorrect support for `RSpec/ExpectActual` cop. ([@dduugg][], [@pirj][]) +* Add `RSpec/RepeatedExampleGroupBody` cop. ([@lazycoder9][]) +* Add `RSpec/RepeatedExampleGroupDescription` cop. ([@lazycoder9][]) ## 1.37.1 (2019-12-16) @@ -477,3 +479,4 @@ Compatibility release so users can upgrade RuboCop to 0.51.0. No new features. [@jfragoulis]: https://github.com/jfragoulis [@ybiquitous]: https://github.com/ybiquitous [@dduugg]: https://github.com/dduugg +[@lazycoder9]: https://github.com/lazycoder9 diff --git a/config/default.yml b/config/default.yml index 25f7cef4f..739e3afd4 100644 --- a/config/default.yml +++ b/config/default.yml @@ -394,6 +394,16 @@ RSpec/RepeatedExample: Description: Check for repeated examples within example groups. StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExample +RSpec/RepeatedExampleGroupBody: + Enabled: true + Description: Check for repeated describe and context block body. + StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExampleGroupBody + +RSpec/RepeatedExampleGroupDescription: + Enabled: true + Description: Check for repeated example group descriptions. + StyleGuide: https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExampleGroupDescription + RSpec/ReturnFromStub: Enabled: true Description: Checks for consistent style of stub's return setting. diff --git a/lib/rubocop/cop/rspec/repeated_example_group_body.rb b/lib/rubocop/cop/rspec/repeated_example_group_body.rb new file mode 100644 index 000000000..ab55a509f --- /dev/null +++ b/lib/rubocop/cop/rspec/repeated_example_group_body.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Check for repeated describe and context block body. + # + # @example + # + # # bad + # describe 'cool feature x' do + # it { cool_predicate } + # end + # + # describe 'cool feature y' do + # it { cool_predicate } + # end + # + # # good + # describe 'cool feature' do + # it { cool_predicate } + # end + # + # describe 'another cool feature' do + # it { another_predicate } + # end + # + # # good + # context 'when case x', :tag do + # it { cool_predicate } + # end + # + # context 'when case y' do + # it { cool_predicate } + # end + # + class RepeatedExampleGroupBody < Cop + MSG = 'Repeated %s block body on line(s) %s' + + def_node_matcher :several_example_groups?, <<-PATTERN + (begin <#example_group_with_body? #example_group_with_body? ...>) + PATTERN + + def_node_matcher :metadata, '(block (send _ _ _ $...) ...)' + def_node_matcher :body, '(block _ args $...)' + + def_node_matcher :skip_or_pending?, <<-PATTERN + (block <(send nil? {:skip :pending}) ...>) + PATTERN + + def on_begin(node) + return unless several_example_groups?(node) + + repeated_group_bodies(node).each do |group, repeats| + add_offense(group, message: message(group, repeats)) + end + end + + private + + def repeated_group_bodies(node) + node + .children + .select { |child| example_group_with_body?(child) } + .reject { |child| skip_or_pending?(child) } + .group_by { |group| signature_keys(group) } + .values + .reject(&:one?) + .flat_map { |groups| add_repeated_lines(groups) } + end + + def add_repeated_lines(groups) + repeated_lines = groups.map(&:first_line) + groups.map { |group| [group, repeated_lines - [group.first_line]] } + end + + def signature_keys(group) + [metadata(group), body(group)] + end + + def message(group, repeats) + format(MSG, group: group.method_name, loc: repeats) + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec/repeated_example_group_description.rb b/lib/rubocop/cop/rspec/repeated_example_group_description.rb new file mode 100644 index 000000000..17da2b5cf --- /dev/null +++ b/lib/rubocop/cop/rspec/repeated_example_group_description.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Check for repeated example group descriptions. + # + # @example + # + # # bad + # describe 'cool feature' do + # # example group + # end + # + # describe 'cool feature' do + # # example group + # end + # + # # bad + # context 'when case x' do + # # example group + # end + # + # describe 'when case x' do + # # example group + # end + # + # # good + # describe 'cool feature' do + # # example group + # end + # + # describe 'another cool feature' do + # # example group + # end + # + # # good + # context 'when case x' do + # # example group + # end + # + # context 'when another case' do + # # example group + # end + # + class RepeatedExampleGroupDescription < Cop + MSG = 'Repeated %s block description on line(s) %s' + + def_node_matcher :several_example_groups?, <<-PATTERN + (begin <#example_group? #example_group? ...>) + PATTERN + + def_node_matcher :doc_string_and_metadata, <<-PATTERN + (block (send _ _ $_ $...) ...) + PATTERN + + def_node_matcher :skip_or_pending?, <<-PATTERN + (block <(send nil? {:skip :pending}) ...>) + PATTERN + + def_node_matcher :empty_description?, '(block (send _ _) ...)' + + def on_begin(node) + return unless several_example_groups?(node) + + repeated_group_descriptions(node).each do |group, repeats| + add_offense(group, message: message(group, repeats)) + end + end + + private + + def repeated_group_descriptions(node) + node + .children + .select { |child| example_group?(child) } + .reject { |child| skip_or_pending?(child) } + .reject { |child| empty_description?(child) } + .group_by { |group| doc_string_and_metadata(group) } + .values + .reject(&:one?) + .flat_map { |groups| add_repeated_lines(groups) } + end + + def add_repeated_lines(groups) + repeated_lines = groups.map(&:first_line) + groups.map { |group| [group, repeated_lines - [group.first_line]] } + end + + def message(group, repeats) + format(MSG, group: group.method_name, loc: repeats) + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec_cops.rb b/lib/rubocop/cop/rspec_cops.rb index 309dfc6d8..5f2a1719b 100644 --- a/lib/rubocop/cop/rspec_cops.rb +++ b/lib/rubocop/cop/rspec_cops.rb @@ -74,6 +74,8 @@ require_relative 'rspec/receive_never' require_relative 'rspec/repeated_description' require_relative 'rspec/repeated_example' +require_relative 'rspec/repeated_example_group_body' +require_relative 'rspec/repeated_example_group_description' require_relative 'rspec/return_from_stub' require_relative 'rspec/scattered_let' require_relative 'rspec/scattered_setup' diff --git a/manual/cops.md b/manual/cops.md index 67c45ce2e..a5a62faad 100644 --- a/manual/cops.md +++ b/manual/cops.md @@ -73,6 +73,8 @@ * [RSpec/ReceiveNever](cops_rspec.md#rspecreceivenever) * [RSpec/RepeatedDescription](cops_rspec.md#rspecrepeateddescription) * [RSpec/RepeatedExample](cops_rspec.md#rspecrepeatedexample) +* [RSpec/RepeatedExampleGroupBody](cops_rspec.md#rspecrepeatedexamplegroupbody) +* [RSpec/RepeatedExampleGroupDescription](cops_rspec.md#rspecrepeatedexamplegroupdescription) * [RSpec/ReturnFromStub](cops_rspec.md#rspecreturnfromstub) * [RSpec/ScatteredLet](cops_rspec.md#rspecscatteredlet) * [RSpec/ScatteredSetup](cops_rspec.md#rspecscatteredsetup) diff --git a/manual/cops_rspec.md b/manual/cops_rspec.md index 1aec29405..c95c1d213 100644 --- a/manual/cops_rspec.md +++ b/manual/cops_rspec.md @@ -2552,6 +2552,101 @@ end * [https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExample](https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExample) +## RSpec/RepeatedExampleGroupBody + +Enabled by default | Supports autocorrection +--- | --- +Enabled | No + +Check for repeated describe and context block body. + +### Examples + +```ruby +# bad +describe 'cool feature x' do + it { cool_predicate } +end + +describe 'cool feature y' do + it { cool_predicate } +end + +# good +describe 'cool feature' do + it { cool_predicate } +end + +describe 'another cool feature' do + it { another_predicate } +end + +# good +context 'when case x', :tag do + it { cool_predicate } +end + +context 'when case y' do + it { cool_predicate } +end +``` + +### References + +* [https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExampleGroupBody](https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExampleGroupBody) + +## RSpec/RepeatedExampleGroupDescription + +Enabled by default | Supports autocorrection +--- | --- +Enabled | No + +Check for repeated example group descriptions. + +### Examples + +```ruby +# bad +describe 'cool feature' do + # example group +end + +describe 'cool feature' do + # example group +end + +# bad +context 'when case x' do + # example group +end + +describe 'when case x' do + # example group +end + +# good +describe 'cool feature' do + # example group +end + +describe 'another cool feature' do + # example group +end + +# good +context 'when case x' do + # example group +end + +context 'when another case' do + # example group +end +``` + +### References + +* [https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExampleGroupDescription](https://www.rubydoc.info/gems/rubocop-rspec/RuboCop/Cop/RSpec/RepeatedExampleGroupDescription) + ## RSpec/ReturnFromStub Enabled by default | Supports autocorrection diff --git a/spec/rubocop/cop/rspec/repeated_example_group_body_spec.rb b/spec/rubocop/cop/rspec/repeated_example_group_body_spec.rb new file mode 100644 index 000000000..749272afc --- /dev/null +++ b/spec/rubocop/cop/rspec/repeated_example_group_body_spec.rb @@ -0,0 +1,254 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpec::RepeatedExampleGroupBody do + subject(:cop) { described_class.new } + + it 'registers an offense for repeated describe body' do + expect_offense(<<-RUBY) + describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [5] + it { cool_predicate_method } + end + + describe 'doing y' do + ^^^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [1] + it { cool_predicate_method } + end + RUBY + end + + it 'registers an offense for repeated context body' do + expect_offense(<<-RUBY) + context 'when awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [5] + it { cool_predicate_method } + end + + context 'when another awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [1] + it { cool_predicate_method } + end + RUBY + end + + it 'registers an offense for several repeated context body' do + expect_offense(<<-RUBY) + context 'when usual case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [5, 9] + it { cool_predicate_method } + end + + context 'when awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [1, 9] + it { cool_predicate_method } + end + + context 'when another awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [1, 5] + it { cool_predicate_method } + end + RUBY + end + + it 'does not register offense for different block body implementation' do + expect_no_offenses(<<-RUBY) + context 'when awesome case' do + it { cool_predicate_method } + end + + context 'when another awesome case' do + it { another_predicate_method } + end + RUBY + end + + it 'does not register offense if metadata is different' do + expect_no_offenses(<<-RUBY) + describe 'doing x' do + it { cool_predicate_method } + end + + describe 'doing x', :request do + it { cool_predicate_method } + end + RUBY + end + + it 'does not register offense with several docstring' do + expect_no_offenses(<<-RUBY) + describe 'doing x', :json, 'request' do + it { cool_predicate_method } + end + + describe 'doing x', 'request' do + it { cool_predicate_method } + end + RUBY + end + + it 'registers offense for different groups' do + expect_offense(<<-RUBY) + describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [5] + it { cool_predicate_method } + end + + context 'when a is true' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [1] + it { cool_predicate_method } + end + RUBY + end + + it 'does not register offense for example groups in different groups' do + expect_no_offenses(<<-RUBY) + describe 'A' do + describe '.b' do + context 'when this' do + it { do_something } + end + end + context 'when this' do + it { do_something } + end + end + RUBY + end + + it 'registers offense only for RSPEC namespace example groups' do + expect_offense(<<-RUBY) + helpers.describe 'doing x' do + it { cool_predicate_method } + end + + RSpec.describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [9] + it { cool_predicate_method } + end + + context 'when a is true' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [5] + it { cool_predicate_method } + end + RUBY + end + + it 'registers offense only for RSPEC namespace example groups in any order' do + expect_offense(<<-RUBY) + RSpec.describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [5] + it { cool_predicate_method } + end + + context 'when a is true' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block body on line(s) [1] + it { cool_predicate_method } + end + + helpers.describe 'doing x' do + it { cool_predicate_method } + end + RUBY + end + + it 'registers offense only for example group' do + expect_offense(<<-RUBY) + RSpec.describe 'A' do + stub_all_http_calls() + allowed_statuses = %i[open submitted approved].freeze + before { create(:admin) } + allowed_statuses = %i[open submitted approved].freeze + + describe '#load' do + ^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [11] + it { cool_predicate_method } + end + + describe '#load' do + ^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [7] + it { cool_predicate_method } + end + end + RUBY + end + + it 'skips `skip` and `pending` statements' do + expect_no_offenses(<<-RUBY) + context 'rejected' do + skip + end + + context 'processed' do + skip + end + + context 'processed' do + pending + end + RUBY + end + + it 'registers offense correctly if example groups are separated' do + expect_offense(<<-RUBY) + describe 'repeated' do + ^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [7] + it { is_expected.to be_truthy } + end + + before { do_something } + + describe 'this is repeated' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block body on line(s) [1] + it { is_expected.to be_truthy } + end + RUBY + end + + it 'does not register offense for same examples with different data' do + expect_no_offenses(<<-RUBY) + context 'when admin' do + let(:user) { admin } + it { is_expected.to be_truthy } + end + + context 'when regular user' do + let(:user) { staff } + it { is_expected.to be_truthy } + end + RUBY + end + + it 'does not register offense if no descriptions, but different body' do + expect_no_offenses(<<-RUBY) + context do + let(:preferences) { default_preferences } + + it { is_expected.to eq false } + end + + context do + let(:preferences) { %w[a] } + + it { is_expected.to eq true } + end + RUBY + end + + it 'registers offense no descriptions and similar body' do + expect_offense(<<-RUBY) + context do + ^^^^^^^^^^ Repeated context block body on line(s) [7] + let(:preferences) { %w[a] } + + it { is_expected.to eq true } + end + + context do + ^^^^^^^^^^ Repeated context block body on line(s) [1] + let(:preferences) { %w[a] } + + it { is_expected.to eq true } + end + RUBY + end +end diff --git a/spec/rubocop/cop/rspec/repeated_example_group_description_spec.rb b/spec/rubocop/cop/rspec/repeated_example_group_description_spec.rb new file mode 100644 index 000000000..e96d1b67c --- /dev/null +++ b/spec/rubocop/cop/rspec/repeated_example_group_description_spec.rb @@ -0,0 +1,244 @@ +# frozen_string_literal: true + +RSpec.describe RuboCop::Cop::RSpec::RepeatedExampleGroupDescription do + subject(:cop) { described_class.new } + + it 'registers an offense for repeated describe descriptions' do + expect_offense(<<-RUBY) + describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [5] + # example group + end + + describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [1] + # example group + end + RUBY + end + + it 'registers an offense for repeated context descriptions' do + expect_offense(<<-RUBY) + context 'when awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [5] + # example group + end + + context 'when awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [1] + # example group + end + RUBY + end + + it 'registers an offense with right pointing to lines of code' do + expect_offense(<<-RUBY) + describe 'super feature' do + context 'when some case' do + # ... + end + + context 'when awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [10, 14] + # example group + end + + context 'when awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [6, 14] + # example group + end + + context 'when awesome case' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [6, 10] + # example group + end + end + RUBY + end + + it 'does not register offense for different block descriptions' do + expect_no_offenses(<<-RUBY) + describe 'doing x' do + # example group + end + + describe 'doing y' do + # example group + end + RUBY + end + + it 'does not register offense for describe block with additional docstring' do + expect_no_offenses(<<-RUBY) + RSpec.describe 'Animal', 'dog' do + # example group + end + + RSpec.describe 'Animal', 'cat' do + # example group + end + RUBY + end + + it 'does not register offense for describe block with several docstring' do + expect_no_offenses(<<-RUBY) + RSpec.describe 'Animal', 'dog', type: :request do + # example group + end + + RSpec.describe 'Animal', 'cat', type: :request do + # example group + end + RUBY + end + + it 'register offense for different example group with similar descriptions' do + expect_offense(<<-RUBY) + describe 'Animal' do + ^^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [5] + # example group + end + + context 'Animal' do + ^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [1] + # example group + end + RUBY + end + + it 'registers offense only for RSPEC namespace example groups' do + expect_offense(<<-RUBY) + helpers.describe 'doing x' do + it { cool_predicate_method } + end + + RSpec.describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [9] + it { cool_predicate_method } + end + + context 'doing x' do + ^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [5] + it { cool_predicate_method } + end + RUBY + end + + it 'registers offense only for RSPEC namespace example groups in any order' do + expect_offense(<<-RUBY) + RSpec.describe 'doing x' do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [5] + it { cool_predicate_method } + end + + context 'doing x' do + ^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [1] + it { cool_predicate_method } + end + + helpers.describe 'doing x' do + it { cool_predicate_method } + end + RUBY + end + + it 'registers offense only for example group' do + expect_offense(<<-RUBY) + RSpec.describe 'A' do + stub_all_http_calls() + allowed_statuses = %i[open submitted approved].freeze + before { create(:admin) } + + describe '#load' do + ^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [10] + it { cool_predicate_method } + end + + describe '#load' do + ^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [6] + it { cool_predicate_method } + end + end + RUBY + end + + it 'considers interpolated docstrings as different descriptions' do + expect_no_offenses(<<-RUBY) + context "when class is \#{A::B}" do + # ... + end + + context "when class is \#{C::D}" do + # ... + end + RUBY + end + + it 'registers offense correctly for interpolated docstrings' do + expect_offense(<<-RUBY) + context "when class is \#{A::B}" do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [5] + # ... + end + + context "when class is \#{A::B}" do + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [1] + # ... + end + RUBY + end + + it 'considers different classes as different descriptions' do + expect_no_offenses(<<-RUBY) + context A::B do + # ... + end + + context C::D do + # ... + end + RUBY + end + + it 'register offense if same method used in docstring' do + expect_offense(<<-RUBY) + context(description) do + ^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [5] + # ... + end + + context(description) do + ^^^^^^^^^^^^^^^^^^^^^^^ Repeated context block description on line(s) [1] + # ... + end + RUBY + end + + it 'registers offense correctly if example groups are separated' do + expect_offense(<<-RUBY) + describe 'repeated' do + ^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [7] + it { is_expected.to be_truthy } + end + + before { do_something } + + describe 'repeated' do + ^^^^^^^^^^^^^^^^^^^^^^ Repeated describe block description on line(s) [1] + it { is_expected.to be_truthy } + end + RUBY + end + + it 'does not register offense for example group without descriptions' do + expect_no_offenses(<<-RUBY) + context do + # ... + end + + context do + # ... + end + RUBY + end +end