lib/rubocop/cop/style/collection_compact.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# Checks for places where custom logic on rejection nils from arrays
# and hashes can be replaced with `{Array,Hash}#{compact,compact!}`.
#
# @safety
# It is unsafe by default because false positives may occur in the
# `nil` check of block arguments to the receiver object. Additionally,
# we can't know the type of the receiver object for sure, which may
# result in false positives as well.
#
# For example, `[[1, 2], [3, nil]].reject { |first, second| second.nil? }`
# and `[[1, 2], [3, nil]].compact` are not compatible. This will work fine
# when the receiver is a hash object.
#
# @example
# # bad
# array.reject(&:nil?)
# array.reject { |e| e.nil? }
# array.select { |e| !e.nil? }
# array.grep_v(nil)
# array.grep_v(NilClass)
#
# # good
# array.compact
#
# # bad
# hash.reject!(&:nil?)
# array.delete_if(&:nil?)
# hash.reject! { |k, v| v.nil? }
# array.delete_if { |e| e.nil? }
# hash.select! { |k, v| !v.nil? }
#
# # good
# hash.compact!
#
# @example AllowedReceivers: ['params']
# # good
# params.reject(&:nil?)
#
class CollectionCompact < Base
include AllowedReceivers
include RangeHelp
extend AutoCorrector
extend TargetRubyVersion
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
RESTRICT_ON_SEND = %i[reject delete_if reject! select select! grep_v].freeze
TO_ENUM_METHODS = %i[to_enum lazy].freeze
minimum_target_ruby_version 2.4
# @!method reject_method_with_block_pass?(node)
def_node_matcher :reject_method_with_block_pass?, <<~PATTERN
(call !nil? {:reject :delete_if :reject!}
(block_pass
(sym :nil?)))
PATTERN
# @!method reject_method?(node)
def_node_matcher :reject_method?, <<~PATTERN
(block
(call
!nil? {:reject :delete_if :reject!})
$(args ...)
(call
$(lvar _) :nil?))
PATTERN
# @!method select_method?(node)
def_node_matcher :select_method?, <<~PATTERN
(block
(call
!nil? {:select :select!})
$(args ...)
(call
(call
$(lvar _) :nil?) :!))
PATTERN
# @!method grep_v_with_nil?(node)
def_node_matcher :grep_v_with_nil?, <<~PATTERN
(send _ :grep_v {(nil) (const {nil? cbase} :NilClass)})
PATTERN
def on_send(node)
return unless (range = offense_range(node))
return if allowed_receiver?(node.receiver)
if (target_ruby_version <= 3.0 || node.method?(:delete_if)) && to_enum_method?(node)
return
end
good = good_method_name(node)
message = format(MSG, good: good, bad: range.source)
add_offense(range, message: message) { |corrector| corrector.replace(range, good) }
end
alias on_csend on_send
private
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def offense_range(node)
if reject_method_with_block_pass?(node) || grep_v_with_nil?(node)
range(node, node)
else
block_node = node.parent
return unless block_node&.block_type?
unless (args, receiver = reject_method?(block_node) || select_method?(block_node))
return
end
return unless args.last.source == receiver.source
range(node, block_node)
end
end
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
def to_enum_method?(node)
return false unless node.receiver.send_type?
TO_ENUM_METHODS.include?(node.receiver.method_name)
end
def good_method_name(node)
if node.bang_method? || node.method?(:delete_if)
'compact!'
else
'compact'
end
end
def range(begin_pos_node, end_pos_node)
range_between(begin_pos_node.loc.selector.begin_pos, end_pos_node.source_range.end_pos)
end
end
end
end
end