Class: SimpleCov::SourceFile::BranchBuilder
| Relationships & Source Files | |
| Inherits: | Object |
| Defined in: | lib/simplecov/source_file/branch_builder.rb |
Overview
Builds the Branch objects for a source file from
the raw branch data Ruby's Coverage library reports. Applies the
ignore_branches :eval_generated / :implicit_else filters and
marks branches inside # simplecov:disable / # :nocov: chunks
as skipped.
Class Method Summary
- .new(source_file) ⇒ BranchBuilder constructor
Instance Method Summary
- #call
- #build_branch(branch_data, hit_count, condition_range) private
- #build_branches_from(condition, branches) private
-
#eval_generated_condition_to_ignore?(condition) ⇒ Boolean
private
Detect a Coverage-reported branch condition that originates from
eval/module_eval/class_eval/instance_evalrather than from the file's literal source. -
#implicit_else_to_ignore?(type, branch_range, condition_range) ⇒ Boolean
private
Detect synthetic
:elsebranches that Ruby's Coverage library reports for constructs with no literalelsekeyword in source (case/in/case/whenwithout else, {||=},&&=,if/unlesswithout else, and the postfixreturn if condshape). - #process_skipped(branches) private
Constructor Details
.new(source_file) ⇒ BranchBuilder
# File 'lib/simplecov/source_file/branch_builder.rb', line 11
def initialize(source_file) @source_file = source_file end
Instance Method Details
#build_branch(branch_data, hit_count, condition_range) (private)
[ GitHub ]# File 'lib/simplecov/source_file/branch_builder.rb', line 63
def build_branch(branch_data, hit_count, condition_range) type, _id, start_line, start_col, end_line, end_col = branch_data return nil if implicit_else_to_ignore?(type, [start_line, start_col, end_line, end_col], condition_range) SourceFile::Branch.new( start_line: start_line, end_line: end_line, coverage: hit_count, inline: start_line == condition_range.first, type: type ) end
#build_branches_from(condition, branches) (private)
[ GitHub ]# File 'lib/simplecov/source_file/branch_builder.rb', line 50
def build_branches_from(condition, branches) # the format handed in from the coverage data is like this: # # [:then, 4, 6, 6, 6, 10] # # which is [type, id, start_line, start_col, end_line, end_col] _condition_type, _condition_id, *condition_range = RubyDataParser.call(condition) branches.filter_map do |branch_data, hit_count| build_branch(RubyDataParser.call(branch_data), hit_count, condition_range) end end
#call
[ GitHub ]# File 'lib/simplecov/source_file/branch_builder.rb', line 15
def call coverage_branch_data = @source_file.coverage_data["branches"] || {} branches = coverage_branch_data.flat_map do |condition, coverage_branches| next [] if eval_generated_condition_to_ignore?(condition) build_branches_from(condition, coverage_branches) end process_skipped(branches) end
#eval_generated_condition_to_ignore?(condition) ⇒ Boolean (private)
Detect a Coverage-reported branch condition that originates from
eval/module_eval/class_eval/instance_eval rather than from
the file's literal source. Coverage attributes such branches to the
caller's __FILE__/__LINE__, so a Rails delegate
call surfaces inside the source file as if there were branches at
the :foo, to: :bardelegate line. Prism never sees those branches in the static
source, so a condition whose start_line isn't in the real-source
branch set must be eval-generated. Only consulted when the user has
opted in via SimpleCov.ignore_branches :eval_generated. See #1046.
# File 'lib/simplecov/source_file/branch_builder.rb', line 37
def eval_generated_condition_to_ignore?(condition) return false unless SimpleCov.ignored_branch?(:eval_generated) positions = @source_file.real_source_positions # simplecov:disable branch — nil branch fires only when Prism is unavailable return false unless positions # simplecov:enable branch _type, _id, start_line, * = RubyDataParser.call(condition) !positions[:branches].include?(start_line) end
#implicit_else_to_ignore?(type, branch_range, condition_range) ⇒ Boolean (private)
Detect synthetic :else branches that Ruby's Coverage library reports
for constructs with no literal else keyword in source (case/in /
case/when without else, {||=}, &&=, if/unless without else,
and the postfix return if cond shape). The signal is structural:
a synthetic else reuses its parent condition's full source range
(start_line, start_col, end_line, end_col all identical), while an
explicit else arm carries a narrower range — its own keyword/body
position rather than the whole conditional. Comparing the full range
(not just start_line) is what distinguishes a ternary's explicit
else on the same line as the condition — arg == 42 ? ,
where the else's columns differ from the parent's — from a postfix
:yes : :noreturn if cond where the synthetic else inherits the full range.
Only consulted when the user has opted in via
SimpleCov.ignore_branches :implicit_else. See #1033.
# File 'lib/simplecov/source_file/branch_builder.rb', line 90
def implicit_else_to_ignore?(type, branch_range, condition_range) return false unless type == :else return false unless SimpleCov.ignored_branch?(:implicit_else) branch_range == condition_range end
#process_skipped(branches) (private)
[ GitHub ]# File 'lib/simplecov/source_file/branch_builder.rb', line 97
def process_skipped(branches) chunks = @source_file.skip_chunks_for(:branch) return branches if chunks.empty? # A non-inline branch's source range starts on its arm body (e.g. the # `:yes` line of `if cond / :yes / else / :no / end`), but `report_line` # is the condition line above it — that's where the user sees the # branch in the report and where they would naturally place an inline # `# simplecov:disable branch` directive. Honour both. branches.each do |branch| branch.skipped! if chunks.any? { |chunk| branch.overlaps_with?(chunk) || chunk.include?(branch.report_line) } end branches end