class RuboCop::Cop::Style::HashExcept
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.
@safety
This cop is unsafe because it cannot be guaranteed that the receiver is a `Hash` or responds to the replacement method.
@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 } {foo: 1, bar: 2, baz: 3}.reject {|k, v| %i[bar].include?(k) } {foo: 1, bar: 2, baz: 3}.select {|k, v| !%i[bar].include?(k) } {foo: 1, bar: 2, baz: 3}.filter {|k, v| !%i[bar].include?(k) } # good {foo: 1, bar: 2, baz: 3}.except(:bar)
Constants
- MSG
- RESTRICT_ON_SEND
Public Instance Methods
on_send(node)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 75 def on_send(node) method_name = node.method_name block = node.parent return unless bad_method?(method_name, block) && semantically_except_method?(node, block) except_key = except_key(block) return if except_key.nil? || !safe_to_register_offense?(block, except_key) range = offense_range(node) preferred_method = "except(#{except_key_source(except_key)})" add_offense(range, message: format(MSG, prefer: preferred_method)) do |corrector| corrector.replace(range, preferred_method) end end
Also aliased as: on_csend
Private Instance Methods
bad_method?(method_name, block)
click to toggle source
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# File lib/rubocop/cop/style/hash_except.rb, line 95 def bad_method?(method_name, block) if active_support_extensions_enabled? bad_method_with_active_support?(block) do |key_arg, send_node| if send_node.method?(:in?) && send_node.receiver&.source != key_arg.source return false end return true if !send_node.method?(:include?) && !send_node.method?(:exclude?) send_node.first_argument&.source == key_arg.source end else bad_method_with_poro?(block) do |key_arg, send_node| return false if method_name == :reject && block.body.method?(:!) !send_node.method?(:include?) || send_node.first_argument&.source == key_arg.source end end end
decorate_source(value)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 169 def decorate_source(value) return ":\"#{value.source}\"" if value.dsym_type? return "\"#{value.source}\"" if value.dstr_type? return ":#{value.source}" if value.sym_type? "'#{value.source}'" end
except_key(node)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 177 def except_key(node) key_argument = node.argument_list.first.source body = extract_body_if_negated(node.body) lhs, _method_name, rhs = *body return if [lhs, rhs].map(&:source).none?(key_argument) [lhs, rhs].find { |operand| operand.source != key_argument } end
except_key_source(key)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 156 def except_key_source(key) if key.array_type? key = if key.percent_literal? key.each_value.map { |v| decorate_source(v) } else key.each_value.map(&:source) end return key.join(', ') end key.literal? ? key.source : "*#{key.source}" end
extract_body_if_negated(body)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 150 def extract_body_if_negated(body) return body unless body.method?('!') body.receiver end
included?(negated, body)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 131 def included?(negated, body) body.method?('include?') || body.method?('in?') || (negated && body.method?('exclude?')) end
not_included?(negated, body)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 135 def not_included?(negated, body) body.method?('exclude?') || (negated && (body.method?('include?') || body.method?('in?'))) end
offense_range(node)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 186 def offense_range(node) range_between(node.loc.selector.begin_pos, node.parent.loc.end.end_pos) end
safe_to_register_offense?(block, except_key)
click to toggle source
# File lib/rubocop/cop/style/hash_except.rb, line 139 def safe_to_register_offense?(block, except_key) extracted = extract_body_if_negated(block.body) if extracted.method?('in?') || extracted.method?('include?') || extracted.method?('exclude?') return true end return true if block.body.method?('eql?') except_key.sym_type? || except_key.str_type? end
semantically_except_method?(send, block)
click to toggle source
rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# File lib/rubocop/cop/style/hash_except.rb, line 115 def semantically_except_method?(send, block) body = block.body negated = body.method?('!') body = body.receiver if negated case send.method_name when :reject body.method?('==') || body.method?('eql?') || included?(negated, body) when :select, :filter body.method?('!=') || not_included?(negated, body) else false end end