class RuboCop::Cop::Lint::SafeNavigationConsistency

Check to make sure that if safe navigation is used in an ‘&&` or `||` condition, consistent and appropriate safe navigation, without excess or deficiency, is used for all method calls on the same object.

@example

# bad
foo&.bar && foo&.baz

# good
foo&.bar && foo.baz

# bad
foo.bar && foo&.baz

# good
foo.bar && foo.baz

# bad
foo&.bar || foo.baz

# good
foo&.bar || foo&.baz

# bad
foo.bar || foo&.baz

# good
foo.bar || foo.baz

# bad
foo&.bar && (foobar.baz || foo&.baz)

# good
foo&.bar && (foobar.baz || foo.baz)

Constants

USE_DOT_MSG
USE_SAFE_NAVIGATION_MSG

Public Instance Methods

on_and(node) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 48
def on_and(node)
  all_operands = collect_operands(node, [])
  operand_groups = all_operands.group_by { |operand| receiver_name_as_key(operand, +'') }

  operand_groups.each_value do |grouped_operands|
    next unless (dot_op, begin_of_rest_operands = find_consistent_parts(grouped_operands))

    rest_operands = grouped_operands[begin_of_rest_operands..]
    rest_operands.each do |operand|
      next if already_appropriate_call?(operand, dot_op)

      register_offense(operand, dot_op)
    end
  end
end
Also aliased as: on_or
on_or(node)
Alias for: on_and

Private Instance Methods

already_appropriate_call?(operand, dot_op) click to toggle source

rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 96
def already_appropriate_call?(operand, dot_op)
  return true if operand.safe_navigation? && dot_op == '&.'

  (operand.dot? || operand.operator_method?) && dot_op == '.'
end
collect_operands(node, operand_nodes) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 67
def collect_operands(node, operand_nodes)
  operand_nodes(node.lhs, operand_nodes)
  operand_nodes(node.rhs, operand_nodes)

  operand_nodes
end
find_consistent_parts(grouped_operands) click to toggle source

rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 83
def find_consistent_parts(grouped_operands)
  csend_in_and, csend_in_or, send_in_and, send_in_or = most_left_indices(grouped_operands)

  if csend_in_and
    ['.', (send_in_and ? [send_in_and, csend_in_and].min : csend_in_and) + 1]
  elsif send_in_or && csend_in_or
    send_in_or < csend_in_or ? ['.', send_in_or + 1] : ['&.', csend_in_or + 1]
  elsif send_in_and && csend_in_or && send_in_and < csend_in_or
    ['.', csend_in_or]
  end
end
most_left_indices(grouped_operands) click to toggle source

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

# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 122
def most_left_indices(grouped_operands)
  indices = { csend_in_and: nil, csend_in_or: nil, send_in_and: nil, send_in_or: nil }

  grouped_operands.each_with_index do |operand, index|
    indices[:csend_in_and] ||= index if operand_in_and?(operand) && operand.csend_type?
    indices[:csend_in_or] ||= index if operand_in_or?(operand) && operand.csend_type?
    indices[:send_in_and] ||= index if operand_in_and?(operand) && !nilable?(operand)
    indices[:send_in_or] ||= index if operand_in_or?(operand) && !nilable?(operand)
  end

  indices.values
end
nilable?(node) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 152
def nilable?(node)
  node.csend_type? || nil_methods.include?(node.method_name)
end
operand_in_and?(node) click to toggle source

rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity

# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 136
def operand_in_and?(node)
  return true if node.parent.and_type?

  parent = node.parent.parent while node.parent.begin_type?

  parent&.and_type?
end
operand_in_or?(node) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 144
def operand_in_or?(node)
  return true if node.parent.or_type?

  parent = node.parent.parent while node.parent.begin_type?

  parent&.or_type?
end
operand_nodes(operand, operand_nodes) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 113
def operand_nodes(operand, operand_nodes)
  if operand.operator_keyword?
    collect_operands(operand, operand_nodes)
  elsif operand.call_type?
    operand_nodes << operand
  end
end
receiver_name_as_key(method, fully_receivers) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 74
def receiver_name_as_key(method, fully_receivers)
  if method.parent.call_type?
    receiver(method.parent, fully_receivers)
  else
    fully_receivers << method.receiver&.source.to_s
  end
end
register_offense(operand, dot_operator) click to toggle source
# File lib/rubocop/cop/lint/safe_navigation_consistency.rb, line 102
def register_offense(operand, dot_operator)
  offense_range = operand.operator_method? ? operand : operand.loc.dot
  message = dot_operator == '.' ? USE_DOT_MSG : USE_SAFE_NAVIGATION_MSG

  add_offense(offense_range, message: message) do |corrector|
    next if operand.operator_method?

    corrector.replace(operand.loc.dot, dot_operator)
  end
end