lib/simplecov/static_coverage_extractor/value_position.rb



# frozen_string_literal: true

module SimpleCov
  module StaticCoverageExtractor
    # 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.
    module ValuePositions
    module_function

      # simplecov:disable
      # This whole pass runs only on legacy Rubies (the modern dogfood
      # never calls it), so its lines can't be covered on the CI Ruby that
      # enforces 100%. Behavior is pinned instead by the differential
      # tuple-equivalence spec, which runs against real Coverage on 3.3.

      # An identity set (a `compare_by_identity` Hash used as a set) of the
      # Prism nodes Coverage treats as being in value position.
      def call(root)
        positions = {} #: Hash[untyped, bool]
        positions.compare_by_identity
        mark(root, true, positions)
        positions
      end

      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

      # 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`.
      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

      # 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.
      def subsequent(node)
        node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
      end

      def else_clause(node)
        node.public_send(ELSE_CLAUSE_METHOD)
      end
      # simplecov:enable
    end
  end
end