123456789_123456789_123456789_123456789_123456789_

Class: RuboCop::Cop::Lint::Utils::NilReceiverChecker

Relationships & Source Files
Inherits: Object
Defined in: lib/rubocop/cop/lint/utils/nil_receiver_checker.rb

Overview

Utility class that checks if the receiver can’t be nil.

Constant Summary

Class Method Summary

Instance Attribute Summary

Instance Method Summary

Constructor Details

.new(receiver, additional_nil_methods) ⇒ NilReceiverChecker

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 11

def initialize(receiver, additional_nil_methods)
  @receiver = receiver
  @additional_nil_methods = additional_nil_methods
  @checked_nodes = {}.compare_by_identity
  @receiver_binding_name = binding_name(receiver)
  @receiver_binding_scope = binding_scope(receiver) if @receiver_binding_name
end

Instance Attribute Details

#cant_be_nil?Boolean (readonly)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 19

def cant_be_nil?
  sole_condition_of_parent_if?(@receiver) || _cant_be_nil?(@receiver.parent, @receiver)
end

Instance Method Details

#_cant_be_nil?(node, receiver) ⇒ Boolean (private)

rubocop:disable-next Metrics

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 26

def _cant_be_nil?(node, receiver)
  return false unless node

  # For some nodes, we check their parent and then some children for these parents.
  # This is added to avoid infinite loops.
  return false if @checked_nodes.key?(node)

  @checked_nodes[node] = true

  case node.type
  when :def, :defs, :class, :module, :sclass
    return false
  when :send
    if node.receiver == receiver && same_binding_as_receiver?(node.receiver)
      return non_nil_method?(node.method_name)
    end

    node.arguments.each do |argument|
      return true if _cant_be_nil?(argument, receiver)
    end

    return true if _cant_be_nil?(node.receiver, receiver)
  when :begin
    return true if _cant_be_nil?(node.children.first, receiver)
  when :if, :case
    return true if _cant_be_nil?(node.condition, receiver)
  when :and, :or
    return true if _cant_be_nil?(node.lhs, receiver)
  when :pair
    if _cant_be_nil?(node.key, receiver) ||
       _cant_be_nil?(node.value, receiver)
      return true
    end
  when :when
    node.conditions.each do |condition|
      return true if _cant_be_nil?(condition, receiver)
    end
  when :lvasgn, :ivasgn, :cvasgn, :gvasgn, :casgn
    return true if _cant_be_nil?(node.expression, receiver)
  end

  if sequentially_reached?(node)
    node.left_siblings.reverse_each do |sibling|
      next unless sibling.is_a?(AST::Node)

      return true if _cant_be_nil?(sibling, receiver)
    end
  end

  if node.parent
    _cant_be_nil?(node.parent, receiver)
  else
    false
  end
end

#binding_name(node) (private)

The name whose binding can differ between structurally equal occurrences: a local variable (block parameters included), or a bare it call, which is how it parses when the target Ruby version is below 3.4 even though the code may run on 3.4+ where it is the implicit block parameter.

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 134

def binding_name(node)
  if node.lvar_type?
    node.children.first
  elsif node.send_type? && !node.receiver && node.method?(:it) && node.arguments.empty?
    :it
  end
end

#binding_scope(occurrence) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 142

def binding_scope(occurrence)
  node = occurrence

  while (parent = node.parent)
    case parent.type
    when :block, :numblock, :itblock
      # Only the body is inside the block's scope; the send node and the arguments of
      # the block node evaluate in the outer scope.
      return parent if parent.body.equal?(node) && block_binds_name?(parent)
    when :def, :defs, :class, :module, :sclass
      return parent
    end

    node = parent
  end

  nil
end

#block_binds_name?(block_node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 161

def block_binds_name?(block_node)
  name = @receiver_binding_name

  return true if block_node.argument_list.any? { |argument| argument.name == name }

  name == :it && block_node.arguments.empty?
end

#csend_root_receiver(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 190

def csend_root_receiver(node)
  return unless (receiver = node.receiver)

  receiver = receiver.receiver while receiver.call_type? && receiver.receiver

  receiver
end

#else_branch?(node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 180

def else_branch?(node)
  node.parent&.if_type? && node.parent.else_branch == node
end

#find_top_if(node) (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 184

def find_top_if(node)
  node = node.parent while node.elsif?

  node
end

#non_nil_condition?(condition, node) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 111

def non_nil_condition?(condition, node)
  return true if condition == node && same_binding_as_receiver?(condition)
  return false unless condition.csend_type?

  root_receiver = csend_root_receiver(condition)

  root_receiver == node && same_binding_as_receiver?(root_receiver)
end

#non_nil_method?(method_name) ⇒ Boolean (private)

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 82

def non_nil_method?(method_name)
  !NIL_METHODS.include?(method_name) && !@additional_nil_methods.include?(method_name)
end

#same_binding_as_receiver?(occurrence) ⇒ Boolean (private)

Structurally equal occurrences can be different variables: it binds to its innermost block, and a block parameter can shadow an outer variable of the same name. Evidence about such a receiver only holds when both occurrences resolve to the same binding.

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 124

def same_binding_as_receiver?(occurrence)
  return true unless @receiver_binding_name

  binding_scope(occurrence).equal?(@receiver_binding_scope)
end

#sequentially_reached?(node) ⇒ Boolean (private)

Whether control reaches node by falling through its left siblings rather than by a non-sequential entry. A resbody is entered via an exception, the ensure branch runs even after a partway raise, and the else arm of an if/elsif chain is reached by branching (its if/case siblings are walked via parent recursion instead).

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 173

def sequentially_reached?(node)
  return false if node.resbody_type?
  return false if node.parent&.ensure_type? && node.parent.branch.equal?(node)

  !else_branch?(node) || (node.if_type? && !node.elsif?)
end

#sole_condition_of_parent_if?(node) ⇒ Boolean (private)

rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity

[ GitHub ]

  
# File 'lib/rubocop/cop/lint/utils/nil_receiver_checker.rb', line 87

def sole_condition_of_parent_if?(node)
  child = node
  parent = node.parent

  while parent
    if parent.if_type?
      unless parent.unless?
        condition = parent.condition
        return true if !child.equal?(condition) && non_nil_condition?(condition, node)
      end

      parent = find_top_if(parent) if parent.elsif?
    elsif else_branch?(parent)
      # Find the top `if` for `else`.
      parent = parent.parent
    end

    child = parent
    parent = parent&.parent
  end

  false
end