class RuboCop::Cop::Style::CombinableDefined

defined?(foo.bar.baz)
# good
defined?(foo) && defined?(foo.bar) && defined?(foo.bar.baz)
# bad
defined?(Foo::Bar::Baz)
# good
defined?(Foo) && defined?(Foo::Bar) && defined?(Foo::Bar::Baz)
# bad
@example
not necessary to check each ancestor or component of the chain.
When checking that a nested constant or chained method is defined, it is
into a single ‘defined?`.
Checks for multiple `defined?` calls joined by `&&` that can be combined

def defined_calls(nodes)

def defined_calls(nodes)
  nodes.filter_map do |defined_node|
    subject = defined_node.first_argument
    subject if subject.type?(:const, :call)
  end
end

def lhs_range_to_remove(term)

the term as well as the subsequent `&&`/`and` operator will be removed.
If the redundant `defined?` node is the LHS of an `and` node,
def lhs_range_to_remove(term)
  source = @processed_source.buffer.source
  pos = term.source_range.end_pos
  pos += 1 until source[..pos].end_with?(*OPERATORS)
  range_with_surrounding_space(
    range: term.source_range.with(end_pos: pos + 1),
    side: :right,
    newlines: false
  )
end

def namespaces(nodes)

def namespaces(nodes)
  nodes.filter_map do |node|
    if node.respond_to?(:namespace)
      node.namespace
    elsif node.respond_to?(:receiver)
      node.receiver
    end
  end
end

def on_and(node)

def on_and(node)
  # Only register an offense if all `&&` terms are `defined?` calls
  return unless (terms = terms(node)).all?(&:defined_type?)
  calls = defined_calls(terms)
  namespaces = namespaces(calls)
  calls.each do |call|
    next unless namespaces.any?(call)
    add_offense(node) do |corrector|
      remove_term(corrector, call)
    end
  end
end

def remove_term(corrector, term)

def remove_term(corrector, term)
  term = term.parent until term.parent.and_type?
  range = if term == term.parent.children.last
            rhs_range_to_remove(term)
          else
            lhs_range_to_remove(term)
          end
  corrector.remove(range)
end

def rhs_range_to_remove(term)

the term as well as the preceding `&&`/`and` operator will be removed.
If the redundant `defined?` node is the RHS of an `and` node,
def rhs_range_to_remove(term)
  source = @processed_source.buffer.source
  pos = term.source_range.begin_pos
  pos -= 1 until source[pos, 3].start_with?(*OPERATORS)
  range_with_surrounding_space(
    range: term.source_range.with(begin_pos: pos - 1),
    side: :right,
    newlines: false
  )
end

def terms(node)

def terms(node)
  node.each_descendant.select do |descendant|
    descendant.parent.and_type? && !descendant.and_type?
  end
end