Feature #22202
openAdd Coverage.branch_stub and Coverage.method_stub as companions to Coverage.line_stub
Description
Coverage.line_stub(path) compiles a file and returns its line coverage array as if the file had been loaded but never run. SimpleCov uses this method to include never-loaded files in a report so that their lines count toward the denominator. But there is no equivalent for branch or method coverage. A tool that wants a never-loaded file to participate in branch or method totals must reconstruct the structures itself with a parser.
The difficulty is that reconstruction must match the VM byte-for-byte. Merged coverage reports key branches and methods on their full location tuples, for example [:if, id, start_line, start_col, end_line, end_col]. If a tool synthesizes a tuple whose location differs from what the VM records for the same construct, merging a synthesized entry with a real one produces a duplicate branch that no execution can ever hit. The report then shows a permanently missed branch in a fully covered file.
These location conventions are not documented and they change between Ruby versions. Cases SimpleCov has had to reverse engineer recently:
- The range of an
elsifclause changed between Ruby 3.3 and 3.4 (https://github.com/simplecov-ruby/simplecov/issues/1226). - A safe navigation call followed by a block ends its branch range at the call, excluding the block (https://github.com/simplecov-ruby/simplecov/issues/1233).
- Conditions that are compile-time literals (
if true, ternary on an integer) are folded and produce no branch at all, whilewhile truestill does. - On Ruby 3.3 the location of an empty branch arm depends on whether the construct is in value or void position. Ruby 3.4 removed this distinction.
x => patternandx in patternproduce a:casebranch on Ruby 3.3 and nothing on 3.4.
SimpleCov now carries roughly 500 lines of Prism-based reconstruction plus a differential fuzzer that compiles thousands of generated programs under real Coverage and diffs the tuples, only to keep parity with conventions the VM already knows. Each new Ruby version can silently change a convention and reintroduce this class of bug.
Proposal¶
Add two class methods to the Coverage standard library:
require "coverage"
Coverage.branch_stub("foo.rb")
# => {[:if, 0, 2, 2, 6, 5] => {[:then, 1, 3, 4, 3, 10] => 0, [:else, 2, 5, 4, 5, 10] => 0}}
Coverage.method_stub("foo.rb")
# => {["Foo", :bar, 2, 2, 4, 5] => 0}
Return value shapes are identical to Coverage.result[path][:branches] and Coverage.result[path][:methods] so existing merge code works unchanged. Like line_stub, the file is compiled but not executed, and Coverage does not need to be running. It should raise the same errors line_stub raises for missing or unparsable files.
The VM already computes these structures at compile time whenever coverage is enabled with branches: true or methods: true. line_stub is implemented by compiling the file and walking iseq.trace_points. A similar approach should work here.
I am happy to help with a patch if this direction is acceptable.