simplecov 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +148 -5
- data/exe/simplecov +0 -3
- data/lib/minitest/simplecov_plugin.rb +12 -9
- data/lib/simplecov/atomic_file.rb +16 -19
- data/lib/simplecov/autostart.rb +5 -7
- data/lib/simplecov/baseline/parser.rb +58 -0
- data/lib/simplecov/baseline.rb +170 -0
- data/lib/simplecov/cli/affected/changed_files.rb +57 -0
- data/lib/simplecov/cli/affected/selection.rb +117 -0
- data/lib/simplecov/cli/affected.rb +148 -0
- data/lib/simplecov/cli/badge/svg.rb +70 -0
- data/lib/simplecov/cli/badge.rb +62 -0
- data/lib/simplecov/cli/clean.rb +23 -24
- data/lib/simplecov/cli/command_helpers.rb +55 -19
- data/lib/simplecov/cli/completions/scripts.rb +114 -0
- data/lib/simplecov/cli/completions.rb +83 -0
- data/lib/simplecov/cli/coverage.rb +25 -28
- data/lib/simplecov/cli/coverage_file.rb +58 -14
- data/lib/simplecov/cli/dead_code/output.rb +100 -0
- data/lib/simplecov/cli/dead_code.rb +139 -0
- data/lib/simplecov/cli/diff/output.rb +45 -0
- data/lib/simplecov/cli/diff.rb +35 -67
- data/lib/simplecov/cli/dotfile.rb +83 -38
- data/lib/simplecov/cli/git.rb +41 -0
- data/lib/simplecov/cli/history/output.rb +135 -0
- data/lib/simplecov/cli/history.rb +67 -0
- data/lib/simplecov/cli/merge.rb +34 -39
- data/lib/simplecov/cli/open.rb +12 -15
- data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
- data/lib/simplecov/cli/patch/output.rb +135 -0
- data/lib/simplecov/cli/patch.rb +169 -0
- data/lib/simplecov/cli/ratchet/output.rb +44 -0
- data/lib/simplecov/cli/ratchet.rb +83 -0
- data/lib/simplecov/cli/report.rb +18 -24
- data/lib/simplecov/cli/run.rb +8 -9
- data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
- data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
- data/lib/simplecov/cli/serve.rb +27 -24
- data/lib/simplecov/cli/show/annotator.rb +87 -0
- data/lib/simplecov/cli/show/sweep.rb +36 -0
- data/lib/simplecov/cli/show.rb +133 -0
- data/lib/simplecov/cli/status/facts.rb +87 -0
- data/lib/simplecov/cli/status.rb +93 -0
- data/lib/simplecov/cli/tests/redundancy.rb +78 -0
- data/lib/simplecov/cli/tests.rb +148 -0
- data/lib/simplecov/cli/uncovered/misses.rb +37 -0
- data/lib/simplecov/cli/uncovered.rb +70 -32
- data/lib/simplecov/cli/usage.rb +152 -0
- data/lib/simplecov/cli/watch/live_report.rb +73 -0
- data/lib/simplecov/cli/watch/narrator.rb +55 -0
- data/lib/simplecov/cli/watch/poller.rb +45 -0
- data/lib/simplecov/cli/watch/session.rb +134 -0
- data/lib/simplecov/cli/watch/test_plan.rb +43 -0
- data/lib/simplecov/cli/watch.rb +85 -0
- data/lib/simplecov/cli.rb +43 -92
- data/lib/simplecov/color.rb +19 -37
- data/lib/simplecov/combine/branches_combiner.rb +7 -31
- data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
- data/lib/simplecov/combine/identity_interner.rb +10 -13
- data/lib/simplecov/combine/interned_counts.rb +7 -9
- data/lib/simplecov/combine/lines_combiner.rb +20 -44
- data/lib/simplecov/combine/methods_combiner.rb +9 -35
- data/lib/simplecov/combine/results_combiner.rb +4 -21
- data/lib/simplecov/combine.rb +0 -6
- data/lib/simplecov/command_guesser.rb +20 -34
- data/lib/simplecov/configuration/baseline.rb +31 -0
- data/lib/simplecov/configuration/coverage.rb +91 -79
- data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
- data/lib/simplecov/configuration/deprecations.rb +21 -0
- data/lib/simplecov/configuration/eval_coverage.rb +6 -10
- data/lib/simplecov/configuration/filters.rb +40 -73
- data/lib/simplecov/configuration/formatting.rb +74 -83
- data/lib/simplecov/configuration/groups.rb +19 -13
- data/lib/simplecov/configuration/history.rb +44 -0
- data/lib/simplecov/configuration/ignored_entries.rb +31 -22
- data/lib/simplecov/configuration/merging.rb +94 -62
- data/lib/simplecov/configuration/missed_caps.rb +51 -0
- data/lib/simplecov/configuration/production.rb +23 -0
- data/lib/simplecov/configuration/test_tracking.rb +48 -0
- data/lib/simplecov/configuration/thresholds.rb +33 -62
- data/lib/simplecov/configuration/view_coverage.rb +38 -0
- data/lib/simplecov/configuration.rb +70 -76
- data/lib/simplecov/configuration_error.rb +5 -0
- data/lib/simplecov/context_map/union.rb +81 -0
- data/lib/simplecov/context_map.rb +187 -0
- data/lib/simplecov/coverage_json.rb +2 -5
- data/lib/simplecov/coverage_statistics.rb +14 -24
- data/lib/simplecov/coverage_violations.rb +157 -64
- data/lib/simplecov/current_run.rb +61 -0
- data/lib/simplecov/defaults.rb +4 -23
- data/lib/simplecov/deprecation.rb +45 -27
- data/lib/simplecov/directive.rb +46 -62
- data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
- data/lib/simplecov/exit_codes/check.rb +17 -11
- data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
- data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
- data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
- data/lib/simplecov/exit_codes.rb +16 -10
- data/lib/simplecov/exit_handling.rb +94 -59
- data/lib/simplecov/file_list.rb +20 -51
- data/lib/simplecov/filter.rb +43 -66
- data/lib/simplecov/formatter/base.rb +37 -55
- data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
- data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
- data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
- data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
- data/lib/simplecov/formatter/html_formatter.rb +24 -41
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
- data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
- data/lib/simplecov/formatter/json_formatter.rb +7 -13
- data/lib/simplecov/formatter/multi_formatter.rb +5 -11
- data/lib/simplecov/formatter/simple_formatter.rb +2 -8
- data/lib/simplecov/formatter.rb +14 -10
- data/lib/simplecov/group_names.rb +13 -12
- data/lib/simplecov/history.rb +115 -0
- data/lib/simplecov/last_run.rb +6 -9
- data/lib/simplecov/lines_classifier.rb +31 -16
- data/lib/simplecov/load_global_config.rb +7 -7
- data/lib/simplecov/parallel_adapters/base.rb +13 -41
- data/lib/simplecov/parallel_adapters/generic.rb +7 -19
- data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
- data/lib/simplecov/parallel_adapters.rb +24 -55
- data/lib/simplecov/parallel_coordination.rb +39 -49
- data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
- data/lib/simplecov/parallel_result_merger.rb +67 -126
- data/lib/simplecov/process.rb +14 -32
- data/lib/simplecov/production/error.rb +11 -0
- data/lib/simplecov/production/file_sink.rb +130 -0
- data/lib/simplecov/production.rb +249 -0
- data/lib/simplecov/profiles/rails.rb +1 -12
- data/lib/simplecov/profiles/root_filter.rb +0 -4
- data/lib/simplecov/profiles/strict.rb +9 -19
- data/lib/simplecov/profiles.rb +11 -33
- data/lib/simplecov/report_deferral.rb +11 -15
- data/lib/simplecov/report_stamp.rb +6 -9
- data/lib/simplecov/result/filter_config.rb +19 -0
- data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
- data/lib/simplecov/result/serialization.rb +43 -0
- data/lib/simplecov/result/source_file_builder.rb +7 -12
- data/lib/simplecov/result.rb +68 -117
- data/lib/simplecov/result_adapter.rb +56 -68
- data/lib/simplecov/result_merger/contexts.rb +22 -0
- data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
- data/lib/simplecov/result_merger/resultset_file.rb +20 -27
- data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
- data/lib/simplecov/result_merger/resultset_store.rb +23 -14
- data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
- data/lib/simplecov/result_merger.rb +59 -58
- data/lib/simplecov/result_processing.rb +74 -119
- data/lib/simplecov/run_identity.rb +16 -18
- data/lib/simplecov/simulate_coverage.rb +34 -50
- data/lib/simplecov/source_file/branch.rb +8 -33
- data/lib/simplecov/source_file/branch_builder.rb +28 -47
- data/lib/simplecov/source_file/builder_context.rb +6 -13
- data/lib/simplecov/source_file/line.rb +13 -33
- data/lib/simplecov/source_file/line_builder.rb +9 -15
- data/lib/simplecov/source_file/method.rb +3 -7
- data/lib/simplecov/source_file/method_builder.rb +6 -15
- data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
- data/lib/simplecov/source_file/skip_chunks.rb +19 -28
- data/lib/simplecov/source_file/source_loader.rb +45 -35
- data/lib/simplecov/source_file/statistics.rb +10 -9
- data/lib/simplecov/source_file.rb +21 -51
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +63 -114
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
- data/lib/simplecov/static_coverage_extractor/method_collector.rb +11 -24
- data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
- data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
- data/lib/simplecov/static_coverage_extractor/visitor.rb +47 -64
- data/lib/simplecov/static_coverage_extractor.rb +46 -62
- data/lib/simplecov/test_tracker/accessors.rb +34 -0
- data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
- data/lib/simplecov/test_tracker/delta.rb +69 -0
- data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
- data/lib/simplecov/test_tracker.rb +162 -0
- data/lib/simplecov/unloaded_file_injector.rb +18 -31
- data/lib/simplecov/useless_results_remover.rb +7 -6
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
- data/lib/simplecov/view_coverage.rb +55 -0
- data/lib/simplecov-html.rb +1 -0
- data/lib/simplecov.rb +57 -113
- data/lib/simplecov_json_formatter.rb +1 -0
- data/man/simplecov.1 +349 -0
- data/sig/simplecov.rbs +523 -595
- metadata +72 -5
|
@@ -4,198 +4,158 @@ require_relative "prism_compat"
|
|
|
4
4
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module StaticCoverageExtractor
|
|
7
|
-
# The source ranges Ruby's Coverage assigns to branch conditions and
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
7
|
+
# The source ranges Ruby's Coverage assigns to branch conditions and arms,
|
|
8
|
+
# resolved from Prism nodes. Simulated entries only ever merge with real
|
|
9
|
+
# entries produced by the running Ruby, and CRuby 3.4 changed several of
|
|
10
|
+
# these conventions, so every resolver here emits whichever shape this
|
|
11
|
+
# Ruby's Coverage uses (#1226, #1233). The "runtime tuple equivalence" spec
|
|
12
|
+
# exercises the module against real Coverage output on every CI Ruby, and
|
|
13
|
+
# is the authority on what each resolver should answer.
|
|
12
14
|
#
|
|
13
|
-
# rubocop:disable Metrics/ModuleLength -- one cohesive home for the
|
|
14
|
-
# per-construct, per-Ruby-version Coverage location conventions;
|
|
15
|
-
# splitting it would scatter closely-related resolvers.
|
|
16
15
|
module LocationConventions
|
|
17
16
|
LEGACY_COVERAGE_LOCATIONS = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
18
17
|
|
|
19
18
|
# A zero-width stand-in for Prism locations, for the arms Coverage
|
|
20
|
-
# anchors to a point rather than a range.
|
|
19
|
+
# anchors to a point rather than a range. Resolvers answer whichever of a
|
|
20
|
+
# node, one of its locations, or this is nearest to hand: all three
|
|
21
|
+
# answer the four position accessors the emitted tuples are built from.
|
|
21
22
|
PointLocation = Data.define(:start_line, :start_column, :end_line, :end_column)
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
private
|
|
24
25
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# Ruby's version, so no single process can cover both sides, and
|
|
28
|
-
# the legacy-only helpers are unreachable on modern Rubies (and
|
|
29
|
-
# vice versa). The "runtime tuple equivalence" spec exercises this
|
|
30
|
-
# module against real Coverage output on every CI Ruby.
|
|
26
|
+
# Which arm of each conditional below runs is fixed by the running Ruby's
|
|
27
|
+
# version, so no single process can cover both sides.
|
|
31
28
|
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
# end an `elsif` clause's range at its last content instead of the
|
|
35
|
-
# shared `end` keyword the clause doesn't own.
|
|
29
|
+
# 3.2/3.3 end an `elsif` clause's range at its last content rather than
|
|
30
|
+
# at the shared `end` keyword the clause doesn't own.
|
|
36
31
|
def if_like_location(node, type)
|
|
37
|
-
return node
|
|
32
|
+
return node unless LEGACY_COVERAGE_LOCATIONS && type.equal?(:if) && elsif_node?(node)
|
|
38
33
|
|
|
39
|
-
span(node
|
|
34
|
+
span(node, legacy_content_end(node))
|
|
40
35
|
end
|
|
41
36
|
|
|
42
37
|
def elsif_node?(node)
|
|
43
38
|
keyword = node.if_keyword_loc
|
|
44
|
-
!keyword.nil? && keyword.slice
|
|
39
|
+
!keyword.nil? && keyword.slice.eql?("elsif")
|
|
45
40
|
end
|
|
46
41
|
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# clause's predicate / `else` keyword when its body is empty.
|
|
42
|
+
# The deepest trailing clause's statements, or that clause's predicate /
|
|
43
|
+
# `else` keyword when its body is empty.
|
|
50
44
|
def legacy_content_end(node)
|
|
51
45
|
tail = node
|
|
52
|
-
while tail.
|
|
46
|
+
while tail.instance_of?(Prism::IfNode)
|
|
53
47
|
sub = PrismCompat.subsequent(tail)
|
|
54
48
|
break unless sub
|
|
55
49
|
|
|
56
50
|
tail = sub
|
|
57
51
|
end
|
|
58
|
-
return
|
|
52
|
+
return tail.statements || tail.predicate if tail.instance_of?(Prism::IfNode)
|
|
59
53
|
|
|
60
|
-
tail.statements
|
|
54
|
+
tail.statements || tail.else_keyword_loc
|
|
61
55
|
end
|
|
62
56
|
|
|
63
|
-
# Location of the then arm. Coverage uses the body statements'
|
|
64
|
-
# range; with an empty then body the arm collapses to a zero-width
|
|
65
|
-
# point at the predicate's end — always on a modern `if`, and on
|
|
66
|
-
# legacy Rubies only when the construct is in void position (a
|
|
67
|
-
# trailing statement discards its value). In value (tail) position,
|
|
68
|
-
# legacy Rubies and `unless` fall back to the node's range.
|
|
69
57
|
def if_like_then_location(node, type)
|
|
70
|
-
return node.statements
|
|
71
|
-
return point_at_end(node.predicate
|
|
58
|
+
return node.statements if node.statements
|
|
59
|
+
return point_at_end(node.predicate) if empty_arm_collapses?(node, type)
|
|
72
60
|
|
|
73
61
|
if_like_location(node, type)
|
|
74
62
|
end
|
|
75
63
|
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# per-Prism-version accessor split). When no else/elsif is present,
|
|
79
|
-
# the synthesized else inherits the condition's range (matches
|
|
80
|
-
# Coverage's convention).
|
|
64
|
+
# With no else/elsif present, the synthesized else inherits the
|
|
65
|
+
# condition's range.
|
|
81
66
|
def if_like_else_location(node, type)
|
|
82
67
|
sub = PrismCompat.subsequent(node)
|
|
83
68
|
return if_like_location(node, type) unless sub
|
|
84
|
-
# An `elsif` arrives as a nested IfNode
|
|
85
|
-
# outer else arm to the clause's own range, not its then body
|
|
86
|
-
#
|
|
87
|
-
return if_like_location(sub, :if) if sub.
|
|
88
|
-
return sub.statements
|
|
69
|
+
# An `elsif` arrives as a nested IfNode, and Coverage attributes the
|
|
70
|
+
# outer else arm to the clause's own range, not its then body (which is
|
|
71
|
+
# what created phantom unmergeable arms).
|
|
72
|
+
return if_like_location(sub, :if) if sub.instance_of?(Prism::IfNode)
|
|
73
|
+
return sub.statements if sub.statements
|
|
89
74
|
|
|
90
75
|
empty_else_location(node, sub, type)
|
|
91
76
|
end
|
|
92
77
|
|
|
93
|
-
# Location of an empty explicit `else`: a modern `if` uses the
|
|
94
|
-
# else..end span; a legacy Ruby in void position collapses to a point
|
|
95
|
-
# at the `else` keyword's end; otherwise (legacy value position, or
|
|
96
|
-
# `unless`) it uses the condition's range.
|
|
97
78
|
def empty_else_location(node, sub, type)
|
|
98
|
-
return sub
|
|
99
|
-
|
|
79
|
+
return sub if !LEGACY_COVERAGE_LOCATIONS && type.equal?(:if)
|
|
80
|
+
# Void position is a legacy distinction: a Ruby that does not need the
|
|
81
|
+
# value-position pass answers every node as value.
|
|
82
|
+
return point_at_end(sub.else_keyword_loc) unless value_position?(node)
|
|
100
83
|
|
|
101
84
|
if_like_location(node, type)
|
|
102
85
|
end
|
|
103
86
|
|
|
104
|
-
# Arm location for a when/in clause: its body statements, or — when
|
|
105
|
-
# the body is empty — the clause's own range on modern Rubies, a
|
|
106
|
-
# point at the pattern's end for a legacy `in`, and for a legacy
|
|
107
|
-
# `when` a point at the clause's end in void position or the tail
|
|
108
|
-
# convention (keyword through the case's remaining content) in value.
|
|
109
87
|
def case_arm_location(case_node, when_node, when_type)
|
|
110
|
-
return when_node.statements
|
|
111
|
-
return when_node
|
|
112
|
-
return point_at_end(when_node.pattern
|
|
113
|
-
return point_at_end(when_node
|
|
88
|
+
return when_node.statements if when_node.statements
|
|
89
|
+
return when_node unless LEGACY_COVERAGE_LOCATIONS
|
|
90
|
+
return point_at_end(when_node.pattern) if when_type.equal?(:in)
|
|
91
|
+
return point_at_end(when_node) unless value_position?(case_node)
|
|
114
92
|
|
|
115
93
|
legacy_when_value_location(case_node, when_node)
|
|
116
94
|
end
|
|
117
95
|
|
|
118
96
|
def legacy_when_value_location(case_node, when_node)
|
|
119
|
-
span(when_node
|
|
97
|
+
span(when_node, legacy_case_tail_end(case_node, when_node))
|
|
120
98
|
end
|
|
121
99
|
|
|
122
|
-
#
|
|
123
|
-
#
|
|
100
|
+
# Only a `when` reaches here, and a `when` always carries at least one
|
|
101
|
+
# condition, so the fallback is always available.
|
|
124
102
|
def legacy_case_tail_end(case_node, when_node)
|
|
125
|
-
following_case_content(case_node, when_node).last ||
|
|
126
|
-
(when_node.conditions.last || when_node).location
|
|
103
|
+
following_case_content(case_node, when_node).last || when_node.conditions.last
|
|
127
104
|
end
|
|
128
105
|
|
|
129
106
|
def following_case_content(case_node, when_node)
|
|
130
107
|
clauses = case_node.conditions
|
|
131
|
-
index = clauses.index { |clause| clause.equal?(when_node) } || 0
|
|
132
108
|
# A when-clause's own location ends where its body ends (or at its
|
|
133
|
-
# condition when empty), so the whole clause extends the range
|
|
134
|
-
# through trailing
|
|
135
|
-
|
|
109
|
+
# condition when empty), so the whole clause is what extends the range
|
|
110
|
+
# through trailing empty clauses that have no `statements`.
|
|
111
|
+
index = clauses.index { |clause| clause.equal?(when_node) }
|
|
112
|
+
content = clauses.drop(index + 1)
|
|
136
113
|
else_statements = PrismCompat.else_clause(case_node)&.statements
|
|
137
|
-
content
|
|
114
|
+
content += [else_statements] if else_statements
|
|
138
115
|
content
|
|
139
116
|
end
|
|
140
117
|
|
|
141
|
-
# Resolve the source range Coverage attributes to a synthetic-or-real
|
|
142
|
-
# `:else` arm of a case construct: the body of an explicit else,
|
|
143
|
-
# the case's full range when no else is present, and — for an
|
|
144
|
-
# explicit else with an empty body — the else..end span on modern
|
|
145
|
-
# Rubies or the case's full range on legacy ones.
|
|
146
118
|
def else_arm_location(node)
|
|
147
119
|
else_clause = PrismCompat.else_clause(node)
|
|
148
|
-
return node
|
|
149
|
-
return else_clause.statements
|
|
150
|
-
return else_clause
|
|
151
|
-
# Empty explicit `else`: a point at the `else` keyword's end in void
|
|
152
|
-
# position, the whole case's range in value position.
|
|
120
|
+
return node unless else_clause
|
|
121
|
+
return else_clause.statements if else_clause.statements
|
|
122
|
+
return else_clause unless LEGACY_COVERAGE_LOCATIONS
|
|
153
123
|
return point_at_end(else_clause.else_keyword_loc) unless value_position?(node)
|
|
154
124
|
|
|
155
|
-
node
|
|
125
|
+
node
|
|
156
126
|
end
|
|
157
127
|
|
|
158
|
-
# An empty loop body falls back to the loop's range on modern
|
|
159
|
-
# Rubies and collapses to a point at the predicate's end on legacy
|
|
160
|
-
# ones.
|
|
161
128
|
def loop_body_location(node)
|
|
162
129
|
return legacy_do_while_body_location(node) if LEGACY_COVERAGE_LOCATIONS && begin_modifier_loop?(node)
|
|
163
|
-
return node.statements
|
|
164
|
-
return point_at_end(node.predicate
|
|
130
|
+
return node.statements if node.statements
|
|
131
|
+
return point_at_end(node.predicate) if LEGACY_COVERAGE_LOCATIONS
|
|
165
132
|
|
|
166
|
-
node
|
|
133
|
+
node
|
|
167
134
|
end
|
|
168
135
|
|
|
169
|
-
#
|
|
170
|
-
#
|
|
171
|
-
# attributes the body to that whole `begin ... end` span (which the
|
|
172
|
-
# generic `node.statements.location` already yields), but 3.3 uses
|
|
173
|
-
# the begin's inner statements instead — or a point at the end of
|
|
174
|
-
# the `begin` keyword when the body is empty.
|
|
136
|
+
# The accessor arrived in Prism 0.25, and this gem supports older ones,
|
|
137
|
+
# where nothing answers the question.
|
|
175
138
|
def begin_modifier_loop?(node)
|
|
176
139
|
node.respond_to?(:begin_modifier?) && node.begin_modifier?
|
|
177
140
|
end
|
|
178
141
|
|
|
142
|
+
# `begin ... end while cond` parses as a while whose sole statement is
|
|
143
|
+
# the BeginNode. Modern Coverage attributes the body to that whole
|
|
144
|
+
# `begin ... end` span, but 3.3 uses the begin's inner statements.
|
|
179
145
|
def legacy_do_while_body_location(node)
|
|
180
|
-
begin_node = node.statements.body
|
|
146
|
+
begin_node, = node.statements.body
|
|
181
147
|
inner = begin_node.statements
|
|
182
|
-
inner
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
# Coverage's safe-navigation branch spans the receiver through the
|
|
186
|
-
#
|
|
187
|
-
#
|
|
188
|
-
# `x&.foo
|
|
189
|
-
# would without the block. `node.location` includes an attached
|
|
190
|
-
# block, so build the end position from `closing_loc` (closing
|
|
191
|
-
# paren) / `arguments` (paren-less args) / `message_loc` instead.
|
|
192
|
-
# This convention is the same on legacy and modern Rubies. See
|
|
193
|
-
# issue #1233.
|
|
148
|
+
inner || point_at_end(begin_node.begin_keyword_loc)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Coverage's safe-navigation branch spans the receiver through the end of
|
|
152
|
+
# the call's arguments (or just the message when there are none), but
|
|
153
|
+
# never includes a trailing block: `x&.foo { ... }` ends exactly where
|
|
154
|
+
# `x&.foo` would. `node.location` would include the block (#1233).
|
|
194
155
|
def safe_navigation_location(node)
|
|
195
|
-
span(node
|
|
156
|
+
span(node, node.closing_loc || node.arguments || node.message_loc)
|
|
196
157
|
end
|
|
197
158
|
|
|
198
|
-
# The range from `from`'s start through `to`'s end.
|
|
199
159
|
def span(from, to)
|
|
200
160
|
PointLocation.new(
|
|
201
161
|
start_line: from.start_line, start_column: from.start_column,
|
|
@@ -210,27 +170,22 @@ module SimpleCov
|
|
|
210
170
|
)
|
|
211
171
|
end
|
|
212
172
|
|
|
213
|
-
#
|
|
214
|
-
#
|
|
215
|
-
# legacy Coverage does it only in void position, for both.
|
|
173
|
+
# Modern Coverage collapses an empty then arm for every `if` but not
|
|
174
|
+
# `unless`; legacy Coverage does it only in void position, for both.
|
|
216
175
|
def empty_arm_collapses?(node, type)
|
|
217
|
-
return type
|
|
176
|
+
return type.equal?(:if) unless LEGACY_COVERAGE_LOCATIONS
|
|
218
177
|
|
|
219
178
|
!value_position?(node)
|
|
220
179
|
end
|
|
221
180
|
|
|
222
|
-
#
|
|
223
|
-
# legacy Rubies
|
|
224
|
-
#
|
|
225
|
-
# ValuePositions (only on legacy; nil elsewhere, which reads as
|
|
226
|
-
# "value" — the safe, pre-audit default).
|
|
181
|
+
# `@value_positions` is computed once per parse by ValuePositions, and
|
|
182
|
+
# only on legacy Rubies. Nil elsewhere, which reads as "value": the safe,
|
|
183
|
+
# pre-audit default.
|
|
227
184
|
def value_position?(node)
|
|
228
185
|
return true if @value_positions.nil?
|
|
229
186
|
|
|
230
187
|
@value_positions.key?(node)
|
|
231
188
|
end
|
|
232
|
-
# simplecov:enable
|
|
233
189
|
end
|
|
234
|
-
# rubocop:enable Metrics/ModuleLength
|
|
235
190
|
end
|
|
236
191
|
end
|
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module StaticCoverageExtractor
|
|
5
|
-
# Visitor mixin that collects method tuples and tracks the lexical
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# method-collection concern separate from branch extraction.
|
|
5
|
+
# Visitor mixin that collects method tuples and tracks the lexical class /
|
|
6
|
+
# module nesting that names them, in the shape Ruby's `Coverage` reports
|
|
7
|
+
# methods. Module and Class are both namespaces here, since `Coverage` reports
|
|
8
|
+
# both as the constant.
|
|
10
9
|
module MethodCollector
|
|
11
|
-
# Track class/module nesting so method tuples carry the lexical
|
|
12
|
-
# class name. Module + Class are both treated as namespaces here
|
|
13
|
-
# since `Coverage` reports both as the constant.
|
|
14
10
|
def visit_class_node(node)
|
|
15
11
|
with_class(constant_name(node.constant_path)) { super }
|
|
16
12
|
end
|
|
@@ -19,37 +15,28 @@ module SimpleCov
|
|
|
19
15
|
with_class(constant_name(node.constant_path)) { super }
|
|
20
16
|
end
|
|
21
17
|
|
|
22
|
-
# `def name(...)` and `def self.name(...)` both produce DefNode.
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# Visitor#visit_dead_arm).
|
|
18
|
+
# `def name(...)` and `def self.name(...)` both produce DefNode. The class
|
|
19
|
+
# context is the surrounding lexical class or module, or `Object` at the top
|
|
20
|
+
# level, matching `Coverage`'s convention. Suppression covers 3.2's folded
|
|
21
|
+
# dead arms, where nested branches stay instrumented but a `def` never
|
|
22
|
+
# registers.
|
|
28
23
|
def visit_def_node(node)
|
|
29
|
-
# simplecov:disable branch — suppression is reachable on 3.2 only
|
|
30
24
|
return super if @suppress_methods
|
|
31
25
|
|
|
32
|
-
# simplecov:enable branch
|
|
33
|
-
loc = node.location
|
|
34
26
|
class_name = @class_stack.last || "Object"
|
|
35
|
-
key = [class_name, node.name,
|
|
27
|
+
key = [class_name, node.name, node.start_line, node.start_column, node.end_line, node.end_column]
|
|
36
28
|
@methods[key] = 0
|
|
37
29
|
super
|
|
38
30
|
end
|
|
39
31
|
|
|
40
|
-
|
|
32
|
+
private
|
|
41
33
|
|
|
42
|
-
# Render a constant path (e.g., `Foo::Bar`) as its source-form
|
|
43
|
-
# string. Defensive nil / to_s fallbacks: ClassNode and ModuleNode
|
|
44
|
-
# always carry a constant_path in practice.
|
|
45
|
-
# simplecov:disable
|
|
46
34
|
def constant_name(node)
|
|
47
35
|
return "<anonymous>" if node.nil?
|
|
48
36
|
return node.slice if node.respond_to?(:slice)
|
|
49
37
|
|
|
50
38
|
node.to_s
|
|
51
39
|
end
|
|
52
|
-
# simplecov:enable
|
|
53
40
|
|
|
54
41
|
def with_class(name)
|
|
55
42
|
@class_stack.push(name)
|
|
@@ -2,51 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
module SimpleCov
|
|
4
4
|
module StaticCoverageExtractor
|
|
5
|
-
# The Prism 1.3
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# back to no simulated data (see the 1.0.2 audit).
|
|
5
|
+
# The Prism 1.3 accessor renames for the trailing clause of conditional
|
|
6
|
+
# nodes, resolved once at load so the per-node hot paths stay branch-free.
|
|
7
|
+
# Ruby 3.3's stdlib Prism predates the renames; 3.4+ and any installed prism
|
|
8
|
+
# gem post-date them, and reaching for the modern name on the old one raised
|
|
9
|
+
# NoMethodError inside the extractor, which `call` swallowed, silently
|
|
10
|
+
# falling back to no simulated data.
|
|
12
11
|
#
|
|
13
12
|
# This lives in its own file, required by every consumer, because the
|
|
14
|
-
# constants are referenced from several extractor files and defining
|
|
15
|
-
#
|
|
16
|
-
#
|
|
13
|
+
# constants are referenced from several extractor files and defining them
|
|
14
|
+
# after those files were loaded worked only while the references happened at
|
|
15
|
+
# call time: a load-order trap for the next editor.
|
|
17
16
|
module PrismCompat
|
|
18
|
-
|
|
17
|
+
extend self
|
|
19
18
|
|
|
20
|
-
# `Prism::IfNode#subsequent` was renamed from `consequent`. The
|
|
21
|
-
# not-taken arm on whichever Prism version we're on can't be
|
|
22
|
-
# exercised by our own dogfood (we only run on one Prism at a time).
|
|
23
19
|
# simplecov:disable
|
|
24
20
|
IF_NODE_SUBSEQUENT_METHOD =
|
|
25
|
-
if
|
|
21
|
+
if Prism::IfNode.method_defined?(:subsequent)
|
|
26
22
|
:subsequent
|
|
27
23
|
else
|
|
28
24
|
:consequent
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
# The same rename hit the `else` accessor on `UnlessNode`,
|
|
32
|
-
# `CaseNode`, and `CaseMatchNode` (all three: `consequent` ->
|
|
33
|
-
# `else_clause`). All three renamed together, so one constant
|
|
34
|
-
# (probed off CaseNode) covers them.
|
|
35
27
|
ELSE_CLAUSE_METHOD =
|
|
36
|
-
if
|
|
28
|
+
if Prism::CaseNode.method_defined?(:else_clause)
|
|
37
29
|
:else_clause
|
|
38
30
|
else
|
|
39
31
|
:consequent
|
|
40
32
|
end
|
|
41
33
|
# simplecov:enable
|
|
42
34
|
|
|
43
|
-
# The `else`/`elsif` clause of an if-like node (an ElseNode, or a
|
|
44
|
-
# nested IfNode for `elsif`), or the `else` clause of anything else
|
|
45
|
-
# that has one, under whichever accessor this Prism exposes.
|
|
46
35
|
def subsequent(node)
|
|
47
|
-
node.
|
|
36
|
+
node.instance_of?(Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
|
|
48
37
|
end
|
|
49
38
|
|
|
39
|
+
# The rename hit `UnlessNode`, `CaseNode`, and `CaseMatchNode` together, so
|
|
40
|
+
# one constant probed off CaseNode covers all three.
|
|
50
41
|
def else_clause(node)
|
|
51
42
|
node.public_send(ELSE_CLAUSE_METHOD)
|
|
52
43
|
end
|
|
@@ -5,33 +5,24 @@ require_relative "prism_compat"
|
|
|
5
5
|
module SimpleCov
|
|
6
6
|
module StaticCoverageExtractor
|
|
7
7
|
# Ruby 3.3 value-position analysis for the extractor's legacy branch
|
|
8
|
-
# conventions (
|
|
8
|
+
# conventions (#1233). On 3.3 the source range Coverage assigns to an empty
|
|
9
|
+
# branch arm depends on whether its construct is in value position, where its
|
|
10
|
+
# result is the method's return value, or void position, where the result is
|
|
11
|
+
# discarded. Value position keeps the whole-construct range; void collapses
|
|
12
|
+
# the arm to a point at its header's end. Ruby 3.4 dropped the distinction.
|
|
9
13
|
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# distinction, so this pass only runs on legacy Rubies.
|
|
16
|
-
#
|
|
17
|
-
# "Value position" here is narrower than general value-use: it is
|
|
18
|
-
# strictly method-return (tail) position. It reaches a node only through
|
|
19
|
-
# statement tails and `if`/`unless`/`when` arms. Assignments, blocks,
|
|
20
|
-
# lambdas, method arguments, `case/in` arms, and loop bodies all discard
|
|
21
|
-
# it (Coverage treats their empty arms as void). So `tail_children`
|
|
22
|
-
# names the constructs that forward tail position and everything else
|
|
23
|
-
# falls through to the void default.
|
|
14
|
+
# "Value position" here is strictly method-return (tail) position. It reaches
|
|
15
|
+
# a node only through statement tails and `if`/`unless`/`when` arms.
|
|
16
|
+
# Assignments, blocks, lambdas, method arguments, `case/in` arms, and loop
|
|
17
|
+
# bodies all discard it, so `tail_children` names the constructs that forward
|
|
18
|
+
# tail position and everything else falls through to the void default.
|
|
24
19
|
module ValuePositions
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
# simplecov:disable
|
|
28
|
-
# This whole pass runs only on legacy Rubies (the modern dogfood
|
|
29
|
-
# never calls it), so its lines can't be covered on the CI Ruby that
|
|
30
|
-
# enforces 100%. Behavior is pinned instead by the differential
|
|
31
|
-
# tuple-equivalence spec, which runs against real Coverage on 3.3.
|
|
20
|
+
extend self
|
|
32
21
|
|
|
33
|
-
# An identity set
|
|
34
|
-
#
|
|
22
|
+
# An identity set of the Prism nodes Coverage treats as being in value
|
|
23
|
+
# position. This pass runs natively only on legacy Rubies; elsewhere the
|
|
24
|
+
# specs drive it directly, pinned by the differential tuple-equivalence spec
|
|
25
|
+
# against real Coverage on 3.3.
|
|
35
26
|
def call(root)
|
|
36
27
|
positions = {} #: Hash[untyped, bool]
|
|
37
28
|
positions.compare_by_identity
|
|
@@ -40,7 +31,7 @@ module SimpleCov
|
|
|
40
31
|
end
|
|
41
32
|
|
|
42
33
|
def mark(node, in_value, positions)
|
|
43
|
-
return unless node.is_a?(
|
|
34
|
+
return unless node.is_a?(Prism::Node)
|
|
44
35
|
|
|
45
36
|
positions[node] = true if in_value
|
|
46
37
|
children = tail_children(node, in_value)
|
|
@@ -49,26 +40,22 @@ module SimpleCov
|
|
|
49
40
|
end
|
|
50
41
|
end
|
|
51
42
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
43
|
+
# A method body is a tail context even when the `def` itself is not, since
|
|
44
|
+
# the method still returns its last expression, so it is included regardless
|
|
45
|
+
# of `in_value`. `case/in` is intentionally absent: its `in` arms and `else`
|
|
46
|
+
# both discard tail position.
|
|
56
47
|
def tail_children(node, in_value)
|
|
57
|
-
|
|
58
|
-
return [node.body] if node.is_a?(::Prism::DefNode)
|
|
48
|
+
return [node.body] if node.instance_of?(Prism::DefNode)
|
|
59
49
|
return [] unless in_value
|
|
60
50
|
|
|
61
|
-
# `case/in` (CaseMatchNode) is intentionally not a tail construct:
|
|
62
|
-
# its `in` arms and `else` both discard tail position.
|
|
63
51
|
case node
|
|
64
|
-
when
|
|
65
|
-
when
|
|
66
|
-
when
|
|
67
|
-
when
|
|
52
|
+
when Prism::StatementsNode then [node.body.last]
|
|
53
|
+
when Prism::IfNode, Prism::UnlessNode then [node.statements, PrismCompat.subsequent(node)]
|
|
54
|
+
when Prism::CaseNode then [*node.conditions, PrismCompat.else_clause(node)]
|
|
55
|
+
when Prism::ElseNode, Prism::WhenNode, Prism::BeginNode, Prism::ProgramNode then [node.statements]
|
|
68
56
|
else []
|
|
69
57
|
end
|
|
70
58
|
end
|
|
71
|
-
# simplecov:enable
|
|
72
59
|
end
|
|
73
60
|
end
|
|
74
61
|
end
|