# frozen_string_literal: true module RuboCop module Cop module Style # This cop checks for usages of `Hash#reject`, `Hash#select`, and `Hash#filter` methods # that can be replaced with `Hash#except` method. # # This cop should only be enabled on Ruby version 3.0 or higher. # (`Hash#except` was added in Ruby 3.0.) # # For safe detection, it is limited to commonly used string and symbol comparisons # when used `==`. # And do not check `Hash#delete_if` and `Hash#keep_if` to change receiver object. # # @example # # # bad # {foo: 1, bar: 2, baz: 3}.reject {|k, v| k == :bar } # {foo: 1, bar: 2, baz: 3}.select {|k, v| k != :bar } # {foo: 1, bar: 2, baz: 3}.filter {|k, v| k != :bar } # # # good # {foo: 1, bar: 2, baz: 3}.except(:bar) # class HashExcept < Base include RangeHelp extend TargetRubyVersion extend AutoCorrector minimum_target_ruby_version 3.0 MSG = 'Use `%s` instead.' RESTRICT_ON_SEND = %i[reject select filter].freeze # @!method bad_method?(node) def_node_matcher :bad_method?, <<~PATTERN (block (send _ _) (args (arg _) (arg _)) (send _ {:== :!= :eql?} _)) PATTERN def on_send(node) block = node.parent return unless bad_method?(block) && semantically_except_method?(node, block) except_key = except_key(block) return unless safe_to_register_offense?(block, except_key) range = offense_range(node) preferred_method = "except(#{except_key.source})" add_offense(range, message: format(MSG, prefer: preferred_method)) do |corrector| corrector.replace(range, preferred_method) end end private def semantically_except_method?(send, block) body = block.body case send.method_name when :reject body.method?('==') || body.method?('eql?') when :select, :filter body.method?('!=') else false end end def safe_to_register_offense?(block, except_key) return true if block.body.method?('eql?') except_key.sym_type? || except_key.str_type? end def except_key(node) key_argument = node.argument_list.first lhs, _method_name, rhs = *node.body [lhs, rhs].find { |operand| operand.source != key_argument.source } end def offense_range(node) range_between(node.loc.selector.begin_pos, node.parent.loc.end.end_pos) end end end end end