class SimpleCov::StaticCoverageExtractor::Visitor

‘StaticCoverageExtractor.available?` is the runtime gate.
conventional shape. Only defined when Prism is loadable;
the file — `Coverage` uses sequential ids too, so this matches the
shape Ruby’s ‘Coverage` reports. Tuple ids are sequential across
Prism visitor that accumulates branch and method tuples in the

def arm_location(statements, fallback_location)

parent when the arm body is empty (e.g., `if cond then end`).
whose span covers the contained expressions; fall back to the
Body location for an arm. Prism's `statements` is a StatementsNode
def arm_location(statements, fallback_location)
  statements&.location || fallback_location
end

def build_tuple(type, location)

def build_tuple(type, location)
  id = @next_id
  @next_id += 1
  [type, id, location.start_line, location.start_column, location.end_line, location.end_column]
end

def constant_name(node)

simplecov:disable
always carry a constant_path in practice.
string. Defensive nil / to_s fallbacks: ClassNode and ModuleNode
Render a constant path (e.g., `Foo::Bar`) as its source-form
def constant_name(node)
  return "<anonymous>" if node.nil?
  return node.slice if node.respond_to?(:slice)
  node.to_s
end

def else_arm_location(node)

or the case's full range when no else is present.
`:else` arm of a case construct: the body of an explicit else,
Resolve the source range Coverage attributes to a synthetic-or-real
def else_arm_location(node)
  return node.location unless node.else_clause
  arm_location(else_body_of(node.else_clause), node.else_clause.location)
end

def else_body_of(else_node)

in here in practice responds to `:statements`.
The `else_node` fallback is defensive: every Prism node passed
simplecov:disable branch
def else_body_of(else_node)
  else_node.respond_to?(:statements) ? else_node.statements : else_node
end

def emit_case_like(node, when_type)

def emit_case_like(node, when_type)
  arms = node.conditions.to_h do |when_node|
    loc = arm_location(when_node.statements, when_node.location)
    [build_tuple(when_type, loc), 0]
  end
  arms[build_tuple(:else, else_arm_location(node))] = 0
  @branches[build_tuple(:case, node.location)] = arms
end

def emit_if_like(node)

accessors. `if_like_else_location` hides that split.
optional else/elsif) but expose the trailing arm under different
IfNode and UnlessNode share a shape (predicate + then body +
def emit_if_like(node)
  then_loc = arm_location(node.statements, node.location)
  else_loc = if_like_else_location(node)
  @branches[build_tuple(:if, node.location)] = {
    build_tuple(:then, then_loc) => 0,
    build_tuple(:else, else_loc) => 0
  }
end

def emit_loop(node, type)

def emit_loop(node, type)
  cond_tuple = build_tuple(type, node.location)
  body_loc = arm_location(node.statements, node.location)
  @branches[cond_tuple] = {build_tuple(:body, body_loc) => 0}
end

def if_like_else_location(node)

convention).
inherits the whole condition's range (matches Coverage's
`else_clause`. When neither is present, the synthesized else
to `IF_NODE_SUBSEQUENT_METHOD` at load time); UnlessNode uses
`subsequent` / `consequent` depending on Prism version (resolved
`:else` arm of an if-like construct. IfNode uses
Resolve the source range Coverage attributes to a real-or-synthetic
def if_like_else_location(node)
  sub = node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : node.else_clause
  return node.location unless sub
  arm_location(else_body_of(sub), sub.location)
end

def initialize

def initialize
  super
  @branches = {}
  @methods = {}
  @next_id = 0
  @class_stack = []
end

def visit_case_match_node(node)

def visit_case_match_node(node)
  emit_case_like(node, :in)
  super
end

def visit_case_node(node)

Coverage synthesizes one at the case's range.
and CaseMatchNode respectively. When there's no explicit `else`,
`case`/`when` and `case`/`in` (pattern matching) parse as CaseNode
def visit_case_node(node)
  emit_case_like(node, :when)
  super
end

def visit_class_node(node)

since `Coverage` reports both as the constant.
class name. Module + Class are both treated as namespaces here
Track class/module nesting so method tuples carry the lexical
def visit_class_node(node)
  with_class(constant_name(node.constant_path)) { super }
end

def visit_def_node(node)

`Object` at the top level, matching `Coverage`'s convention).
The class context is the surrounding lexical class/module (or
`def name(...)` and `def self.name(...)` both produce DefNode.
def visit_def_node(node)
  loc = node.location
  class_name = @class_stack.last || "Object"
  key = [class_name, node.name, loc.start_line, loc.start_column, loc.end_line, loc.end_column]
  @methods[key] = 0
  super
end

def visit_if_node(node)

whole condition's range — we do the same.
missing, Coverage synthesizes a `:else` arm attributed to the
`else`, another IfNode for `elsif`). When the subsequent is
statements body) and an optional `subsequent` (an ElseNode for
as IfNode (or UnlessNode). Both carry a `then` arm (the
`if` / `unless` / postfix-if / postfix-unless / ternary all parse
def visit_if_node(node)
  emit_if_like(node)
  super
end

def visit_module_node(node)

def visit_module_node(node)
  with_class(constant_name(node.constant_path)) { super }
end

def visit_unless_node(node)

def visit_unless_node(node)
  emit_if_like(node)
  super
end

def visit_until_node(node)

def visit_until_node(node)
  emit_loop(node, :until)
  super
end

def visit_while_node(node)

else (the loop either runs the body or doesn't).
`while` / `until` loops get a single `:body` arm. No synthetic
def visit_while_node(node)
  emit_loop(node, :while)
  super
end

def with_class(name)

def with_class(name)
  @class_stack.push(name)
  yield
ensure
  @class_stack.pop
end