lib/rubocop/cop/performance/redundant_equality_comparison_block.rb
# frozen_string_literal: true module RuboCop module Cop module Performance # Checks for uses `Enumerable#all?`, `Enumerable#any?`, `Enumerable#one?`, # and `Enumerable#none?` are compared with `===` or similar methods in block. # # By default, `Object#===` behaves the same as `Object#==`, but this # behavior is appropriately overridden in subclass. For example, # `Range#===` returns `true` when argument is within the range. # # This cop has `AllowRegexpMatch` option and it is true by default because # `regexp.match?('string')` often used in block changes to the opposite result: # # [source,ruby] # ---- # [/pattern/].all? { |regexp| regexp.match?('pattern') } # => true # [/pattern/].all? { |regexp| regexp =~ 'pattern' } # => true # [/pattern/].all?('pattern') # => false # ---- # # @safety # This cop is unsafe because `===` and `==` do not always behave the same. # # @example # # bad # items.all? { |item| pattern === item } # items.all? { |item| item == other } # items.all? { |item| item.is_a?(Klass) } # items.all? { |item| item.kind_of?(Klass) } # # # good # items.all?(pattern) # items.all?(Klass) # # @example AllowRegexpMatch: true (default) # # # good # items.all? { |item| item =~ pattern } # items.all? { |item| item.match?(pattern) } # # @example AllowRegexpMatch: false # # # bad # items.all? { |item| item =~ pattern } # items.all? { |item| item.match?(pattern) } # class RedundantEqualityComparisonBlock < Base extend AutoCorrector extend TargetRubyVersion minimum_target_ruby_version 2.5 MSG = 'Use `%<prefer>s` instead of block.' TARGET_METHODS = %i[all? any? one? none?].freeze COMPARISON_METHODS = %i[== === is_a? kind_of?].freeze REGEXP_METHODS = %i[=~ match?].freeze IS_A_METHODS = %i[is_a? kind_of?].freeze def on_block(node) return unless TARGET_METHODS.include?(node.method_name) return unless one_block_argument?(node.arguments) block_argument = node.first_argument return unless (block_body = node.body) return unless use_equality_comparison_block?(block_body) return if same_block_argument_and_is_a_argument?(block_body, block_argument) return unless (new_argument = new_argument(block_argument, block_body)) range = offense_range(node) prefer = "#{node.method_name}(#{new_argument})" add_offense(range, message: format(MSG, prefer: prefer)) do |corrector| corrector.replace(range, prefer) end end private def one_block_argument?(block_arguments) block_arguments.one? && !block_arguments.source.include?(',') end def use_equality_comparison_block?(block_body) return false unless block_body.send_type? method_name = block_body.method_name COMPARISON_METHODS.include?(method_name) || (!allow_regexp_match? && REGEXP_METHODS.include?(method_name)) end def same_block_argument_and_is_a_argument?(block_body, block_argument) if block_body.method?(:===) block_argument.source != block_body.children[2].source elsif IS_A_METHODS.include?(block_body.method_name) block_argument.source == block_body.first_argument.source else block_body.receiver.source == block_body.first_argument.receiver&.source end end def new_argument(block_argument, block_body) if block_argument.source == block_body.receiver.source rhs = block_body.first_argument return if use_block_argument_in_method_argument_of_operand?(block_argument, rhs) rhs.source elsif block_argument.source == block_body.first_argument.source lhs = block_body.receiver return if use_block_argument_in_method_argument_of_operand?(block_argument, lhs) lhs.source end end def use_block_argument_in_method_argument_of_operand?(block_argument, operand) return false unless operand.send_type? arguments = operand.arguments arguments.inject(arguments.map(&:source)) do |operand_sources, argument| operand_sources + argument.each_descendant(:lvar).map(&:source) end.any?(block_argument.source) end def offense_range(node) node.send_node.loc.selector.join(node.source_range.end) end def allow_regexp_match? cop_config.fetch('AllowRegexpMatch', true) end end end end end