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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/lib/simplecov/cli/coverage.rb +5 -3
  3. data/lib/simplecov/cli/diff.rb +2 -1
  4. data/lib/simplecov/cli/dotfile.rb +9 -4
  5. data/lib/simplecov/cli/merge.rb +5 -3
  6. data/lib/simplecov/cli/report.rb +11 -6
  7. data/lib/simplecov/cli/run.rb +1 -1
  8. data/lib/simplecov/cli/serve.rb +17 -7
  9. data/lib/simplecov/cli/uncovered.rb +2 -1
  10. data/lib/simplecov/color.rb +6 -1
  11. data/lib/simplecov/combine/branches_combiner.rb +2 -1
  12. data/lib/simplecov/combine/files_combiner.rb +42 -2
  13. data/lib/simplecov/combine/methods_combiner.rb +27 -4
  14. data/lib/simplecov/combine/results_combiner.rb +2 -1
  15. data/lib/simplecov/configuration/coverage.rb +6 -2
  16. data/lib/simplecov/configuration/coverage_criteria.rb +3 -1
  17. data/lib/simplecov/configuration/formatting.rb +17 -7
  18. data/lib/simplecov/configuration/thresholds.rb +13 -9
  19. data/lib/simplecov/configuration.rb +22 -13
  20. data/lib/simplecov/coverage_statistics.rb +4 -1
  21. data/lib/simplecov/coverage_violations.rb +2 -1
  22. data/lib/simplecov/defaults.rb +3 -3
  23. data/lib/simplecov/directive.rb +8 -3
  24. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +1 -1
  25. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +1 -1
  26. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +1 -1
  27. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +1 -1
  28. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +3 -3
  29. data/lib/simplecov/exit_codes.rb +12 -0
  30. data/lib/simplecov/exit_handling.rb +3 -3
  31. data/lib/simplecov/file_list.rb +12 -5
  32. data/lib/simplecov/formatter/base.rb +2 -1
  33. data/lib/simplecov/formatter/html_formatter.rb +13 -7
  34. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +9 -3
  35. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +1 -1
  36. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +1 -1
  37. data/lib/simplecov/formatter/json_formatter.rb +4 -2
  38. data/lib/simplecov/formatter/multi_formatter.rb +7 -2
  39. data/lib/simplecov/formatter/simple_formatter.rb +3 -1
  40. data/lib/simplecov/process.rb +22 -0
  41. data/lib/simplecov/result_adapter.rb +45 -10
  42. data/lib/simplecov/result_processing.rb +4 -4
  43. data/lib/simplecov/simulate_coverage.rb +2 -2
  44. data/lib/simplecov/source_file/line.rb +2 -2
  45. data/lib/simplecov/source_file/method.rb +1 -1
  46. data/lib/simplecov/source_file/method_builder.rb +3 -1
  47. data/lib/simplecov/source_file/source_loader.rb +2 -2
  48. data/lib/simplecov/source_file.rb +2 -2
  49. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
  50. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
  51. data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
  52. data/lib/simplecov/static_coverage_extractor/visitor.rb +71 -46
  53. data/lib/simplecov/version.rb +1 -1
  54. data/lib/simplecov.rb +24 -11
  55. data/sig/simplecov.rbs +1638 -0
  56. 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 = arm_location(node.statements, node.location)
96
- else_loc = if_like_else_location(node)
97
- @branches[build_tuple(type, node.location)] = {
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.location
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
- # Resolve the source range Coverage attributes to a real-or-synthetic
112
- # `:else` arm of an if-like construct. IfNode uses
113
- # `subsequent` / `consequent` depending on Prism version (resolved
114
- # to `IF_NODE_SUBSEQUENT_METHOD` at load time); UnlessNode uses
115
- # `else_clause`. When neither is present, the synthesized else
116
- # inherits the whole condition's range (matches Coverage's
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
- loc = arm_location(when_node.statements, when_node.location)
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
- body_loc = arm_location(node.statements, node.location)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.2"
5
5
  end
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
- class << self
12
- CRITERION_TO_RUBY_COVERAGE = {
13
- branch: :branches,
14
- line: :lines,
15
- method: :methods,
16
- oneshot_line: :oneshot_lines
17
- }.freeze
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
- previous = @autoloading_dot_simplecov
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
- defer_to_minitest_after_run if minitest_autorun_pending?
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
- return if org.jruby.RubyInstanceConfig.FULL_TRACE_ENABLED
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" ' \