Class: RuboCop::Cop::Style::SafeNavigation

Inherits:
Base
  • Object
show all
Extended by:
AutoCorrector, TargetRubyVersion
Includes:
NilMethods, RangeHelp
Defined in:
lib/rubocop/cop/style/safe_navigation.rb

Overview

Transforms usages of a method call safeguarded by a non ‘nil` check for the variable whose method is being called to safe navigation (`&.`). If there is a method chain, all of the methods in the chain need to be checked for safety, and all of the methods will need to be changed to use safe navigation.

The default for ‘ConvertCodeThatCanStartToReturnNil` is `false`. When configured to `true`, this will check for code in the format `!foo.nil? && foo.bar`. As it is written, the return of this code is limited to `false` and whatever the return of the method is. If this is converted to safe navigation, `foo&.bar` can start returning `nil` as well as what the method returns.

The default for ‘MaxChainLength` is `2`. We have limited the cop to not register an offense for method chains that exceed this option’s value.

NOTE: This cop will recognize offenses but not autocorrect code when the right hand side (RHS) of the ‘&&` statement is an `||` statement (eg. `foo && (foo.bar? || foo.baz?)`). It can be corrected manually by removing the `foo &&` and adding `&.` to each `foo` on the RHS.

Examples:

# bad
foo.bar if foo
foo.bar.baz if foo
foo.bar(param1, param2) if foo
foo.bar { |e| e.something } if foo
foo.bar(param) { |e| e.something } if foo

foo.bar if !foo.nil?
foo.bar unless !foo
foo.bar unless foo.nil?

foo && foo.bar
foo && foo.bar.baz
foo && foo.bar(param1, param2)
foo && foo.bar { |e| e.something }
foo && foo.bar(param) { |e| e.something }

foo ? foo.bar : nil
foo.nil? ? nil : foo.bar
!foo.nil? ? foo.bar : nil
!foo ? nil : foo.bar

# good
foo&.bar
foo&.bar&.baz
foo&.bar(param1, param2)
foo&.bar { |e| e.something }
foo&.bar(param) { |e| e.something }
foo && foo.bar.baz.qux # method chain with more than 2 methods
foo && foo.nil? # method that `nil` responds to

# Method calls that do not use `.`
foo && foo < bar
foo < bar if foo

# When checking `foo&.empty?` in a conditional, `foo` being `nil` will actually
# do the opposite of what the author intends.
foo && foo.empty?

# This could start returning `nil` as well as the return of the method
foo.nil? || foo.bar
!foo || foo.bar

# Methods that are used on assignment, arithmetic operation or
# comparison should not be converted to use safe navigation
foo.baz = bar if foo
foo.baz + bar if foo
foo.bar > 2 if foo

foo ? foo[index] : nil    # Ignored `foo&.[](index)` due to unclear readability benefit.
foo ? foo[idx] = v : nil  # Ignored `foo&.[]=(idx, v)` due to unclear readability benefit.
foo ? foo * 42 : nil      # Ignored `foo&.*(42)` due to unclear readability benefit.

Constant Summary collapse

MSG =
'Use safe navigation (`&.`) instead of checking if an object ' \
'exists before calling the method.'
LOGIC_JUMP_KEYWORDS =
%i[break fail next raise return throw yield].freeze

Constants included from RangeHelp

RangeHelp::BYTE_ORDER_MARK, RangeHelp::NOT_GIVEN

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 included from TargetRubyVersion

maximum_target_ruby_version, minimum_target_ruby_version, required_maximum_ruby_version, required_minimum_ruby_version, support_target_ruby_version?

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

#and_inside_begin?(node) ⇒ Object



140
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 140

def_node_matcher :and_inside_begin?, '`(begin and ...)'

#and_with_rhs_or?(node) ⇒ Object



134
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 134

def_node_matcher :and_with_rhs_or?, '(and _ {or (begin or)})'

#modifier_if_safe_navigation_candidate(node) ⇒ Object

if format: (if checked_variable body nil) unless format: (if checked_variable nil body)



108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 108

def_node_matcher :modifier_if_safe_navigation_candidate, <<~PATTERN
  {
    (if {
          (send $_ {:nil? :!})
          $_
        } nil? $_)

    (if {
          (send (send $_ :nil?) :!)
          $_
        } $_ nil?)
  }
PATTERN

#not_nil_check?(node) ⇒ Object



137
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 137

def_node_matcher :not_nil_check?, '(send (send $_ :nil?) :!)'

#on_and(node) ⇒ Object

rubocop:enable Metrics/AbcSize



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 165

def on_and(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
  collect_and_clauses(node).each do |(lhs, lhs_operator_range), (rhs, _rhs_operator_range)|
    lhs_not_nil_check = not_nil_check?(lhs)
    lhs_receiver = lhs_not_nil_check || lhs
    rhs_receiver = find_matching_receiver_invocation(strip_begin(rhs), lhs_receiver)

    next if !cop_config['ConvertCodeThatCanStartToReturnNil'] && lhs_not_nil_check
    next unless offending_node?(node, lhs_receiver, rhs, rhs_receiver)

    # Since we are evaluating every clause in potentially a complex chain of `and` nodes,
    # we need to ensure that there isn't an object check happening
    lhs_method_chain = find_method_chain(lhs_receiver)
    next unless lhs_method_chain == lhs_receiver || lhs_not_nil_check

    report_offense(
      node,
      rhs, rhs_receiver,
      range_with_surrounding_space(range: lhs.source_range, side: :right),
      range_with_surrounding_space(range: lhs_operator_range, side: :right),
      offense_range: range_between(lhs.source_range.begin_pos, rhs.source_range.end_pos)
    ) do |corrector|
      corrector.replace(rhs_receiver, lhs_receiver.source)
    end
    ignore_node(node)
  end
end

#on_if(node) ⇒ Object

rubocop:disable Metrics/AbcSize



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 146

def on_if(node)
  return if allowed_if_condition?(node)

  checked_variable, receiver, method_chain, _method = extract_parts_from_if(node)
  return unless offending_node?(node, checked_variable, method_chain, receiver)

  body = extract_if_body(node)
  method_call = receiver.parent
  return if dotless_operator_call?(method_call) || method_call.double_colon?

  removal_ranges = [begin_range(node, body), end_range(node, body)]

  report_offense(node, method_chain, method_call, *removal_ranges) do |corrector|
    corrector.replace(receiver, checked_variable.source) if checked_variable.csend_type?
    corrector.insert_before(method_call.loc.dot, '&') unless method_call.safe_navigation?
  end
end

#strip_begin(node) ⇒ Object



143
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 143

def_node_matcher :strip_begin, '{ (begin $!begin) $!(begin) }'

#ternary_safe_navigation_candidate(node) ⇒ Object



123
124
125
126
127
128
129
130
131
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 123

def_node_matcher :ternary_safe_navigation_candidate, <<~PATTERN
  {
    (if (send $_ {:nil? :!}) nil $_)

    (if (send (send $_ :nil?) :!) $_ nil)

    (if $_ $_ nil)
  }
PATTERN