diff --git a/lib/rubocop-rspec.rb b/lib/rubocop-rspec.rb index 3de03a7ae..fbb3fae43 100644 --- a/lib/rubocop-rspec.rb +++ b/lib/rubocop-rspec.rb @@ -18,6 +18,7 @@ require_relative 'rubocop/cop/rspec/mixin/location_help' require_relative 'rubocop/cop/rspec/mixin/metadata' require_relative 'rubocop/cop/rspec/mixin/namespace' +require_relative 'rubocop/cop/rspec/mixin/repeated_items' require_relative 'rubocop/cop/rspec/mixin/skip_or_pending' require_relative 'rubocop/cop/rspec/mixin/top_level_group' require_relative 'rubocop/cop/rspec/mixin/variable' diff --git a/lib/rubocop/cop/rspec/mixin/repeated_items.rb b/lib/rubocop/cop/rspec/mixin/repeated_items.rb new file mode 100644 index 000000000..59a0ccd68 --- /dev/null +++ b/lib/rubocop/cop/rspec/mixin/repeated_items.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + module RSpec + # Helps find repeated items in a collection + # + # Provides a generic method to find repeated items by grouping them + # by a key and returning pairs of [item, repeated_lines] for items + # that appear more than once. + module RepeatedItems + # Groups items by key and returns only groups with more than one item + # + # @param items [Enumerable] the filtered collection to group + # @param key_proc [Proc] block returning the grouping key for each item + # @return [Array] array of groups containing more than one item + # that share the same key and there are multiple items in the group + def find_repeated_groups(items, key_proc:) + items + .group_by(&key_proc) + .values + .reject(&:one?) + end + + # Maps a group of items to pairs of [item, repeated_lines] + # + # @param items [Array] array of items that share the same key + # @return [Array] array of [item, repeated_lines] pairs + def add_repeated_lines(items) + repeated_lines = items.map(&:first_line) + items.map { |item| [item, repeated_lines - [item.first_line]] } + end + end + end + end +end diff --git a/lib/rubocop/cop/rspec/repeated_example.rb b/lib/rubocop/cop/rspec/repeated_example.rb index a4a81aa13..1b9c5cc80 100644 --- a/lib/rubocop/cop/rspec/repeated_example.rb +++ b/lib/rubocop/cop/rspec/repeated_example.rb @@ -16,6 +16,8 @@ module RSpec # end # class RepeatedExample < Base + include RepeatedItems + MSG = "Don't repeat examples within an example group. " \ 'Repeated on line(s) %s.' @@ -32,10 +34,10 @@ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler def find_repeated_examples(node) examples = RuboCop::RSpec::ExampleGroup.new(node).examples - examples - .group_by { |example| build_example_signature(example) } - .values - .select { |group| group.size > 1 } + find_repeated_groups( + examples, + key_proc: ->(example) { build_example_signature(example) } + ) end def build_example_signature(example) diff --git a/lib/rubocop/cop/rspec/repeated_example_group_body.rb b/lib/rubocop/cop/rspec/repeated_example_group_body.rb index 9e2fc6fb5..cd802fc50 100644 --- a/lib/rubocop/cop/rspec/repeated_example_group_body.rb +++ b/lib/rubocop/cop/rspec/repeated_example_group_body.rb @@ -44,6 +44,7 @@ module RSpec # class RepeatedExampleGroupBody < Base include SkipOrPending + include RepeatedItems MSG = 'Repeated %s block body on line(s) %s' @@ -72,19 +73,14 @@ def on_begin(node) private def repeated_group_bodies(node) - node - .children + items = node.children .select { |child| example_group_with_body?(child) } .reject { |child| skip_or_pending_inside_block?(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]] } + find_repeated_groups( + items, + key_proc: ->(group) { signature_keys(group) } + ).flat_map { |group| add_repeated_lines(group) } end def signature_keys(group) diff --git a/lib/rubocop/cop/rspec/repeated_example_group_description.rb b/lib/rubocop/cop/rspec/repeated_example_group_description.rb index 1e534141d..e815bf004 100644 --- a/lib/rubocop/cop/rspec/repeated_example_group_description.rb +++ b/lib/rubocop/cop/rspec/repeated_example_group_description.rb @@ -44,6 +44,7 @@ module RSpec # class RepeatedExampleGroupDescription < Base include SkipOrPending + include RepeatedItems MSG = 'Repeated %s block description on line(s) %s' @@ -71,20 +72,15 @@ def on_begin(node) private def repeated_group_descriptions(node) - node - .children + items = node.children .select { |child| example_group?(child) } .reject { |child| skip_or_pending_inside_block?(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]] } + find_repeated_groups( + items, + key_proc: ->(group) { doc_string_and_metadata(group) } + ).flat_map { |group| add_repeated_lines(group) } end def message(group, repeats) diff --git a/lib/rubocop/cop/rspec/repeated_include_example.rb b/lib/rubocop/cop/rspec/repeated_include_example.rb index 811387411..a5a0a645f 100644 --- a/lib/rubocop/cop/rspec/repeated_include_example.rb +++ b/lib/rubocop/cop/rspec/repeated_include_example.rb @@ -46,6 +46,8 @@ module RSpec # end # class RepeatedIncludeExample < Base + include RepeatedItems + MSG = 'Repeated include of shared_examples %s ' \ 'on line(s) %s' @@ -73,13 +75,13 @@ def on_begin(node) private def repeated_include_examples(node) - node - .children + items = node.children .select { |child| literal_include_examples?(child) } - .group_by { |child| signature_keys(child) } - .values - .reject(&:one?) - .flat_map { |items| add_repeated_lines(items) } + + find_repeated_groups( + items, + key_proc: ->(child) { signature_keys(child) } + ).flat_map { |group| add_repeated_lines(group) } end def literal_include_examples?(node) @@ -87,11 +89,6 @@ def literal_include_examples?(node) node.arguments.all?(&:recursive_literal_or_const?) end - def add_repeated_lines(items) - repeated_lines = items.map(&:first_line) - items.map { |item| [item, repeated_lines - [item.first_line]] } - end - def signature_keys(item) item.arguments end diff --git a/lib/rubocop/cop/rspec/scattered_setup.rb b/lib/rubocop/cop/rspec/scattered_setup.rb index 6c02f249e..be4c096e4 100644 --- a/lib/rubocop/cop/rspec/scattered_setup.rb +++ b/lib/rubocop/cop/rspec/scattered_setup.rb @@ -42,6 +42,7 @@ module RSpec class ScatteredSetup < Base include FinalEndLocation include RangeHelp + include RepeatedItems extend AutoCorrector MSG = 'Do not define multiple `%s` hooks in the same ' \ @@ -63,17 +64,14 @@ def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler private def repeated_hooks(node) # rubocop:disable Metrics/CyclomaticComplexity - hooks = RuboCop::RSpec::ExampleGroup.new(node) - .hooks + hooks = RuboCop::RSpec::ExampleGroup.new(node).hooks .reject(&:inside_class_method?) .select { |hook| hook.knowable_scope? && hook.name != :around } - .group_by { |hook| [hook.name, hook.scope, hook.metadata] } - .values - .reject(&:one?) - hooks.map do |hook| - hook.map(&:to_node) - end + find_repeated_groups( + hooks, + key_proc: ->(hook) { [hook.name, hook.scope, hook.metadata] } + ).map { |hook_group| hook_group.map(&:to_node) } end def lines_msg(numbers)