module SimpleCov::StaticCoverageExtractor::LocationConventions
def begin_modifier_loop?(node)
the begin's inner statements instead — or a point at the end of
generic `node.statements.location` already yields), but 3.3 uses
attributes the body to that whole `begin ... end` span (which the
while/until whose sole statement is the BeginNode. Modern Coverage
`begin ... end while/until cond` (the do-while form) parses as a
def begin_modifier_loop?(node) node.respond_to?(:begin_modifier?) && node.begin_modifier? end
def case_arm_location(case_node, when_node, when_type)
`when` a point at the clause's end in void position or the tail
point at the pattern's end for a legacy `in`, and for a legacy
the body is empty — the clause's own range on modern Rubies, a
Arm location for a when/in clause: its body statements, or — when
def case_arm_location(case_node, when_node, when_type) return when_node.statements.location if when_node.statements return when_node.location unless LEGACY_COVERAGE_LOCATIONS return point_at_end(when_node.pattern.location) if when_type == :in return point_at_end(when_node.location) unless value_position?(case_node) legacy_when_value_location(case_node, when_node) end
def else_arm_location(node)
explicit else with an empty body — the else..end span on modern
the case's full range when no else is present, and — for an
`: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) else_clause = node.public_send(ELSE_CLAUSE_METHOD) return node.location unless else_clause return else_clause.statements.location if else_clause.statements return else_clause.location unless LEGACY_COVERAGE_LOCATIONS # Empty explicit `else`: a point at the `else` keyword's end in void # position, the whole case's range in value position. return point_at_end(else_clause.else_keyword_loc) unless value_position?(node) node.location end
def elsif_node?(node)
def elsif_node?(node) keyword = node.if_keyword_loc !keyword.nil? && keyword.slice == "elsif" end
def empty_arm_collapses?(node, type)
end. Modern Coverage does this for every `if` (but not `unless`);
Whether an empty then arm collapses to a point at the predicate's
def empty_arm_collapses?(node, type) return type == :if unless LEGACY_COVERAGE_LOCATIONS !value_position?(node) end
def empty_else_location(node, sub, type)
at the `else` keyword's end; otherwise (legacy value position, or
else..end span; a legacy Ruby in void position collapses to a point
Location of an empty explicit `else`: a modern `if` uses the
def empty_else_location(node, sub, type) return sub.location if !LEGACY_COVERAGE_LOCATIONS && type == :if return point_at_end(sub.else_keyword_loc) if LEGACY_COVERAGE_LOCATIONS && !value_position?(node) if_like_location(node, type) end
def following_case_content(case_node, when_node)
def following_case_content(case_node, when_node) clauses = case_node.conditions index = clauses.index { |clause| clause.equal?(when_node) } || 0 # A when-clause's own location ends where its body ends (or at its # condition when empty), so the whole clause extends the range # through trailing EMPTY clauses that have no `statements`. content = clauses.drop(index + 1).map(&:location) else_statements = case_node.public_send(ELSE_CLAUSE_METHOD)&.statements content << else_statements.location if else_statements content end
def if_like_else_location(node, type)
When neither is present, the synthesized else inherits the
`IF_NODE_SUBSEQUENT_METHOD` / `ELSE_CLAUSE_METHOD` at load time).
`consequent`, both depending on Prism version (resolved to
`subsequent` / `consequent` and UnlessNode `else_clause` /
`: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, type) sub = if_like_subsequent(node) return if_like_location(node, type) unless sub # An `elsif` arrives as a nested IfNode. Coverage attributes the # outer else arm to the clause's own range, not its then body # (which is what created phantom unmergeable arms). return if_like_location(sub, :if) if sub.is_a?(::Prism::IfNode) return sub.statements.location if sub.statements empty_else_location(node, sub, type) end
def if_like_location(node, type)
end an `elsif` clause's range at its last content instead of the
CRuby uses the node's full source range for every form; 3.2/3.3
The range Coverage assigns to an if-like node itself. Modern
def if_like_location(node, type) return node.location unless LEGACY_COVERAGE_LOCATIONS && type == :if && elsif_node?(node) content_end = legacy_content_end(node) PointLocation.new( start_line: node.location.start_line, start_column: node.location.start_column, end_line: content_end.end_line, end_column: content_end.end_column ) end
def if_like_subsequent(node)
accessor this Prism version exposes (see the two *_METHOD
The `else`/`elsif` clause of an if-like node, under whichever
def if_like_subsequent(node) node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : node.public_send(ELSE_CLAUSE_METHOD) end
def if_like_then_location(node, type)
trailing statement discards its value). In value (tail) position,
legacy Rubies only when the construct is in void position (a
point at the predicate's end — always on a modern `if`, and on
range; with an empty then body the arm collapses to a zero-width
Location of the then arm. Coverage uses the body statements'
def if_like_then_location(node, type) return node.statements.location if node.statements return point_at_end(node.predicate.location) if empty_arm_collapses?(node, type) if_like_location(node, type) end
def legacy_case_tail_end(case_node, when_node)
The last body content in the case after `when_node`, falling
def legacy_case_tail_end(case_node, when_node) following_case_content(case_node, when_node).last || (when_node.conditions.last || when_node).location end
def legacy_content_end(node)
convention: the deepest trailing clause's statements, or that
Where an if/elsif chain's content ends, for the legacy range
def legacy_content_end(node) tail = node while tail.is_a?(::Prism::IfNode) sub = tail.public_send(IF_NODE_SUBSEQUENT_METHOD) break unless sub tail = sub end return (tail.statements || tail.predicate).location if tail.is_a?(::Prism::IfNode) tail.statements ? tail.statements.location : tail.else_keyword_loc end
def legacy_do_while_body_location(node)
def legacy_do_while_body_location(node) begin_node = node.statements.body.first inner = begin_node.statements inner ? inner.location : point_at_end(begin_node.begin_keyword_loc) end
def legacy_when_value_location(case_node, when_node)
def legacy_when_value_location(case_node, when_node) tail_end = legacy_case_tail_end(case_node, when_node) PointLocation.new( start_line: when_node.location.start_line, start_column: when_node.location.start_column, end_line: tail_end.end_line, end_column: tail_end.end_column ) end
def loop_body_location(node)
Rubies and collapses to a point at the predicate's end on legacy
An empty loop body falls back to the loop's range on modern
def loop_body_location(node) return legacy_do_while_body_location(node) if LEGACY_COVERAGE_LOCATIONS && begin_modifier_loop?(node) return node.statements.location if node.statements return point_at_end(node.predicate.location) if LEGACY_COVERAGE_LOCATIONS node.location end
def point_at_end(location)
def point_at_end(location) PointLocation.new( start_line: location.end_line, start_column: location.end_column, end_line: location.end_line, end_column: location.end_column ) end
def safe_navigation_location(node)
This convention is the same on legacy and modern Rubies. See
paren) / `arguments` (paren-less args) / `message_loc` instead.
block, so build the end position from `closing_loc` (closing
would without the block. `node.location` includes an attached
`x&.foo(1) { ... }` both end exactly where `x&.foo` / `x&.foo(1)`
none), but never includes a trailing block: `x&.foo { ... }` and
end of the call's arguments (or just the message when there are
Coverage's safe-navigation branch spans the receiver through the
def safe_navigation_location(node) end_loc = node.closing_loc || node.arguments&.location || node.message_loc PointLocation.new( start_line: node.location.start_line, start_column: node.location.start_column, end_line: end_loc.end_line, end_column: end_loc.end_column ) end
def value_position?(node)
ValuePositions (only on legacy; nil elsewhere, which reads as
to a point. `@value_positions` is computed once per parse by
legacy Rubies keeps an empty arm's range instead of collapsing it
Whether `node` sits in value (method-return) position, which on
def value_position?(node) return true if @value_positions.nil? @value_positions.key?(node) end