nevir/rubocop-rspec

View on GitHub
lib/rubocop/cop/rspec/indexed_let.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module RuboCop
  module Cop
    module RSpec
      # Do not set up test data using indexes (e.g., `item_1`, `item_2`).
      #
      # It makes reading the test harder because it's not clear what exactly
      # is tested by this particular example.
      #
      # The configurable options `AllowedIdentifiers` and `AllowedPatterns`
      # will also read those set in `Naming/VariableNumber`.
      #
      # @example `Max: 1 (default)`
      #   # bad
      #   let(:item_1) { create(:item) }
      #   let(:item_2) { create(:item) }
      #
      #   let(:item1) { create(:item) }
      #   let(:item2) { create(:item) }
      #
      #   # good
      #
      #   let(:visible_item) { create(:item, visible: true) }
      #   let(:invisible_item) { create(:item, visible: false) }
      #
      # @example `Max: 2`
      #   # bad
      #   let(:item_1) { create(:item) }
      #   let(:item_2) { create(:item) }
      #   let(:item_3) { create(:item) }
      #
      #   # good
      #   let(:item_1) { create(:item) }
      #   let(:item_2) { create(:item) }
      #
      # @example `AllowedIdentifiers: ['item_1', 'item_2']`
      #   # good
      #   let(:item_1) { create(:item) }
      #   let(:item_2) { create(:item) }
      #
      # @example `AllowedPatterns: ['item']`
      #   # good
      #   let(:item_1) { create(:item) }
      #   let(:item_2) { create(:item) }
      #
      class IndexedLet < Base
        include AllowedIdentifiers
        include AllowedPattern

        MSG = 'This `let` statement uses `%<index>s` in its name. ' \
              'Please give it a meaningful name.'

        # @!method let_name(node)
        def_node_matcher :let_name, <<~PATTERN
          {
            (block (send nil? #Helpers.all ({str sym} $_) ...) ...)
            (send nil? #Helpers.all ({str sym} $_) block_pass)
          }
        PATTERN

        def on_block(node) # rubocop:disable InternalAffairs/NumblockHandler
          return unless spec_group?(node)

          children = node.body&.child_nodes
          return unless children

          filter_indexed_lets(children).each do |let_node|
            index = let_name(let_node)[INDEX_REGEX]
            add_offense(let_node, message: format(MSG, index: index))
          end
        end

        private

        SUFFIX_INDEX_REGEX = /_?\d+$/.freeze
        INDEX_REGEX = /\d+/.freeze

        def filter_indexed_lets(candidates)
          candidates
            .filter { |node| indexed_let?(node) }
            .group_by { |node| let_name_stripped_index(node) }
            .values
            .filter { |lets| lets.length > cop_config['Max'] }
            .flatten
        end

        def indexed_let?(node)
          let?(node) &&
            SUFFIX_INDEX_REGEX.match?(let_name(node)) &&
            !allowed_identifier?(let_name(node).to_s) &&
            !matches_allowed_pattern?(let_name(node).to_s)
        end

        def let_name_stripped_index(node)
          let_name(node).to_s.gsub(INDEX_REGEX, '')
        end

        def cop_config_patterns_values
          Array(config.for_cop('Naming/VariableNumber')
            .fetch('AllowedPatterns', [])) +
            Array(cop_config.fetch('AllowedPatterns', []))
        end

        def allowed_identifiers
          Array(config.for_cop('Naming/VariableNumber')
            .fetch('AllowedIdentifiers', [])) +
            Array(cop_config.fetch('AllowedIdentifiers', []))
        end
      end
    end
  end
end