Module: SimpleCov::StaticCoverageExtractor::ValuePositions

Defined in:
lib/simplecov/static_coverage_extractor/value_position.rb

Overview

Ruby 3.3 value-position analysis for the extractor's legacy branch conventions (see LocationConventions and the #1233 audit).

On Ruby 3.3, the source range Coverage assigns to an EMPTY branch arm depends on whether its construct is in value position — its result is the method's return value — or void position, where the result is discarded. Value position keeps the whole-construct range; void collapses the arm to a point at its header's end. Ruby 3.4 dropped the distinction, so this pass only runs on legacy Rubies.

"Value position" here is narrower than general value-use: it is strictly method-return (tail) position. It reaches a node only through statement tails and if/unless/when arms. Assignments, blocks, lambdas, method arguments, case/in arms, and loop bodies all discard it (Coverage treats their empty arms as void). So tail_children names the constructs that forward tail position and everything else falls through to the void default.

Class Method Summary collapse

Class Method Details

.call(root) ⇒ Object

An identity set (a compare_by_identity Hash used as a set) of the Prism nodes Coverage treats as being in value position.



33
34
35
36
37
38
# File 'lib/simplecov/static_coverage_extractor/value_position.rb', line 33

def call(root)
  positions = {} #: Hash[untyped, bool]
  positions.compare_by_identity
  mark(root, true, positions)
  positions
end

.else_clause(node) ⇒ Object



76
77
78
# File 'lib/simplecov/static_coverage_extractor/value_position.rb', line 76

def else_clause(node)
  node.public_send(ELSE_CLAUSE_METHOD)
end

.mark(node, in_value, positions) ⇒ Object



40
41
42
43
44
45
46
47
48
# File 'lib/simplecov/static_coverage_extractor/value_position.rb', line 40

def mark(node, in_value, positions)
  return unless node.is_a?(::Prism::Node)

  positions[node] = true if in_value
  children = tail_children(node, in_value)
  node.compact_child_nodes.each do |child|
    mark(child, children.any? { |c| c.equal?(child) }, positions)
  end
end

.subsequent(node) ⇒ Object

The else/elsif clause of an if-like node, and the else clause of a case, under whichever accessor this Prism version exposes. case/in (CaseMatchNode) is intentionally not a tail construct: its in arms and else both discard tail position.



72
73
74
# File 'lib/simplecov/static_coverage_extractor/value_position.rb', line 72

def subsequent(node)
  node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
end

.tail_children(node, in_value) ⇒ Object

The children of node that inherit its tail position; empty for the void default. A method body is a tail context even when the def itself is not (the method still returns its last expression), so it is included regardless of in_value.



54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/simplecov/static_coverage_extractor/value_position.rb', line 54

def tail_children(node, in_value)
  # A method body is a tail context even when the `def` is not.
  return [node.body] if node.is_a?(::Prism::DefNode)
  return [] unless in_value

  case node
  when ::Prism::StatementsNode then [node.body.last]
  when ::Prism::IfNode, ::Prism::UnlessNode then [node.statements, subsequent(node)]
  when ::Prism::CaseNode then [*node.conditions, else_clause(node)]
  when ::Prism::ElseNode, ::Prism::WhenNode, ::Prism::BeginNode, ::Prism::ProgramNode then [node.statements]
  else []
  end
end