simplecov 1.0.0 → 1.0.2
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/lib/simplecov/cli/coverage.rb +5 -3
- data/lib/simplecov/cli/diff.rb +2 -1
- data/lib/simplecov/cli/dotfile.rb +9 -4
- data/lib/simplecov/cli/merge.rb +5 -3
- data/lib/simplecov/cli/report.rb +11 -6
- data/lib/simplecov/cli/run.rb +1 -1
- data/lib/simplecov/cli/serve.rb +17 -7
- data/lib/simplecov/cli/uncovered.rb +2 -1
- data/lib/simplecov/color.rb +6 -1
- data/lib/simplecov/combine/branches_combiner.rb +2 -1
- data/lib/simplecov/combine/files_combiner.rb +42 -2
- data/lib/simplecov/combine/methods_combiner.rb +27 -4
- data/lib/simplecov/combine/results_combiner.rb +2 -1
- data/lib/simplecov/configuration/coverage.rb +6 -2
- data/lib/simplecov/configuration/coverage_criteria.rb +3 -1
- data/lib/simplecov/configuration/formatting.rb +17 -7
- data/lib/simplecov/configuration/thresholds.rb +13 -9
- data/lib/simplecov/configuration.rb +22 -13
- data/lib/simplecov/coverage_statistics.rb +4 -1
- data/lib/simplecov/coverage_violations.rb +2 -1
- data/lib/simplecov/defaults.rb +3 -3
- data/lib/simplecov/directive.rb +8 -3
- data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +1 -1
- data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +1 -1
- data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +1 -1
- data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +1 -1
- data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +3 -3
- data/lib/simplecov/exit_codes.rb +12 -0
- data/lib/simplecov/exit_handling.rb +3 -3
- data/lib/simplecov/file_list.rb +12 -5
- data/lib/simplecov/formatter/base.rb +2 -1
- data/lib/simplecov/formatter/html_formatter.rb +13 -7
- data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +9 -3
- data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +1 -1
- data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +1 -1
- data/lib/simplecov/formatter/json_formatter.rb +4 -2
- data/lib/simplecov/formatter/multi_formatter.rb +7 -2
- data/lib/simplecov/formatter/simple_formatter.rb +3 -1
- data/lib/simplecov/process.rb +22 -0
- data/lib/simplecov/result_adapter.rb +45 -10
- data/lib/simplecov/result_processing.rb +4 -4
- data/lib/simplecov/simulate_coverage.rb +2 -2
- data/lib/simplecov/source_file/line.rb +2 -2
- data/lib/simplecov/source_file/method.rb +1 -1
- data/lib/simplecov/source_file/method_builder.rb +3 -1
- data/lib/simplecov/source_file/source_loader.rb +2 -2
- data/lib/simplecov/source_file.rb +2 -2
- data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
- data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
- data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
- data/lib/simplecov/static_coverage_extractor/visitor.rb +71 -46
- data/lib/simplecov/version.rb +1 -1
- data/lib/simplecov.rb +24 -11
- data/sig/simplecov.rbs +1638 -0
- metadata +7 -3
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# The source ranges Ruby's Coverage assigns to branch conditions and
|
|
6
|
+
# arms, resolved from Prism nodes. Simulated entries only ever merge
|
|
7
|
+
# with real entries produced by the running Ruby, and CRuby 3.4
|
|
8
|
+
# changed several of these conventions, so every resolver here emits
|
|
9
|
+
# whichever shape this Ruby's Coverage uses. See issues #1226 / #1233.
|
|
10
|
+
#
|
|
11
|
+
# rubocop:disable Metrics/ModuleLength -- one cohesive home for the
|
|
12
|
+
# per-construct, per-Ruby-version Coverage location conventions;
|
|
13
|
+
# splitting it would scatter closely-related resolvers.
|
|
14
|
+
module LocationConventions
|
|
15
|
+
LEGACY_COVERAGE_LOCATIONS = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
|
|
16
|
+
|
|
17
|
+
# A zero-width stand-in for Prism locations, for the arms Coverage
|
|
18
|
+
# anchors to a point rather than a range.
|
|
19
|
+
PointLocation = Data.define(:start_line, :start_column, :end_line, :end_column)
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# simplecov:disable
|
|
24
|
+
# Which arm of each conditional below runs is fixed by the running
|
|
25
|
+
# Ruby's version, so no single process can cover both sides, and
|
|
26
|
+
# the legacy-only helpers are unreachable on modern Rubies (and
|
|
27
|
+
# vice versa). The "runtime tuple equivalence" spec exercises this
|
|
28
|
+
# module against real Coverage output on every CI Ruby.
|
|
29
|
+
|
|
30
|
+
# The range Coverage assigns to an if-like node itself. Modern
|
|
31
|
+
# CRuby uses the node's full source range for every form; 3.2/3.3
|
|
32
|
+
# end an `elsif` clause's range at its last content instead of the
|
|
33
|
+
# shared `end` keyword the clause doesn't own.
|
|
34
|
+
def if_like_location(node, type)
|
|
35
|
+
return node.location unless LEGACY_COVERAGE_LOCATIONS && type == :if && elsif_node?(node)
|
|
36
|
+
|
|
37
|
+
content_end = legacy_content_end(node)
|
|
38
|
+
PointLocation.new(
|
|
39
|
+
start_line: node.location.start_line, start_column: node.location.start_column,
|
|
40
|
+
end_line: content_end.end_line, end_column: content_end.end_column
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def elsif_node?(node)
|
|
45
|
+
keyword = node.if_keyword_loc
|
|
46
|
+
!keyword.nil? && keyword.slice == "elsif"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Where an if/elsif chain's content ends, for the legacy range
|
|
50
|
+
# convention: the deepest trailing clause's statements, or that
|
|
51
|
+
# clause's predicate / `else` keyword when its body is empty.
|
|
52
|
+
def legacy_content_end(node)
|
|
53
|
+
tail = node
|
|
54
|
+
while tail.is_a?(::Prism::IfNode)
|
|
55
|
+
sub = tail.public_send(IF_NODE_SUBSEQUENT_METHOD)
|
|
56
|
+
break unless sub
|
|
57
|
+
|
|
58
|
+
tail = sub
|
|
59
|
+
end
|
|
60
|
+
return (tail.statements || tail.predicate).location if tail.is_a?(::Prism::IfNode)
|
|
61
|
+
|
|
62
|
+
tail.statements ? tail.statements.location : tail.else_keyword_loc
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Location of the then arm. Coverage uses the body statements'
|
|
66
|
+
# range; with an empty then body the arm collapses to a zero-width
|
|
67
|
+
# point at the predicate's end — always on a modern `if`, and on
|
|
68
|
+
# legacy Rubies only when the construct is in void position (a
|
|
69
|
+
# trailing statement discards its value). In value (tail) position,
|
|
70
|
+
# legacy Rubies and `unless` fall back to the node's range.
|
|
71
|
+
def if_like_then_location(node, type)
|
|
72
|
+
return node.statements.location if node.statements
|
|
73
|
+
return point_at_end(node.predicate.location) if empty_arm_collapses?(node, type)
|
|
74
|
+
|
|
75
|
+
if_like_location(node, type)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Resolve the source range Coverage attributes to a real-or-synthetic
|
|
79
|
+
# `:else` arm of an if-like construct. IfNode uses
|
|
80
|
+
# `subsequent` / `consequent` and UnlessNode `else_clause` /
|
|
81
|
+
# `consequent`, both depending on Prism version (resolved to
|
|
82
|
+
# `IF_NODE_SUBSEQUENT_METHOD` / `ELSE_CLAUSE_METHOD` at load time).
|
|
83
|
+
# When neither is present, the synthesized else inherits the
|
|
84
|
+
# condition's range (matches Coverage's convention).
|
|
85
|
+
def if_like_else_location(node, type)
|
|
86
|
+
sub = if_like_subsequent(node)
|
|
87
|
+
return if_like_location(node, type) unless sub
|
|
88
|
+
# An `elsif` arrives as a nested IfNode. Coverage attributes the
|
|
89
|
+
# outer else arm to the clause's own range, not its then body
|
|
90
|
+
# (which is what created phantom unmergeable arms).
|
|
91
|
+
return if_like_location(sub, :if) if sub.is_a?(::Prism::IfNode)
|
|
92
|
+
return sub.statements.location if sub.statements
|
|
93
|
+
|
|
94
|
+
empty_else_location(node, sub, type)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Location of an empty explicit `else`: a modern `if` uses the
|
|
98
|
+
# else..end span; a legacy Ruby in void position collapses to a point
|
|
99
|
+
# at the `else` keyword's end; otherwise (legacy value position, or
|
|
100
|
+
# `unless`) it uses the condition's range.
|
|
101
|
+
def empty_else_location(node, sub, type)
|
|
102
|
+
return sub.location if !LEGACY_COVERAGE_LOCATIONS && type == :if
|
|
103
|
+
return point_at_end(sub.else_keyword_loc) if LEGACY_COVERAGE_LOCATIONS && !value_position?(node)
|
|
104
|
+
|
|
105
|
+
if_like_location(node, type)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Arm location for a when/in clause: its body statements, or — when
|
|
109
|
+
# the body is empty — the clause's own range on modern Rubies, a
|
|
110
|
+
# point at the pattern's end for a legacy `in`, and for a legacy
|
|
111
|
+
# `when` a point at the clause's end in void position or the tail
|
|
112
|
+
# convention (keyword through the case's remaining content) in value.
|
|
113
|
+
def case_arm_location(case_node, when_node, when_type)
|
|
114
|
+
return when_node.statements.location if when_node.statements
|
|
115
|
+
return when_node.location unless LEGACY_COVERAGE_LOCATIONS
|
|
116
|
+
return point_at_end(when_node.pattern.location) if when_type == :in
|
|
117
|
+
return point_at_end(when_node.location) unless value_position?(case_node)
|
|
118
|
+
|
|
119
|
+
legacy_when_value_location(case_node, when_node)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def legacy_when_value_location(case_node, when_node)
|
|
123
|
+
tail_end = legacy_case_tail_end(case_node, when_node)
|
|
124
|
+
PointLocation.new(
|
|
125
|
+
start_line: when_node.location.start_line, start_column: when_node.location.start_column,
|
|
126
|
+
end_line: tail_end.end_line, end_column: tail_end.end_column
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# The last body content in the case after `when_node`, falling
|
|
131
|
+
# back to the clause's final condition value.
|
|
132
|
+
def legacy_case_tail_end(case_node, when_node)
|
|
133
|
+
following_case_content(case_node, when_node).last ||
|
|
134
|
+
(when_node.conditions.last || when_node).location
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def following_case_content(case_node, when_node)
|
|
138
|
+
clauses = case_node.conditions
|
|
139
|
+
index = clauses.index { |clause| clause.equal?(when_node) } || 0
|
|
140
|
+
# A when-clause's own location ends where its body ends (or at its
|
|
141
|
+
# condition when empty), so the whole clause extends the range
|
|
142
|
+
# through trailing EMPTY clauses that have no `statements`.
|
|
143
|
+
content = clauses.drop(index + 1).map(&:location)
|
|
144
|
+
else_statements = case_node.public_send(ELSE_CLAUSE_METHOD)&.statements
|
|
145
|
+
content << else_statements.location if else_statements
|
|
146
|
+
content
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Resolve the source range Coverage attributes to a synthetic-or-real
|
|
150
|
+
# `:else` arm of a case construct: the body of an explicit else,
|
|
151
|
+
# the case's full range when no else is present, and — for an
|
|
152
|
+
# explicit else with an empty body — the else..end span on modern
|
|
153
|
+
# Rubies or the case's full range on legacy ones.
|
|
154
|
+
def else_arm_location(node)
|
|
155
|
+
else_clause = node.public_send(ELSE_CLAUSE_METHOD)
|
|
156
|
+
return node.location unless else_clause
|
|
157
|
+
return else_clause.statements.location if else_clause.statements
|
|
158
|
+
return else_clause.location unless LEGACY_COVERAGE_LOCATIONS
|
|
159
|
+
# Empty explicit `else`: a point at the `else` keyword's end in void
|
|
160
|
+
# position, the whole case's range in value position.
|
|
161
|
+
return point_at_end(else_clause.else_keyword_loc) unless value_position?(node)
|
|
162
|
+
|
|
163
|
+
node.location
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# An empty loop body falls back to the loop's range on modern
|
|
167
|
+
# Rubies and collapses to a point at the predicate's end on legacy
|
|
168
|
+
# ones.
|
|
169
|
+
def loop_body_location(node)
|
|
170
|
+
return legacy_do_while_body_location(node) if LEGACY_COVERAGE_LOCATIONS && begin_modifier_loop?(node)
|
|
171
|
+
return node.statements.location if node.statements
|
|
172
|
+
return point_at_end(node.predicate.location) if LEGACY_COVERAGE_LOCATIONS
|
|
173
|
+
|
|
174
|
+
node.location
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# `begin ... end while/until cond` (the do-while form) parses as a
|
|
178
|
+
# while/until whose sole statement is the BeginNode. Modern Coverage
|
|
179
|
+
# attributes the body to that whole `begin ... end` span (which the
|
|
180
|
+
# generic `node.statements.location` already yields), but 3.3 uses
|
|
181
|
+
# the begin's inner statements instead — or a point at the end of
|
|
182
|
+
# the `begin` keyword when the body is empty.
|
|
183
|
+
def begin_modifier_loop?(node)
|
|
184
|
+
node.respond_to?(:begin_modifier?) && node.begin_modifier?
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def legacy_do_while_body_location(node)
|
|
188
|
+
begin_node = node.statements.body.first
|
|
189
|
+
inner = begin_node.statements
|
|
190
|
+
inner ? inner.location : point_at_end(begin_node.begin_keyword_loc)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Coverage's safe-navigation branch spans the receiver through the
|
|
194
|
+
# end of the call's arguments (or just the message when there are
|
|
195
|
+
# none), but never includes a trailing block: `x&.foo { ... }` and
|
|
196
|
+
# `x&.foo(1) { ... }` both end exactly where `x&.foo` / `x&.foo(1)`
|
|
197
|
+
# would without the block. `node.location` includes an attached
|
|
198
|
+
# block, so build the end position from `closing_loc` (closing
|
|
199
|
+
# paren) / `arguments` (paren-less args) / `message_loc` instead.
|
|
200
|
+
# This convention is the same on legacy and modern Rubies. See
|
|
201
|
+
# issue #1233.
|
|
202
|
+
def safe_navigation_location(node)
|
|
203
|
+
end_loc = node.closing_loc || node.arguments&.location || node.message_loc
|
|
204
|
+
PointLocation.new(
|
|
205
|
+
start_line: node.location.start_line, start_column: node.location.start_column,
|
|
206
|
+
end_line: end_loc.end_line, end_column: end_loc.end_column
|
|
207
|
+
)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def point_at_end(location)
|
|
211
|
+
PointLocation.new(
|
|
212
|
+
start_line: location.end_line, start_column: location.end_column,
|
|
213
|
+
end_line: location.end_line, end_column: location.end_column
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The `else`/`elsif` clause of an if-like node, under whichever
|
|
218
|
+
# accessor this Prism version exposes (see the two *_METHOD
|
|
219
|
+
# constants).
|
|
220
|
+
def if_like_subsequent(node)
|
|
221
|
+
node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : node.public_send(ELSE_CLAUSE_METHOD)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Whether an empty then arm collapses to a point at the predicate's
|
|
225
|
+
# end. Modern Coverage does this for every `if` (but not `unless`);
|
|
226
|
+
# legacy Coverage does it only in void position, for both.
|
|
227
|
+
def empty_arm_collapses?(node, type)
|
|
228
|
+
return type == :if unless LEGACY_COVERAGE_LOCATIONS
|
|
229
|
+
|
|
230
|
+
!value_position?(node)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Whether `node` sits in value (method-return) position, which on
|
|
234
|
+
# legacy Rubies keeps an empty arm's range instead of collapsing it
|
|
235
|
+
# to a point. `@value_positions` is computed once per parse by
|
|
236
|
+
# ValuePositions (only on legacy; nil elsewhere, which reads as
|
|
237
|
+
# "value" — the safe, pre-audit default).
|
|
238
|
+
def value_position?(node)
|
|
239
|
+
return true if @value_positions.nil?
|
|
240
|
+
|
|
241
|
+
@value_positions.key?(node)
|
|
242
|
+
end
|
|
243
|
+
# simplecov:enable
|
|
244
|
+
end
|
|
245
|
+
# rubocop:enable Metrics/ModuleLength
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleCov
|
|
4
|
+
module StaticCoverageExtractor
|
|
5
|
+
# Ruby 3.3 value-position analysis for the extractor's legacy branch
|
|
6
|
+
# conventions (see LocationConventions and the #1233 audit).
|
|
7
|
+
#
|
|
8
|
+
# On Ruby 3.3, the source range Coverage assigns to an EMPTY branch arm
|
|
9
|
+
# depends on whether its construct is in value position — its result is
|
|
10
|
+
# the method's return value — or void position, where the result is
|
|
11
|
+
# discarded. Value position keeps the whole-construct range; void
|
|
12
|
+
# collapses the arm to a point at its header's end. Ruby 3.4 dropped the
|
|
13
|
+
# distinction, so this pass only runs on legacy Rubies.
|
|
14
|
+
#
|
|
15
|
+
# "Value position" here is narrower than general value-use: it is
|
|
16
|
+
# strictly method-return (tail) position. It reaches a node only through
|
|
17
|
+
# statement tails and `if`/`unless`/`when` arms. Assignments, blocks,
|
|
18
|
+
# lambdas, method arguments, `case/in` arms, and loop bodies all discard
|
|
19
|
+
# it (Coverage treats their empty arms as void). So `tail_children`
|
|
20
|
+
# names the constructs that forward tail position and everything else
|
|
21
|
+
# falls through to the void default.
|
|
22
|
+
module ValuePositions
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# simplecov:disable
|
|
26
|
+
# This whole pass runs only on legacy Rubies (the modern dogfood
|
|
27
|
+
# never calls it), so its lines can't be covered on the CI Ruby that
|
|
28
|
+
# enforces 100%. Behavior is pinned instead by the differential
|
|
29
|
+
# tuple-equivalence spec, which runs against real Coverage on 3.3.
|
|
30
|
+
|
|
31
|
+
# An identity set (a `compare_by_identity` Hash used as a set) of the
|
|
32
|
+
# Prism nodes Coverage treats as being in value position.
|
|
33
|
+
def call(root)
|
|
34
|
+
positions = {} #: Hash[untyped, bool]
|
|
35
|
+
positions.compare_by_identity
|
|
36
|
+
mark(root, true, positions)
|
|
37
|
+
positions
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mark(node, in_value, positions)
|
|
41
|
+
return unless node.is_a?(::Prism::Node)
|
|
42
|
+
|
|
43
|
+
positions[node] = true if in_value
|
|
44
|
+
children = tail_children(node, in_value)
|
|
45
|
+
node.compact_child_nodes.each do |child|
|
|
46
|
+
mark(child, children.any? { |c| c.equal?(child) }, positions)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The children of `node` that inherit its tail position; empty for the
|
|
51
|
+
# void default. A method body is a tail context even when the `def`
|
|
52
|
+
# itself is not (the method still returns its last expression), so it
|
|
53
|
+
# is included regardless of `in_value`.
|
|
54
|
+
def tail_children(node, in_value)
|
|
55
|
+
# A method body is a tail context even when the `def` is not.
|
|
56
|
+
return [node.body] if node.is_a?(::Prism::DefNode)
|
|
57
|
+
return [] unless in_value
|
|
58
|
+
|
|
59
|
+
case node
|
|
60
|
+
when ::Prism::StatementsNode then [node.body.last]
|
|
61
|
+
when ::Prism::IfNode, ::Prism::UnlessNode then [node.statements, subsequent(node)]
|
|
62
|
+
when ::Prism::CaseNode then [*node.conditions, else_clause(node)]
|
|
63
|
+
when ::Prism::ElseNode, ::Prism::WhenNode, ::Prism::BeginNode, ::Prism::ProgramNode then [node.statements]
|
|
64
|
+
else []
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The `else`/`elsif` clause of an if-like node, and the `else` clause
|
|
69
|
+
# of a case, under whichever accessor this Prism version exposes.
|
|
70
|
+
# `case/in` (CaseMatchNode) is intentionally not a tail construct: its
|
|
71
|
+
# `in` arms and `else` both discard tail position.
|
|
72
|
+
def subsequent(node)
|
|
73
|
+
node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(node)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def else_clause(node)
|
|
77
|
+
node.public_send(ELSE_CLAUSE_METHOD)
|
|
78
|
+
end
|
|
79
|
+
# simplecov:enable
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "condition_folding"
|
|
4
|
+
require_relative "location_conventions"
|
|
3
5
|
require_relative "method_collector"
|
|
6
|
+
require_relative "value_position"
|
|
4
7
|
|
|
5
8
|
module SimpleCov
|
|
6
9
|
module StaticCoverageExtractor
|
|
@@ -18,6 +21,22 @@ module SimpleCov
|
|
|
18
21
|
else
|
|
19
22
|
:consequent
|
|
20
23
|
end
|
|
24
|
+
|
|
25
|
+
# The same Prism 1.3 rename hit the `else` accessor on `UnlessNode`,
|
|
26
|
+
# `CaseNode`, and `CaseMatchNode` (all three: `consequent` ->
|
|
27
|
+
# `else_clause`). Ruby 3.3's stdlib Prism (0.19) only exposes
|
|
28
|
+
# `consequent`, so reaching for `else_clause` there raised
|
|
29
|
+
# NoMethodError inside the extractor — `call` swallowed it and the
|
|
30
|
+
# whole file silently fell back to no simulated data for any
|
|
31
|
+
# `unless`/`else` or empty-arm `case`. Resolve the name once, like
|
|
32
|
+
# IF_NODE_SUBSEQUENT_METHOD. All three nodes renamed together, so one
|
|
33
|
+
# constant (probed off CaseNode) covers them.
|
|
34
|
+
ELSE_CLAUSE_METHOD =
|
|
35
|
+
if ::Prism::CaseNode.method_defined?(:else_clause)
|
|
36
|
+
:else_clause
|
|
37
|
+
else
|
|
38
|
+
:consequent
|
|
39
|
+
end
|
|
21
40
|
# simplecov:enable
|
|
22
41
|
|
|
23
42
|
# Prism visitor that accumulates branch and method tuples in the
|
|
@@ -29,6 +48,12 @@ module SimpleCov
|
|
|
29
48
|
# Method tuples and the class/module nesting that names them are
|
|
30
49
|
# collected by this mixin; this class focuses on branch extraction.
|
|
31
50
|
include MethodCollector
|
|
51
|
+
# Source-range resolution, including the per-Ruby-version Coverage
|
|
52
|
+
# conventions. See issue #1226.
|
|
53
|
+
include LocationConventions
|
|
54
|
+
# Which literal `if`/`unless`/ternary conditions the compiler folds
|
|
55
|
+
# away (so we emit no branch for them).
|
|
56
|
+
include ConditionFolding
|
|
32
57
|
|
|
33
58
|
attr_reader :branches, :methods
|
|
34
59
|
|
|
@@ -38,6 +63,19 @@ module SimpleCov
|
|
|
38
63
|
@methods = {}
|
|
39
64
|
@next_id = 0
|
|
40
65
|
@class_stack = []
|
|
66
|
+
@value_positions = nil
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Entry point for a parsed file. On legacy Rubies the location of an
|
|
70
|
+
# empty branch arm depends on whether its construct is in value
|
|
71
|
+
# (tail) position, so precompute that once for the whole tree before
|
|
72
|
+
# emitting anything. Modern Rubies don't need it (see
|
|
73
|
+
# LocationConventions), so the pass is skipped there.
|
|
74
|
+
def visit_program_node(node)
|
|
75
|
+
# simplecov:disable branch — legacy-only arm; unreachable on the modern dogfood Ruby
|
|
76
|
+
@value_positions = ValuePositions.call(node) if LEGACY_COVERAGE_LOCATIONS
|
|
77
|
+
# simplecov:enable branch
|
|
78
|
+
super
|
|
41
79
|
end
|
|
42
80
|
|
|
43
81
|
# `if` / `unless` / postfix-if / postfix-unless / ternary all parse
|
|
@@ -47,12 +85,12 @@ module SimpleCov
|
|
|
47
85
|
# missing, Coverage synthesizes a `:else` arm attributed to the
|
|
48
86
|
# whole condition's range — we do the same.
|
|
49
87
|
def visit_if_node(node)
|
|
50
|
-
emit_if_like(node, :if)
|
|
88
|
+
emit_if_like(node, :if) unless static_condition?(node.predicate)
|
|
51
89
|
super
|
|
52
90
|
end
|
|
53
91
|
|
|
54
92
|
def visit_unless_node(node)
|
|
55
|
-
emit_if_like(node, :unless)
|
|
93
|
+
emit_if_like(node, :unless) unless static_condition?(node.predicate)
|
|
56
94
|
super
|
|
57
95
|
end
|
|
58
96
|
|
|
@@ -74,6 +112,24 @@ module SimpleCov
|
|
|
74
112
|
super
|
|
75
113
|
end
|
|
76
114
|
|
|
115
|
+
# One-line pattern matching: `x => pattern` (MatchRequiredNode) and
|
|
116
|
+
# `x in pattern` (MatchPredicateNode). Ruby 3.3's Coverage reports
|
|
117
|
+
# these as a `:case` with an `:in` and an `:else` arm; 3.4 dropped
|
|
118
|
+
# them entirely (no branch), so this is legacy-only. The two forms
|
|
119
|
+
# differ only in where Coverage anchors the synthesized `:else`:
|
|
120
|
+
# `=>` uses the whole expression, `in` uses just the pattern.
|
|
121
|
+
# simplecov:disable branch — legacy-only arms; unreachable on the modern dogfood Ruby
|
|
122
|
+
def visit_match_required_node(node)
|
|
123
|
+
emit_oneline_pattern(node, node.location) if LEGACY_COVERAGE_LOCATIONS
|
|
124
|
+
super
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def visit_match_predicate_node(node)
|
|
128
|
+
emit_oneline_pattern(node, node.pattern.location) if LEGACY_COVERAGE_LOCATIONS
|
|
129
|
+
super
|
|
130
|
+
end
|
|
131
|
+
# simplecov:enable branch
|
|
132
|
+
|
|
77
133
|
# `while` / `until` loops get a single `:body` arm. No synthetic
|
|
78
134
|
# else (the loop either runs the body or doesn't).
|
|
79
135
|
def visit_while_node(node)
|
|
@@ -92,75 +148,44 @@ module SimpleCov
|
|
|
92
148
|
# optional else/elsif) but expose the trailing arm under different
|
|
93
149
|
# accessors. `if_like_else_location` hides that split.
|
|
94
150
|
def emit_if_like(node, type)
|
|
95
|
-
then_loc =
|
|
96
|
-
else_loc = if_like_else_location(node)
|
|
97
|
-
@branches[build_tuple(type, node
|
|
151
|
+
then_loc = if_like_then_location(node, type)
|
|
152
|
+
else_loc = if_like_else_location(node, type)
|
|
153
|
+
@branches[build_tuple(type, if_like_location(node, type))] = {
|
|
98
154
|
build_tuple(:then, then_loc) => 0,
|
|
99
155
|
build_tuple(:else, else_loc) => 0
|
|
100
156
|
}
|
|
101
157
|
end
|
|
102
158
|
|
|
103
159
|
def emit_safe_navigation(node)
|
|
104
|
-
loc = node
|
|
160
|
+
loc = safe_navigation_location(node)
|
|
105
161
|
@branches[build_tuple(:"&.", loc)] = {
|
|
106
162
|
build_tuple(:then, loc) => 0,
|
|
107
163
|
build_tuple(:else, loc) => 0
|
|
108
164
|
}
|
|
109
165
|
end
|
|
110
166
|
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
# convention).
|
|
118
|
-
def if_like_else_location(node)
|
|
119
|
-
sub = node.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : node.else_clause
|
|
120
|
-
return node.location unless sub
|
|
121
|
-
|
|
122
|
-
arm_location(else_body_of(sub), sub.location)
|
|
167
|
+
# simplecov:disable — legacy-only (3.4 emits no branch for one-line patterns)
|
|
168
|
+
def emit_oneline_pattern(node, else_location)
|
|
169
|
+
@branches[build_tuple(:case, node.location)] = {
|
|
170
|
+
build_tuple(:in, node.pattern.location) => 0,
|
|
171
|
+
build_tuple(:else, else_location) => 0
|
|
172
|
+
}
|
|
123
173
|
end
|
|
174
|
+
# simplecov:enable
|
|
124
175
|
|
|
125
176
|
def emit_case_like(node, when_type)
|
|
126
177
|
arms = node.conditions.to_h do |when_node|
|
|
127
|
-
|
|
128
|
-
[build_tuple(when_type, loc), 0]
|
|
178
|
+
[build_tuple(when_type, case_arm_location(node, when_node, when_type)), 0]
|
|
129
179
|
end
|
|
130
180
|
arms[build_tuple(:else, else_arm_location(node))] = 0
|
|
131
181
|
@branches[build_tuple(:case, node.location)] = arms
|
|
132
182
|
end
|
|
133
183
|
|
|
134
|
-
# Resolve the source range Coverage attributes to a synthetic-or-real
|
|
135
|
-
# `:else` arm of a case construct: the body of an explicit else,
|
|
136
|
-
# or the case's full range when no else is present.
|
|
137
|
-
def else_arm_location(node)
|
|
138
|
-
return node.location unless node.else_clause
|
|
139
|
-
|
|
140
|
-
arm_location(else_body_of(node.else_clause), node.else_clause.location)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
184
|
def emit_loop(node, type)
|
|
144
185
|
cond_tuple = build_tuple(type, node.location)
|
|
145
|
-
|
|
146
|
-
@branches[cond_tuple] = {build_tuple(:body, body_loc) => 0}
|
|
186
|
+
@branches[cond_tuple] = {build_tuple(:body, loop_body_location(node)) => 0}
|
|
147
187
|
end
|
|
148
188
|
|
|
149
|
-
# Body location for an arm. Prism's `statements` is a StatementsNode
|
|
150
|
-
# whose span covers the contained expressions; fall back to the
|
|
151
|
-
# parent when the arm body is empty (e.g., `if cond then end`).
|
|
152
|
-
def arm_location(statements, fallback_location)
|
|
153
|
-
statements&.location || fallback_location
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
# simplecov:disable branch
|
|
157
|
-
# The `else_node` fallback is defensive: every Prism node passed
|
|
158
|
-
# in here in practice responds to `:statements`.
|
|
159
|
-
def else_body_of(else_node)
|
|
160
|
-
else_node.respond_to?(:statements) ? else_node.statements : else_node
|
|
161
|
-
end
|
|
162
|
-
# simplecov:enable branch
|
|
163
|
-
|
|
164
189
|
def build_tuple(type, location)
|
|
165
190
|
id = @next_id
|
|
166
191
|
@next_id += 1
|
data/lib/simplecov/version.rb
CHANGED
data/lib/simplecov.rb
CHANGED
|
@@ -8,14 +8,18 @@ module SimpleCov
|
|
|
8
8
|
# every coverage criterion has been disabled.
|
|
9
9
|
class ConfigurationError < StandardError; end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
# Maps SimpleCov's criterion names to the keys `Coverage.start` expects.
|
|
12
|
+
# Lives at module scope (not inside `class << self`) so it can be
|
|
13
|
+
# declared in the RBS signatures; lexical scoping keeps every existing
|
|
14
|
+
# reference inside the singleton class working.
|
|
15
|
+
CRITERION_TO_RUBY_COVERAGE = {
|
|
16
|
+
branch: :branches,
|
|
17
|
+
line: :lines,
|
|
18
|
+
method: :methods,
|
|
19
|
+
oneshot_line: :oneshot_lines
|
|
20
|
+
}.freeze
|
|
18
21
|
|
|
22
|
+
class << self
|
|
19
23
|
attr_accessor :pid
|
|
20
24
|
# When this process started tracking coverage. Captured by SimpleCov.start
|
|
21
25
|
# so JSONFormatter can detect when an existing coverage.json was written
|
|
@@ -93,10 +97,13 @@ module SimpleCov
|
|
|
93
97
|
# the warning is the cue to move `SimpleCov.start` into a test helper.
|
|
94
98
|
# See #581.
|
|
95
99
|
def with_dot_simplecov_autoload
|
|
96
|
-
|
|
100
|
+
# Read in the ensure clause, where flow analysis cannot see the
|
|
101
|
+
# assignment above; anchor the type here.
|
|
102
|
+
previous = @autoloading_dot_simplecov # : bool?
|
|
97
103
|
@autoloading_dot_simplecov = true
|
|
98
104
|
yield
|
|
99
105
|
ensure
|
|
106
|
+
# @type var previous: bool?
|
|
100
107
|
@autoloading_dot_simplecov = previous
|
|
101
108
|
end
|
|
102
109
|
|
|
@@ -123,7 +130,11 @@ module SimpleCov
|
|
|
123
130
|
return if @at_exit_hook_installed
|
|
124
131
|
|
|
125
132
|
@at_exit_hook_installed = true
|
|
126
|
-
|
|
133
|
+
# Never defer in a forked child: Minitest pins its after_run at_exit
|
|
134
|
+
# to the pid that armed autorun, so the deferral target can't fire
|
|
135
|
+
# there and the child's resultset would be silently dropped. See
|
|
136
|
+
# issue #1227.
|
|
137
|
+
defer_to_minitest_after_run if minitest_autorun_pending? && !forked_subprocess?
|
|
127
138
|
Kernel.at_exit do
|
|
128
139
|
next if SimpleCov.external_at_exit?
|
|
129
140
|
|
|
@@ -173,7 +184,7 @@ module SimpleCov
|
|
|
173
184
|
|
|
174
185
|
start_arguments[:eval] = true if coverage_for_eval_enabled?
|
|
175
186
|
|
|
176
|
-
Coverage.start(start_arguments) unless Coverage.running?
|
|
187
|
+
Coverage.start(**start_arguments) unless Coverage.running?
|
|
177
188
|
end
|
|
178
189
|
|
|
179
190
|
# `Rake::TestTask` runs `ruby -e 'require "minitest/autorun"; ...'`,
|
|
@@ -203,7 +214,9 @@ module SimpleCov
|
|
|
203
214
|
return unless defined?(JRUBY_VERSION) && defined?(JRuby) # simplecov:disable — JRuby-only branch
|
|
204
215
|
|
|
205
216
|
# simplecov:disable — JRuby-only branches; unreachable from CRuby
|
|
206
|
-
|
|
217
|
+
# `org` is JRuby's Java-package entry point; it does not exist on
|
|
218
|
+
# CRuby, so no RBS declaration can be truthful here.
|
|
219
|
+
return if org.jruby.RubyInstanceConfig.FULL_TRACE_ENABLED # steep:ignore NoMethod
|
|
207
220
|
|
|
208
221
|
warn 'Coverage may be inaccurate; set the "--debug" command line option, ' \
|
|
209
222
|
'or do JRUBY_OPTS="--debug" ' \
|