123456789_123456789_123456789_123456789_123456789_

Class: SimpleCov::StaticCoverageExtractor::Visitor

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
self, Prism::Visitor
Instance Chain:
Inherits: Prism::Visitor
  • Object
Defined in: lib/simplecov/static_coverage_extractor/visitor.rb

Overview

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

Constant Summary

LocationConventions - Included

LEGACY_COVERAGE_LOCATIONS

ConditionFolding - Included

STATIC_CONDITION_TYPES

Class Method Summary

Instance Attribute Summary

Instance Method Summary

ConditionFolding - Included

#static_condition?

Parentheses are transparent to the fold (if (1) folds like if 1), so see through a single parenthesized expression.

#unwrap_parentheses

LocationConventions - Included

#begin_modifier_loop?

begin ...

#case_arm_location

Arm location for a when/in clause: its body statements, or — when the body is empty — the clause's own range on modern Rubies, a point at the pattern's end for a legacy in, and for a legacy when a point at the clause's end in void position or the tail convention (keyword through the case's remaining content) in value.

#else_arm_location

Resolve the source range Coverage attributes to a synthetic-or-real :else arm of a case construct: the body of an explicit else, the case's full range when no else is present, and — for an explicit else with an empty body — the else..end span on modern Rubies or the case's full range on legacy ones.

#elsif_node?,
#empty_arm_collapses?

Whether an empty then arm collapses to a point at the predicate's end.

#empty_else_location

Location of an empty explicit else: a modern if uses the else..end span; a legacy Ruby in void position collapses to a point at the else keyword's end; otherwise (legacy value position, or unless) it uses the condition's range.

#following_case_content,
#if_like_else_location

Resolve the source range Coverage attributes to a real-or-synthetic :else arm of an if-like construct.

#if_like_location

The range Coverage assigns to an if-like node itself.

#if_like_subsequent

The else/elsif clause of an if-like node, under whichever accessor this Prism version exposes (see the two *_METHOD constants).

#if_like_then_location

Location of the then arm.

#legacy_case_tail_end

The last body content in the case after when_node, falling back to the clause's final condition value.

#legacy_content_end

Where an if/elsif chain's content ends, for the legacy range convention: the deepest trailing clause's statements, or that clause's predicate / else keyword when its body is empty.

#legacy_do_while_body_location, #legacy_when_value_location,
#loop_body_location

An empty loop body falls back to the loop's range on modern Rubies and collapses to a point at the predicate's end on legacy ones.

#point_at_end,
#safe_navigation_location

Coverage's safe-navigation branch spans the receiver through the end of the call's arguments (or just the message when there are none), but never includes a trailing block: x&.foo { ... } and x&.foo(1) { ... } both end exactly where x&.foo / x&.foo(1) would without the block.

#value_position?

Whether node sits in value (method-return) position, which on legacy Rubies keeps an empty arm's range instead of collapsing it to a point.

MethodCollector - Included

#visit_class_node

Track class/module nesting so method tuples carry the lexical class name.

#visit_def_node

def name(...) and def self.name(...) both produce DefNode.

#visit_module_node,
#constant_name

Render a constant path (e.g., Foo::Bar) as its source-form string.

#with_class

simplecov:enable.

Constructor Details

.newVisitor

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 60

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

Instance Attribute Details

#branches (readonly)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 58

attr_reader :branches, :methods

#methods (readonly)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 58

attr_reader :branches, :methods

Instance Method Details

#build_tuple(type, location) (private)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 189

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

#emit_case_like(node, when_type) (private)

simplecov:enable

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 176

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

#emit_if_like(node, type) (private)

IfNode and UnlessNode share a shape (predicate + then body + optional else/elsif) but expose the trailing arm under different accessors. if_like_else_location hides that split.

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 150

def emit_if_like(node, type)
  then_loc = if_like_then_location(node, type)
  else_loc = if_like_else_location(node, type)
  @branches[build_tuple(type, if_like_location(node, type))] = {
    build_tuple(:then, then_loc) => 0,
    build_tuple(:else, else_loc) => 0
  }
end

#emit_loop(node, type) (private)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 184

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

#emit_oneline_pattern(node, else_location) (private)

simplecov:disable — legacy-only (3.4 emits no branch for one-line patterns)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 168

def emit_oneline_pattern(node, else_location)
  @branches[build_tuple(:case, node.location)] = {
    build_tuple(:in, node.pattern.location) => 0,
    build_tuple(:else, else_location) => 0
  }
end

#emit_safe_navigation(node) (private)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 159

def emit_safe_navigation(node)
  loc = safe_navigation_location(node)
  @branches[build_tuple(:"&.", loc)] = {
    build_tuple(:then, loc) => 0,
    build_tuple(:else, loc) => 0
  }
end

#visit_call_node(node)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 97

def visit_call_node(node)
  emit_safe_navigation(node) if node.respond_to?(:safe_navigation?) && node.safe_navigation?
  super
end

#visit_case_match_node(node)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 110

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

#visit_case_node(node)

case/when and case/in (pattern matching) parse as CaseNode and CaseMatchNode respectively. When there's no explicit else, Coverage synthesizes one at the case's range.

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 105

def visit_case_node(node)
  emit_case_like(node, :when)
  super
end

#visit_if_node(node)

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

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 87

def visit_if_node(node)
  emit_if_like(node, :if) unless static_condition?(node.predicate)
  super
end

#visit_match_predicate_node(node)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 127

def visit_match_predicate_node(node)
  emit_oneline_pattern(node, node.pattern.location) if LEGACY_COVERAGE_LOCATIONS
  super
end

#visit_match_required_node(node)

One-line pattern matching: x => pattern (MatchRequiredNode) and x in pattern (MatchPredicateNode). Ruby 3.3's Coverage reports these as a :case with an :in and an :else arm; 3.4 dropped them entirely (no branch), so this is legacy-only. The two forms differ only in where Coverage anchors the synthesized :else: => uses the whole expression, in uses just the pattern. simplecov:disable branch — legacy-only arms; unreachable on the modern dogfood Ruby

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 122

def visit_match_required_node(node)
  emit_oneline_pattern(node, node.location) if LEGACY_COVERAGE_LOCATIONS
  super
end

#visit_program_node(node)

Entry point for a parsed file. On legacy Rubies the location of an empty branch arm depends on whether its construct is in value (tail) position, so precompute that once for the whole tree before emitting anything. Modern Rubies don't need it (see LocationConventions), so the pass is skipped there.

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 74

def visit_program_node(node)
  # simplecov:disable branch — legacy-only arm; unreachable on the modern dogfood Ruby
  @value_positions = ValuePositions.call(node) if LEGACY_COVERAGE_LOCATIONS
  # simplecov:enable branch
  super
end

#visit_unless_node(node)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 92

def visit_unless_node(node)
  emit_if_like(node, :unless) unless static_condition?(node.predicate)
  super
end

#visit_until_node(node)

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 140

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

#visit_while_node(node)

while / until loops get a single :body arm. No synthetic else (the loop either runs the body or doesn't).

[ GitHub ]

  
# File 'lib/simplecov/static_coverage_extractor/visitor.rb', line 135

def visit_while_node(node)
  emit_loop(node, :while)
  super
end