Class: RuboCop::Cop::Lint::RedundantSafeNavigation

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector
Includes:
AllowedMethods
Defined in:
lib/rubocop/cop/lint/redundant_safe_navigation.rb

Overview

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 `self`, and 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?`.

The ‘InferNonNilReceiver` option specifies whether to look into previous code paths to infer if the receiver can’t be nil. This check is unsafe because the receiver can be redefined between the safe navigation call and previous regular method call. It does the inference only in the current scope, e.g. within the same method definition etc.

The ‘AdditionalNilMethods` option specifies additional custom methods which are defined on `NilClass`. When `InferNonNilReceiver` is set, they are used to determine whether the receiver can be nil.

Examples:

# bad
CamelCaseConst&.do_something

# good
CamelCaseConst.do_something

# bad
foo.to_s&.strip
foo.to_i&.zero?
foo.to_f&.zero?
foo.to_a&.size
foo.to_h&.size

# good
foo.to_s.strip
foo.to_i.zero?
foo.to_f.zero?
foo.to_a.size
foo.to_h.size

# 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
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

# bad
self&.foo

# good
self.foo

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(:[])

InferNonNilReceiver: false (default)

# good
foo.bar
foo&.baz

InferNonNilReceiver: true

# bad
foo.bar
foo&.baz # would raise on previous line if `foo` is nil

# good
foo.bar
foo.baz

# bad
if foo.condition?
  foo&.bar
end

# good
if foo.condition?
  foo.bar
end

# good (different scopes)
def method1
  foo.bar
end

def method2
  foo&.bar
end

AdditionalNilMethods: [present?]

# good
foo.present?
foo&.bar

Constant Summary collapse

MSG =
'Redundant safe navigation detected, use `.` instead.'
MSG_LITERAL =
'Redundant safe navigation with default literal detected.'
MSG_NON_NIL =
'Redundant safe navigation on non-nil receiver (detected by analyzing ' \
'previous code/method invocations).'
NIL_SPECIFIC_METHODS =
(nil.methods - Object.new.methods).to_set.freeze
SNAKE_CASE =
/\A[[:digit:][:upper:]_]+\z/.freeze
GUARANTEED_INSTANCE_METHODS =
%i[to_s to_i to_f to_a to_h].freeze

Constants inherited from Base

Base::RESTRICT_ON_SEND

Instance Attribute Summary

Attributes inherited from Base

#config, #processed_source

Instance Method Summary collapse

Methods included from AutoCorrector

support_autocorrect?

Methods inherited from Base

#active_support_extensions_enabled?, #add_global_offense, #add_offense, #always_autocorrect?, autocorrect_incompatible_with, badge, #begin_investigation, #callbacks_needed, callbacks_needed, #config_to_allow_offenses, #config_to_allow_offenses=, #contextual_autocorrect?, #cop_config, #cop_name, cop_name, department, documentation_url, exclude_from_registry, #excluded_file?, #external_dependency_checksum, inherited, #initialize, #inspect, joining_forces, lint?, match?, #message, #offenses, #on_investigation_end, #on_new_investigation, #on_other_file, #parse, #parser_engine, #ready, #relevant_file?, requires_gem, #string_literals_frozen_by_default?, support_autocorrect?, support_multiple_source?, #target_gem_version, #target_rails_version, #target_ruby_version

Methods included from ExcludeLimit

#exclude_limit

Methods included from AutocorrectLogic

#autocorrect?, #autocorrect_enabled?, #autocorrect_requested?, #autocorrect_with_disable_uncorrectable?, #correctable?, #disable_uncorrectable?, #safe_autocorrect?

Methods included from IgnoredNode

#ignore_node, #ignored_node?, #part_of_ignored_node?

Methods included from Util

silence_warnings

Constructor Details

This class inherits a constructor from RuboCop::Cop::Base

Instance Method Details

#conversion_with_default?(node) ⇒ Object



166
167
168
169
170
171
172
173
174
175
# File 'lib/rubocop/cop/lint/redundant_safe_navigation.rb', line 166

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

#on_csend(node) ⇒ Object

rubocop:disable Metrics/AbcSize



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/rubocop/cop/lint/redundant_safe_navigation.rb', line 178

def on_csend(node)
  range = node.loc.dot

  if infer_non_nil_receiver?
    checker = Lint::Utils::NilReceiverChecker.new(node.receiver, additional_nil_methods)

    if checker.cant_be_nil?
      add_offense(range, message: MSG_NON_NIL) { |corrector| corrector.replace(range, '.') }
      return
    end
  end

  unless assume_receiver_instance_exists?(node.receiver)
    return if !guaranteed_instance?(node.receiver) && !check?(node)
    return if respond_to_nil_specific_method?(node)
  end

  add_offense(range) { |corrector| corrector.replace(range, '.') }
end

#on_or(node) ⇒ Object

rubocop:disable Metrics/AbcSize



200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/rubocop/cop/lint/redundant_safe_navigation.rb', line 200

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

#respond_to_nil_specific_method?(node) ⇒ Object



161
162
163
# File 'lib/rubocop/cop/lint/redundant_safe_navigation.rb', line 161

def_node_matcher :respond_to_nil_specific_method?, <<~PATTERN
  (csend _ :respond_to? (sym %NIL_SPECIFIC_METHODS))
PATTERN