class RuboCop::Cop::Style::SafeNavigation

This cop 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. We have limited the cop to not register an offense for method chains that exceed 2 methods.

Configuration option: ConvertCodeThatCanStartToReturnNil The default for this 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.

@example

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

# 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

# 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

Constants

LOGIC_JUMP_KEYWORDS
MSG

Public Instance Methods

autocorrect(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 114
def autocorrect(node)
  _check, body, = node.node_parts
  _checked_variable, matching_receiver, = extract_parts(node)
  method_call, = matching_receiver.parent

  lambda do |corrector|
    corrector.remove(begin_range(node, body))
    corrector.remove(end_range(node, body))
    corrector.insert_before(method_call.loc.dot, '&')

    add_safe_nav_to_all_methods_in_chain(corrector, method_call, body)
  end
end
check_node(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 98
def check_node(node)
  checked_variable, receiver, method_chain, method = extract_parts(node)
  return unless receiver == checked_variable
  return if use_var_only_in_unless_modifier?(node, checked_variable)
  # method is already a method call so this is actually checking for a
  # chain greater than 2
  return if chain_size(method_chain, method) > 1
  return if unsafe_method_used?(method_chain, method)

  add_offense(node)
end
on_and(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 94
def on_and(node)
  check_node(node)
end
on_if(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 88
def on_if(node)
  return if allowed_if_condition?(node)

  check_node(node)
end
use_var_only_in_unless_modifier?(node, variable) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 110
def use_var_only_in_unless_modifier?(node, variable)
  node.if_type? && node.unless? && !method_called?(variable)
end

Private Instance Methods

add_safe_nav_to_all_methods_in_chain(corrector, start_method, method_chain) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 240
def add_safe_nav_to_all_methods_in_chain(corrector,
                                         start_method,
                                         method_chain)
  start_method.each_ancestor do |ancestor|
    break unless %i[send block].include?(ancestor.type)
    next unless ancestor.send_type?

    corrector.insert_before(ancestor.loc.dot, '&')

    break if ancestor == method_chain
  end
end
allowed_if_condition?(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 130
def allowed_if_condition?(node)
  node.else? || node.elsif? || node.ternary?
end
begin_range(node, method_call) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 230
def begin_range(node, method_call)
  range_between(node.loc.expression.begin_pos,
                method_call.loc.expression.begin_pos)
end
chain_size(method_chain, method) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 192
def chain_size(method_chain, method)
  method.each_ancestor(:send).inject(0) do |total, ancestor|
    break total + 1 if ancestor == method_chain

    total + 1
  end
end
end_range(node, method_call) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 235
def end_range(node, method_call)
  range_between(method_call.loc.expression.end_pos,
                node.loc.expression.end_pos)
end
extract_common_parts(method_chain, checked_variable) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 169
def extract_common_parts(method_chain, checked_variable)
  matching_receiver =
    find_matching_receiver_invocation(method_chain, checked_variable)

  method = matching_receiver.parent if matching_receiver

  [checked_variable, matching_receiver, method]
end
extract_parts(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 134
def extract_parts(node)
  case node.type
  when :if
    extract_parts_from_if(node)
  when :and
    extract_parts_from_and(node)
  end
end
extract_parts_from_and(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 157
def extract_parts_from_and(node)
  checked_variable, rhs = *node
  if cop_config['ConvertCodeThatCanStartToReturnNil']
    checked_variable =
      not_nil_check?(checked_variable) || checked_variable
  end

  checked_variable, matching_receiver, method =
    extract_common_parts(rhs, checked_variable)
  [checked_variable, matching_receiver, rhs, method]
end
extract_parts_from_if(node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 143
def extract_parts_from_if(node)
  variable, receiver =
    modifier_if_safe_navigation_candidate(node)

  checked_variable, matching_receiver, method =
    extract_common_parts(receiver, variable)

  if receiver && LOGIC_JUMP_KEYWORDS.include?(receiver.type)
    matching_receiver = nil
  end

  [checked_variable, matching_receiver, receiver, method]
end
find_matching_receiver_invocation(method_chain, checked_variable) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 178
def find_matching_receiver_invocation(method_chain, checked_variable)
  return nil unless method_chain

  receiver = if method_chain.block_type?
               method_chain.send_node.receiver
             else
               method_chain.receiver
             end

  return receiver if receiver == checked_variable

  find_matching_receiver_invocation(receiver, checked_variable)
end
method_called?(send_node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 226
def method_called?(send_node)
  send_node&.parent&.send_type?
end
negated?(send_node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 218
def negated?(send_node)
  if method_called?(send_node)
    negated?(send_node.parent)
  else
    send_node.send_type? && send_node.method?(:!)
  end
end
unsafe_method?(send_node) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 214
def unsafe_method?(send_node)
  negated?(send_node) || send_node.assignment? || !send_node.dot?
end
unsafe_method_used?(method_chain, method) click to toggle source
# File lib/rubocop/cop/style/safe_navigation.rb, line 200
def unsafe_method_used?(method_chain, method)
  return true if unsafe_method?(method)

  method.each_ancestor(:send).any? do |ancestor|
    unless config.for_cop('Lint/SafeNavigationChain')['Enabled']
      break true
    end

    break true if unsafe_method?(ancestor)
    break true if nil_methods.include?(ancestor.method_name)
    break false if ancestor == method_chain
  end
end