lib/simplecov/static_coverage_extractor.rb
# frozen_string_literal: true begin require "prism" rescue LoadError # Prism isn't available on this Ruby (older than 3.3 without the gem). # `StaticCoverageExtractor.available?` will return false and callers # fall back to the previous "empty hashes" behavior. end module SimpleCov # Static enumeration of the branches and methods Ruby's `Coverage` library # WOULD have reported if a file had been loaded with `branches: true` / # `methods: true`. Used by `SimulateCoverage` to backfill data for files # added via `cover` / `track_files` that were never `require`'d during the # run — so unloaded files contribute to the branch/method denominators # symmetrically with their line coverage, instead of vanishing from the # totals (see #1059). # # Implementation uses Prism (stdlib in Ruby 3.3+, gem on older Rubies). # When Prism isn't available, `available?` returns false and SimulateCoverage # falls back to the previous behavior — older Rubies keep working, just # without the synthesized data. # # The emitted shape mirrors `Coverage.result[path]` for the same file: # branches are nested as `{condition_tuple => {arm_tuple => 0, ...}}` and # methods as `{["ClassName", :name, lines/cols] => 0}`. Position info # comes from Prism's reported source locations; it doesn't always match # `Coverage`'s byte-for-byte (the two parsers report slightly different # column conventions for some constructs), but lines are reliable and # downstream consumers that key off line numbers (the HTML formatter, # SonarQube, etc.) see the data they expect. module StaticCoverageExtractor module_function # simplecov:disable branch # The Prism-unavailable arm of this ternary is unreachable when Prism # itself IS loadable — i.e., on every engine that exercises the dogfood # report. Asserted-on by callers; tested indirectly via the # `available?`-returns-false fallback path in SimulateCoverage's spec. def available? defined?(::Prism) ? true : false end # simplecov:enable branch # Parse `source` (a string of Ruby) and return a hash of the form # `{"branches" => {...}, "methods" => {...}}` matching the shape that # `Coverage.result[path]` produces. Returns nil on parse failure or # when Prism isn't available; callers should treat that as "couldn't # extract — fall back to empty hashes." def call(source) # simplecov:disable branch — `then` arm unreachable when Prism IS loadable return nil unless available? # simplecov:enable branch result = ::Prism.parse(source) return nil if result.failure? visitor = Visitor.new visitor.visit(result.value) {"branches" => visitor.branches, "methods" => visitor.methods} rescue StandardError # simplecov:disable line # Parser errors beyond the .failure? check, unsupported AST shapes, # or anything else: fall back to empty hashes rather than crashing # the whole report. Defensive; hard to trigger from a real source # input that Prism accepts at parse time. nil # simplecov:enable line end # Summarize a source file's REAL branch and method positions, for the # `:eval_generated` filter (SimpleCov.ignore_branches / # SimpleCov.ignore_methods, #1046). Returns a hash: # # { # branches: Set[start_line, ...], # e.g., [3, 12, 20] # methods: Set[[name, start_line], ...] # e.g., [[:foo, 7], [:bar, 13]] # } # # Branch matching is start_line-only rather than by the full tuple. # Static extraction and Coverage can still disagree on a branch's exact # column positions (and, for some constructs, its type), so matching on # start_line alone is the conservative choice that tolerates those # differences. Coincidental line-sharing between a real branch and an # eval-generated one will keep both, which is an acceptable # false-negative for an opt-in filter. Method matching uses # (name, start_line) since a method name is unique at any line. # # Returns nil when Prism is unavailable or parsing fails, signaling # callers to keep every Coverage entry (no false drops). def real_source_positions(source) extracted = call(source) return nil unless extracted { branches: extracted["branches"].keys.to_set { |tuple| tuple[2] }, methods: extracted["methods"].keys.to_set { |tuple| [tuple[1], tuple[2]] } } end end end # simplecov:disable branch # The `else` arm (Prism missing) is unreachable on engines where the # dogfood report runs; the Visitor class only matters when Prism is # loadable. require_relative "static_coverage_extractor/visitor" if SimpleCov::StaticCoverageExtractor.available? # simplecov:enable branch