class RuboCop::Cop::Lint::SafeNavigationChain

The safe navigation operator returns nil if the receiver is nil. If you chain an ordinary method call after a safe navigation operator, it raises NoMethodError. We should use a safe navigation operator after a safe navigation operator. This cop checks for the problem outlined above.

@example

# bad
x&.foo.bar
x&.foo + bar
x&.foo[bar]

# good
x&.foo&.bar
x&.foo || bar

Constants

MSG
PLUS_MINUS_METHODS

Public Instance Methods

on_send(node) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_chain.rb, line 40
def on_send(node)
  return unless require_safe_navigation?(node)

  bad_method?(node) do |safe_nav, method|
    return if nil_methods.include?(method) || PLUS_MINUS_METHODS.include?(node.method_name)

    begin_range = node.loc.dot || safe_nav.source_range.end
    location = begin_range.join(node.source_range.end)

    add_offense(location) do |corrector|
      autocorrect(corrector, offense_range: location, send_node: node)
    end
  end
end

Private Instance Methods

add_safe_navigation_operator(offense_range:, send_node:) click to toggle source

@param [Parser::Source::Range] offense_range @param [RuboCop::AST::SendNode] send_node @return [String]

# File lib/rubocop/cop/lint/safe_navigation_chain.rb, line 67
def add_safe_navigation_operator(offense_range:, send_node:)
  source =
    if brackets?(send_node)
      format(
        '%<method_name>s(%<arguments>s)%<method_chain>s',
        arguments: send_node.arguments.map(&:source).join(', '),
        method_name: send_node.method_name,
        method_chain: send_node.source_range.end.join(send_node.source_range.end).source
      )
    else
      offense_range.source
    end
  source.prepend('.') unless source.start_with?('.')
  source.prepend('&')
end
autocorrect(corrector, offense_range:, send_node:) click to toggle source

@param [RuboCop::Cop::Corrector] corrector @param [Parser::Source::Range] offense_range @param [RuboCop::AST::SendNode] send_node

# File lib/rubocop/cop/lint/safe_navigation_chain.rb, line 86
def autocorrect(corrector, offense_range:, send_node:)
  corrector.replace(
    offense_range,
    add_safe_navigation_operator(offense_range: offense_range, send_node: send_node)
  )

  corrector.wrap(send_node, '(', ')') if require_parentheses?(send_node)
end
brackets?(send_node) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_chain.rb, line 95
def brackets?(send_node)
  send_node.method?(:[]) || send_node.method?(:[]=)
end
require_parentheses?(send_node) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_chain.rb, line 99
def require_parentheses?(send_node)
  return false unless send_node.comparison_method?
  return false unless (node = send_node.parent)

  (node.respond_to?(:logical_operator?) && node.logical_operator?) ||
    (node.respond_to?(:comparison_method?) && node.comparison_method?)
end
require_safe_navigation?(node) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_chain.rb, line 57
def require_safe_navigation?(node)
  parent = node.parent
  return true unless parent&.and_type?

  parent.rhs != node || parent.lhs.receiver != parent.rhs.receiver
end