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

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



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

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

#and_with_rhs_or?(node) ⇒ Object



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

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)



104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 104

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

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

#not_nil_check?(node) ⇒ Object



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

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

#on_and(node) ⇒ Object

rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 157

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)
    )
  end
end

#on_if(node) ⇒ Object



141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 141

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

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

  report_offense(node, method_chain, method_call, *removal_ranges) do |corrector|
    corrector.insert_before(method_call.loc.dot, '&') unless method_call.safe_navigation?
  end
end

#report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node) ⇒ Object



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 181

def report_offense(node, rhs, rhs_receiver, *removal_ranges, offense_range: node)
  add_offense(offense_range) do |corrector|
    # If the RHS is an `or` we cannot safely autocorrect because in order to remove
    # the non-nil check we need to add safe-navs to all clauses where the receiver is used
    next if and_with_rhs_or?(node)

    removal_ranges.each { |range| corrector.remove(range) }
    yield corrector if block_given?

    handle_comments(corrector, node, rhs)

    add_safe_nav_to_all_methods_in_chain(corrector, rhs_receiver, rhs)
  end
end

#strip_begin(node) ⇒ Object



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

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

#ternary_safe_navigation_candidate(node) ⇒ Object



119
120
121
122
123
124
125
126
127
# File 'lib/rubocop/cop/style/safe_navigation.rb', line 119

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

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

    (if $_ $_ nil)
  }
PATTERN