lib/rubocop/cop/lint/redundant_safe_navigation.rb
# frozen_string_literal: true
module RuboCop
module Cop
module Lint
# Checks for redundant safe navigation calls.
# Use cases where a constant, named in camel case for classes and modules is `nil` are rare,
# and an offense is not detected when the receiver is a constant. The detection also applies
# to literal receivers, except for `nil`.
#
# For all receivers, the `instance_of?`, `kind_of?`, `is_a?`, `eql?`, `respond_to?`,
# and `equal?` methods are checked by default.
# These are customizable with `AllowedMethods` option.
#
# The `AllowedMethods` option specifies nil-safe methods,
# in other words, it is a method that is allowed to skip safe navigation.
# Note that the `AllowedMethod` option is not an option that specifies methods
# for which to suppress (allow) this cop's check.
#
# In the example below, the safe navigation operator (`&.`) is unnecessary
# because `NilClass` has methods like `respond_to?` and `is_a?`.
#
# @safety
# This cop is unsafe, because autocorrection can change the return type of
# the expression. An offending expression that previously could return `nil`
# will be autocorrected to never return `nil`.
#
# @example
# # bad
# CamelCaseConst&.do_something
#
# # bad
# do_something if attrs&.respond_to?(:[])
#
# # good
# do_something if attrs.respond_to?(:[])
#
# # bad
# while node&.is_a?(BeginNode)
# node = node.parent
# end
#
# # good
# CamelCaseConst.do_something
#
# # good
# while node.is_a?(BeginNode)
# node = node.parent
# end
#
# # good - without `&.` this will always return `true`
# foo&.respond_to?(:to_a)
#
# # bad - for `nil`s conversion methods return default values for the type
# foo&.to_h || {}
# foo&.to_h { |k, v| [k, v] } || {}
# foo&.to_a || []
# foo&.to_i || 0
# foo&.to_f || 0.0
# foo&.to_s || ''
#
# # good
# foo.to_h
# foo.to_h { |k, v| [k, v] }
# foo.to_a
# foo.to_i
# foo.to_f
# foo.to_s
#
# @example AllowedMethods: [nil_safe_method]
# # bad
# do_something if attrs&.nil_safe_method(:[])
#
# # good
# do_something if attrs.nil_safe_method(:[])
# do_something if attrs&.not_nil_safe_method(:[])
#
class RedundantSafeNavigation < Base
include AllowedMethods
extend AutoCorrector
MSG = 'Redundant safe navigation detected, use `.` instead.'
MSG_LITERAL = 'Redundant safe navigation with default literal detected.'
NIL_SPECIFIC_METHODS = (nil.methods - Object.new.methods).to_set.freeze
SNAKE_CASE = /\A[[:digit:][:upper:]_]+\z/.freeze
# @!method respond_to_nil_specific_method?(node)
def_node_matcher :respond_to_nil_specific_method?, <<~PATTERN
(csend _ :respond_to? (sym %NIL_SPECIFIC_METHODS))
PATTERN
# @!method conversion_with_default?(node)
def_node_matcher :conversion_with_default?, <<~PATTERN
{
(or $(csend _ :to_h) (hash))
(or (block $(csend _ :to_h) ...) (hash))
(or $(csend _ :to_a) (array))
(or $(csend _ :to_i) (int 0))
(or $(csend _ :to_f) (float 0.0))
(or $(csend _ :to_s) (str empty?))
}
PATTERN
# rubocop:disable Metrics/AbcSize
def on_csend(node)
unless assume_receiver_instance_exists?(node.receiver)
return unless check?(node) && allowed_method?(node.method_name)
return if respond_to_nil_specific_method?(node)
end
range = node.loc.dot
add_offense(range) { |corrector| corrector.replace(range, '.') }
end
def on_or(node)
conversion_with_default?(node) do |send_node|
range = send_node.loc.dot.begin.join(node.source_range.end)
add_offense(range, message: MSG_LITERAL) do |corrector|
corrector.replace(send_node.loc.dot, '.')
range_with_default = node.lhs.source_range.end.begin.join(node.source_range.end)
corrector.remove(range_with_default)
end
end
end
# rubocop:enable Metrics/AbcSize
private
def assume_receiver_instance_exists?(receiver)
return true if receiver.const_type? && !receiver.source.match?(SNAKE_CASE)
receiver.literal? && !receiver.nil_type?
end
def check?(node)
parent = node.parent
return false unless parent
condition?(parent, node) ||
parent.and_type? ||
parent.or_type? ||
(parent.send_type? && parent.negation_method?)
end
def condition?(parent, node)
(parent.conditional? || parent.post_condition_loop?) && parent.condition == node
end
end
end
end
end