simplecov 1.1.1 → 1.3.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.
Files changed (200) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +148 -5
  3. data/exe/simplecov +0 -3
  4. data/lib/minitest/simplecov_plugin.rb +12 -9
  5. data/lib/simplecov/atomic_file.rb +16 -19
  6. data/lib/simplecov/autostart.rb +5 -7
  7. data/lib/simplecov/baseline/parser.rb +58 -0
  8. data/lib/simplecov/baseline.rb +170 -0
  9. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  10. data/lib/simplecov/cli/affected/selection.rb +117 -0
  11. data/lib/simplecov/cli/affected.rb +148 -0
  12. data/lib/simplecov/cli/annotations.rb +166 -0
  13. data/lib/simplecov/cli/badge/svg.rb +70 -0
  14. data/lib/simplecov/cli/badge.rb +62 -0
  15. data/lib/simplecov/cli/clean.rb +23 -24
  16. data/lib/simplecov/cli/command_helpers.rb +55 -19
  17. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  18. data/lib/simplecov/cli/completions.rb +83 -0
  19. data/lib/simplecov/cli/coverage.rb +25 -28
  20. data/lib/simplecov/cli/coverage_file.rb +58 -14
  21. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  22. data/lib/simplecov/cli/dead_code.rb +139 -0
  23. data/lib/simplecov/cli/diff/output.rb +45 -0
  24. data/lib/simplecov/cli/diff.rb +35 -67
  25. data/lib/simplecov/cli/dotfile.rb +83 -38
  26. data/lib/simplecov/cli/git.rb +41 -0
  27. data/lib/simplecov/cli/history/output.rb +135 -0
  28. data/lib/simplecov/cli/history.rb +67 -0
  29. data/lib/simplecov/cli/merge.rb +34 -39
  30. data/lib/simplecov/cli/open.rb +12 -15
  31. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  32. data/lib/simplecov/cli/patch/output.rb +151 -0
  33. data/lib/simplecov/cli/patch.rb +175 -0
  34. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  35. data/lib/simplecov/cli/ratchet.rb +83 -0
  36. data/lib/simplecov/cli/report.rb +18 -24
  37. data/lib/simplecov/cli/run.rb +8 -9
  38. data/lib/simplecov/cli/serve/report_preparer.rb +7 -6
  39. data/lib/simplecov/cli/serve/static_file_handler.rb +62 -56
  40. data/lib/simplecov/cli/serve.rb +27 -24
  41. data/lib/simplecov/cli/show/annotator.rb +87 -0
  42. data/lib/simplecov/cli/show/sweep.rb +36 -0
  43. data/lib/simplecov/cli/show.rb +133 -0
  44. data/lib/simplecov/cli/status/facts.rb +87 -0
  45. data/lib/simplecov/cli/status.rb +93 -0
  46. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  47. data/lib/simplecov/cli/tests.rb +148 -0
  48. data/lib/simplecov/cli/uncovered/misses.rb +33 -0
  49. data/lib/simplecov/cli/uncovered.rb +58 -34
  50. data/lib/simplecov/cli/usage.rb +153 -0
  51. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  52. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  53. data/lib/simplecov/cli/watch/poller.rb +45 -0
  54. data/lib/simplecov/cli/watch/session.rb +134 -0
  55. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  56. data/lib/simplecov/cli/watch.rb +85 -0
  57. data/lib/simplecov/cli.rb +43 -92
  58. data/lib/simplecov/color.rb +19 -37
  59. data/lib/simplecov/combine/branches_combiner.rb +7 -31
  60. data/lib/simplecov/combine/coverage_accumulator.rb +49 -142
  61. data/lib/simplecov/combine/identity_interner.rb +10 -13
  62. data/lib/simplecov/combine/interned_counts.rb +7 -9
  63. data/lib/simplecov/combine/lines_combiner.rb +20 -44
  64. data/lib/simplecov/combine/methods_combiner.rb +9 -35
  65. data/lib/simplecov/combine/results_combiner.rb +4 -21
  66. data/lib/simplecov/combine.rb +0 -6
  67. data/lib/simplecov/command_guesser.rb +20 -34
  68. data/lib/simplecov/configuration/baseline.rb +31 -0
  69. data/lib/simplecov/configuration/coverage.rb +91 -79
  70. data/lib/simplecov/configuration/coverage_criteria.rb +43 -54
  71. data/lib/simplecov/configuration/deprecations.rb +21 -0
  72. data/lib/simplecov/configuration/eval_coverage.rb +7 -11
  73. data/lib/simplecov/configuration/filters.rb +40 -73
  74. data/lib/simplecov/configuration/formatting.rb +74 -83
  75. data/lib/simplecov/configuration/groups.rb +19 -13
  76. data/lib/simplecov/configuration/history.rb +44 -0
  77. data/lib/simplecov/configuration/ignored_entries.rb +31 -22
  78. data/lib/simplecov/configuration/merging.rb +94 -62
  79. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  80. data/lib/simplecov/configuration/production.rb +23 -0
  81. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  82. data/lib/simplecov/configuration/thresholds.rb +33 -62
  83. data/lib/simplecov/configuration/view_coverage.rb +37 -0
  84. data/lib/simplecov/configuration.rb +70 -76
  85. data/lib/simplecov/configuration_error.rb +5 -0
  86. data/lib/simplecov/context_map/union.rb +81 -0
  87. data/lib/simplecov/context_map.rb +187 -0
  88. data/lib/simplecov/coverage_json.rb +2 -5
  89. data/lib/simplecov/coverage_statistics.rb +14 -24
  90. data/lib/simplecov/coverage_violations.rb +160 -65
  91. data/lib/simplecov/current_run.rb +61 -0
  92. data/lib/simplecov/defaults.rb +4 -23
  93. data/lib/simplecov/deprecation.rb +45 -27
  94. data/lib/simplecov/directive/erb.rb +44 -0
  95. data/lib/simplecov/directive/haml.rb +33 -0
  96. data/lib/simplecov/directive/indented_template.rb +37 -0
  97. data/lib/simplecov/directive/slim.rb +35 -0
  98. data/lib/simplecov/directive/template.rb +29 -0
  99. data/lib/simplecov/directive.rb +47 -62
  100. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  101. data/lib/simplecov/exit_codes/check.rb +17 -11
  102. data/lib/simplecov/exit_codes/exit_code_handling.rb +21 -8
  103. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +7 -10
  104. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  105. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  106. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +7 -12
  107. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +11 -10
  108. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +7 -9
  109. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +16 -17
  110. data/lib/simplecov/exit_codes.rb +16 -10
  111. data/lib/simplecov/exit_handling.rb +94 -59
  112. data/lib/simplecov/file_list.rb +20 -51
  113. data/lib/simplecov/filter.rb +43 -66
  114. data/lib/simplecov/formatter/base.rb +37 -55
  115. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  116. data/lib/simplecov/formatter/coverage_json_writer.rb +29 -31
  117. data/lib/simplecov/formatter/html_formatter/public/index.html +15 -9
  118. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +74 -17
  119. data/lib/simplecov/formatter/html_formatter.rb +24 -41
  120. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +50 -16
  121. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  122. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +43 -29
  123. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +14 -10
  124. data/lib/simplecov/formatter/json_formatter.rb +7 -13
  125. data/lib/simplecov/formatter/multi_formatter.rb +5 -11
  126. data/lib/simplecov/formatter/simple_formatter.rb +2 -8
  127. data/lib/simplecov/formatter.rb +14 -10
  128. data/lib/simplecov/group_names.rb +13 -12
  129. data/lib/simplecov/history.rb +115 -0
  130. data/lib/simplecov/last_run.rb +6 -9
  131. data/lib/simplecov/lines_classifier.rb +31 -16
  132. data/lib/simplecov/load_global_config.rb +7 -7
  133. data/lib/simplecov/parallel_adapters/base.rb +13 -41
  134. data/lib/simplecov/parallel_adapters/generic.rb +7 -19
  135. data/lib/simplecov/parallel_adapters/parallel_tests.rb +21 -42
  136. data/lib/simplecov/parallel_adapters.rb +24 -55
  137. data/lib/simplecov/parallel_coordination.rb +39 -49
  138. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  139. data/lib/simplecov/parallel_result_merger.rb +67 -126
  140. data/lib/simplecov/process.rb +14 -32
  141. data/lib/simplecov/production/error.rb +11 -0
  142. data/lib/simplecov/production/file_sink.rb +130 -0
  143. data/lib/simplecov/production.rb +249 -0
  144. data/lib/simplecov/profiles/rails.rb +4 -15
  145. data/lib/simplecov/profiles/root_filter.rb +0 -4
  146. data/lib/simplecov/profiles/strict.rb +11 -21
  147. data/lib/simplecov/profiles.rb +11 -33
  148. data/lib/simplecov/report_deferral.rb +11 -15
  149. data/lib/simplecov/report_stamp.rb +6 -9
  150. data/lib/simplecov/result/filter_config.rb +19 -0
  151. data/lib/simplecov/result/missing_source_files_reporter.rb +8 -9
  152. data/lib/simplecov/result/serialization.rb +43 -0
  153. data/lib/simplecov/result/source_file_builder.rb +7 -12
  154. data/lib/simplecov/result.rb +74 -117
  155. data/lib/simplecov/result_adapter.rb +56 -68
  156. data/lib/simplecov/result_merger/contexts.rb +22 -0
  157. data/lib/simplecov/result_merger/legacy_format_adapter.rb +4 -8
  158. data/lib/simplecov/result_merger/resultset_file.rb +20 -27
  159. data/lib/simplecov/result_merger/resultset_run_identity.rb +23 -25
  160. data/lib/simplecov/result_merger/resultset_store.rb +23 -14
  161. data/lib/simplecov/result_merger/unloaded_files.rb +24 -32
  162. data/lib/simplecov/result_merger.rb +59 -58
  163. data/lib/simplecov/result_processing.rb +92 -122
  164. data/lib/simplecov/run_identity.rb +16 -18
  165. data/lib/simplecov/simulate_coverage.rb +34 -50
  166. data/lib/simplecov/source_file/branch.rb +8 -33
  167. data/lib/simplecov/source_file/branch_builder.rb +28 -47
  168. data/lib/simplecov/source_file/builder_context.rb +13 -13
  169. data/lib/simplecov/source_file/line.rb +13 -33
  170. data/lib/simplecov/source_file/line_builder.rb +9 -15
  171. data/lib/simplecov/source_file/method.rb +3 -7
  172. data/lib/simplecov/source_file/method_builder.rb +6 -15
  173. data/lib/simplecov/source_file/ruby_data_parser.rb +59 -66
  174. data/lib/simplecov/source_file/skip_chunks.rb +29 -29
  175. data/lib/simplecov/source_file/source_loader.rb +45 -35
  176. data/lib/simplecov/source_file/statistics.rb +20 -9
  177. data/lib/simplecov/source_file.rb +21 -51
  178. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +55 -139
  179. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +76 -121
  180. data/lib/simplecov/static_coverage_extractor/method_collector.rb +9 -26
  181. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +15 -24
  182. data/lib/simplecov/static_coverage_extractor/value_position.rb +25 -38
  183. data/lib/simplecov/static_coverage_extractor/visitor.rb +45 -65
  184. data/lib/simplecov/static_coverage_extractor.rb +43 -75
  185. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  186. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  187. data/lib/simplecov/test_tracker/delta.rb +69 -0
  188. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  189. data/lib/simplecov/test_tracker.rb +162 -0
  190. data/lib/simplecov/unloaded_file_injector.rb +18 -31
  191. data/lib/simplecov/useless_results_remover.rb +7 -6
  192. data/lib/simplecov/version.rb +1 -1
  193. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  194. data/lib/simplecov/view_coverage.rb +55 -0
  195. data/lib/simplecov-html.rb +1 -0
  196. data/lib/simplecov.rb +57 -113
  197. data/lib/simplecov_json_formatter.rb +1 -0
  198. data/man/simplecov.1 +354 -0
  199. data/sig/simplecov.rbs +528 -595
  200. metadata +79 -6
@@ -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
- # class / module nesting that names them, in the shape Ruby's
7
- # `Coverage` reports methods. Mixed into `Visitor`, it shares that
8
- # visitor's `@methods` / `@class_stack` state and keeps the
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,24 @@ 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
- # The class context is the surrounding lexical class/module (or
24
- # `Object` at the top level, matching `Coverage`'s convention).
25
- # Suppression covers 3.2's folded dead arms, where nested branches
26
- # stay instrumented but a `def` never registers (see
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.
28
21
  def visit_def_node(node)
29
- # simplecov:disable branch — suppression is reachable on 3.2 only
30
- return super if @suppress_methods
31
-
32
- # simplecov:enable branch
33
- loc = node.location
34
22
  class_name = @class_stack.last || "Object"
35
- key = [class_name, node.name, loc.start_line, loc.start_column, loc.end_line, loc.end_column]
23
+ key = [class_name, node.name, node.start_line, node.start_column, node.end_line, node.end_column]
36
24
  @methods[key] = 0
37
25
  super
38
26
  end
39
27
 
40
- private
28
+ private
41
29
 
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
30
  def constant_name(node)
47
31
  return "<anonymous>" if node.nil?
48
32
  return node.slice if node.respond_to?(:slice)
49
33
 
50
34
  node.to_s
51
35
  end
52
- # simplecov:enable
53
36
 
54
37
  def with_class(name)
55
38
  @class_stack.push(name)
@@ -2,51 +2,42 @@
2
2
 
3
3
  module SimpleCov
4
4
  module StaticCoverageExtractor
5
- # The Prism 1.3 (Dec 2024) accessor renames for the trailing clause of
6
- # conditional nodes, resolved ONCE at load so the per-node hot paths
7
- # stay branch-free. Ruby 3.3's stdlib Prism (0.19) predates the
8
- # renames; 3.4+ and any installed prism gem post-dates them. Reaching
9
- # for the modern name on 0.19 raised NoMethodError inside the
10
- # extractor `call` swallowed it and the whole file silently fell
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
- # them after those files were loaded worked only while the references
16
- # happened at call time a load-order trap for the next editor.
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
- module_function
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 ::Prism::IfNode.method_defined?(:subsequent)
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 ::Prism::CaseNode.method_defined?(:else_clause)
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.is_a?(::Prism::IfNode) ? node.public_send(IF_NODE_SUBSEQUENT_METHOD) : else_clause(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 (see LocationConventions and the #1233 audit).
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
- # On Ruby 3.3, the source range Coverage assigns to an EMPTY branch arm
11
- # depends on whether its construct is in value position — its result is
12
- # the method's return value or void position, where the result is
13
- # discarded. Value position keeps the whole-construct range; void
14
- # collapses the arm to a point at its header's end. Ruby 3.4 dropped the
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
- module_function
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 (a `compare_by_identity` Hash used as a set) of the
34
- # Prism nodes Coverage treats as being in value position.
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?(::Prism::Node)
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
- # The children of `node` that inherit its tail position; empty for the
53
- # void default. A method body is a tail context even when the `def`
54
- # itself is not (the method still returns its last expression), so it
55
- # is included regardless of `in_value`.
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
- # A method body is a tail context even when the `def` is not.
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 ::Prism::StatementsNode then [node.body.last]
65
- when ::Prism::IfNode, ::Prism::UnlessNode then [node.statements, PrismCompat.subsequent(node)]
66
- when ::Prism::CaseNode then [*node.conditions, PrismCompat.else_clause(node)]
67
- when ::Prism::ElseNode, ::Prism::WhenNode, ::Prism::BeginNode, ::Prism::ProgramNode then [node.statements]
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
@@ -8,27 +8,22 @@ require_relative "value_position"
8
8
 
9
9
  module SimpleCov
10
10
  module StaticCoverageExtractor
11
- # Prism visitor that accumulates branch and method tuples in the
12
- # shape Ruby's `Coverage` reports. Tuple ids are sequential across
13
- # the file like `Coverage`'s, but the numbering order can differ
14
- # (e.g. `case`/`when` and chained `&.` are visited in a different
15
- # order than Coverage numbers them). That's fine: the combiners
16
- # intern on source span and the report output drops ids, so nothing
17
- # downstream compares them. Only defined when Prism is loadable;
18
- # `StaticCoverageExtractor.available?` is the runtime gate.
19
- class Visitor < ::Prism::Visitor
20
- # Method tuples and the class/module nesting that names them are
21
- # collected by this mixin; this class focuses on branch extraction.
11
+ # Prism visitor that accumulates branch and method tuples in the shape
12
+ # Ruby's `Coverage` reports. Tuple ids are sequential across the file like
13
+ # `Coverage`'s, but the numbering order can differ. That's fine: the
14
+ # combiners intern on source span and the report output drops ids, so
15
+ # nothing downstream compares them.
16
+ class Visitor < Prism::Visitor
22
17
  include MethodCollector
23
- # Source-range resolution, including the per-Ruby-version Coverage
24
- # conventions. See issue #1226.
25
18
  include LocationConventions
26
- # Which literal `if`/`unless`/ternary conditions the compiler folds
27
- # away (so we emit no branch for them).
28
19
  include ConditionFolding
29
20
 
30
21
  attr_reader :branches, :methods
31
22
 
23
+ # Prism's Visitor is a stateless dispatch table whose initializer is
24
+ # Object's, so no mutation of the `super` call can be told apart. It
25
+ # stays for the day that stops being true.
26
+ # mutant:disable
32
27
  def initialize
33
28
  super
34
29
  @branches = {}
@@ -36,35 +31,25 @@ module SimpleCov
36
31
  @next_id = 0
37
32
  @class_stack = []
38
33
  @value_positions = nil
39
- @suppress_methods = false
40
34
  end
41
35
 
42
- # Entry point for a parsed file. On legacy Rubies the location of an
43
- # empty branch arm depends on whether its construct is in value
44
- # (tail) position, so precompute that once for the whole tree before
45
- # emitting anything. Modern Rubies don't need it (see
46
- # LocationConventions), so the pass is skipped there.
36
+ # On legacy Rubies the location of an empty branch arm depends on whether
37
+ # its construct is in value (tail) position, so precompute that once for
38
+ # the whole tree before emitting anything.
47
39
  def visit_program_node(node)
48
- # simplecov:disable branch — legacy-only arm; unreachable on the modern dogfood Ruby
49
40
  @value_positions = ValuePositions.call(node) if LEGACY_COVERAGE_LOCATIONS
50
- # simplecov:enable branch
51
41
  super
52
42
  end
53
43
 
54
- # `if` / `unless` / postfix-if / postfix-unless / ternary all parse
55
- # as IfNode (or UnlessNode). Both carry a `then` arm (the
56
- # statements body) and an optional `subsequent` (an ElseNode for
57
- # `else`, another IfNode for `elsif`). When the subsequent is
58
- # missing, Coverage synthesizes a `:else` arm attributed to the
59
- # whole condition's range — we do the same.
44
+ # `if` / `unless` / postfix / ternary all parse as IfNode (or UnlessNode).
45
+ # Both carry a `then` arm and an optional `subsequent` (an ElseNode for
46
+ # `else`, another IfNode for `elsif`). When the subsequent is missing,
47
+ # Coverage synthesizes a `:else` arm attributed to the whole condition's
48
+ # range, and so do we.
60
49
  #
61
- # A folded condition emits no tuple, and on modern Rubies only its
62
- # live arm is descended into: the compiler eliminates the dead
63
- # arm's entire subtree, so a branch or method nested there would be
64
- # a phantom no loaded run can produce. A falsy `if`'s elsif chain
65
- # survives as a plain `if`, which is what visiting the subsequent
66
- # IfNode emits. On 3.2 the dead arm is visited too, branches only
67
- # (see visit_folded_arms).
50
+ # A folded condition emits no tuple, and only its live arm is descended
51
+ # into: the compiler eliminates the dead arm's entire subtree, so a branch
52
+ # or method nested there would be a phantom no loaded run can produce.
68
53
  def visit_if_node(node)
69
54
  verdict = folded_condition(node.predicate)
70
55
  return visit_folded_arms(verdict, node.statements, PrismCompat.subsequent(node)) if verdict
@@ -86,9 +71,8 @@ module SimpleCov
86
71
  super
87
72
  end
88
73
 
89
- # `case`/`when` and `case`/`in` (pattern matching) parse as CaseNode
90
- # and CaseMatchNode respectively. When there's no explicit `else`,
91
- # Coverage synthesizes one at the case's range.
74
+ # When there's no explicit `else`, Coverage synthesizes one at the case's
75
+ # range.
92
76
  def visit_case_node(node)
93
77
  emit_case_like(node, :when)
94
78
  super
@@ -99,26 +83,22 @@ module SimpleCov
99
83
  super
100
84
  end
101
85
 
102
- # One-line pattern matching: `x => pattern` (MatchRequiredNode) and
103
- # `x in pattern` (MatchPredicateNode). Ruby 3.3's Coverage reports
104
- # these as a `:case` with an `:in` and an `:else` arm; 3.4 dropped
105
- # them entirely (no branch), so this is legacy-only. The two forms
106
- # differ only in where Coverage anchors the synthesized `:else`:
107
- # `=>` uses the whole expression, `in` uses just the pattern.
108
- # simplecov:disable branch — legacy-only arms; unreachable on the modern dogfood Ruby
86
+ # One-line pattern matching: `x => pattern` and `x in pattern`. Ruby 3.3's
87
+ # Coverage reports these as a `:case` with an `:in` and an `:else` arm;
88
+ # 3.4 dropped them entirely, so this is legacy-only. The two forms differ
89
+ # only in where Coverage anchors the synthesized `:else`: `=>` uses the
90
+ # whole expression, `in` uses just the pattern.
109
91
  def visit_match_required_node(node)
110
- emit_oneline_pattern(node, node.location) if LEGACY_COVERAGE_LOCATIONS
92
+ emit_oneline_pattern(node, node) if LEGACY_COVERAGE_LOCATIONS
111
93
  super
112
94
  end
113
95
 
114
96
  def visit_match_predicate_node(node)
115
- emit_oneline_pattern(node, node.pattern.location) if LEGACY_COVERAGE_LOCATIONS
97
+ emit_oneline_pattern(node, node.pattern) if LEGACY_COVERAGE_LOCATIONS
116
98
  super
117
99
  end
118
- # simplecov:enable branch
119
100
 
120
- # `while` / `until` loops get a single `:body` arm. No synthetic
121
- # else (the loop either runs the body or doesn't).
101
+ # A loop gets a single `:body` arm and no synthetic else.
122
102
  def visit_while_node(node)
123
103
  emit_loop(node, :while)
124
104
  super
@@ -129,11 +109,10 @@ module SimpleCov
129
109
  super
130
110
  end
131
111
 
132
- private
112
+ private
133
113
 
134
- # IfNode and UnlessNode share a shape (predicate + then body +
135
- # optional else/elsif) but expose the trailing arm under different
136
- # accessors. `if_like_else_location` hides that split.
114
+ # IfNode and UnlessNode share a shape but expose the trailing arm under
115
+ # different accessors, which `if_like_else_location` hides.
137
116
  def emit_if_like(node, type)
138
117
  then_loc = if_like_then_location(node, type)
139
118
  else_loc = if_like_else_location(node, type)
@@ -151,32 +130,33 @@ module SimpleCov
151
130
  }
152
131
  end
153
132
 
154
- # simplecov:disable — legacy-only (3.4 emits no branch for one-line patterns)
155
- def emit_oneline_pattern(node, else_location)
156
- @branches[build_tuple(:case, node.location)] = {
157
- build_tuple(:in, node.pattern.location) => 0,
158
- build_tuple(:else, else_location) => 0
133
+ def emit_oneline_pattern(node, else_span)
134
+ @branches[build_tuple(:case, node)] = {
135
+ build_tuple(:in, node.pattern) => 0,
136
+ build_tuple(:else, else_span) => 0
159
137
  }
160
138
  end
161
- # simplecov:enable
162
139
 
163
140
  def emit_case_like(node, when_type)
164
141
  arms = node.conditions.to_h do |when_node|
165
142
  [build_tuple(when_type, case_arm_location(node, when_node, when_type)), 0]
166
143
  end
167
144
  arms[build_tuple(:else, else_arm_location(node))] = 0
168
- @branches[build_tuple(:case, node.location)] = arms
145
+ @branches[build_tuple(:case, node)] = arms
169
146
  end
170
147
 
171
148
  def emit_loop(node, type)
172
- cond_tuple = build_tuple(type, node.location)
149
+ cond_tuple = build_tuple(type, node)
173
150
  @branches[cond_tuple] = {build_tuple(:body, loop_body_location(node)) => 0}
174
151
  end
175
152
 
176
- def build_tuple(type, location)
153
+ # `span` is anything that answers the four position accessors: a location
154
+ # from LocationConventions, or the node itself where the range wanted is
155
+ # the node's own.
156
+ def build_tuple(type, span)
177
157
  id = @next_id
178
158
  @next_id += 1
179
- [type, id, location.start_line, location.start_column, location.end_line, location.end_column]
159
+ [type, id, span.start_line, span.start_column, span.end_line, span.end_column]
180
160
  end
181
161
  end
182
162
  end
@@ -1,110 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- begin
4
- require "prism"
5
- rescue LoadError
6
- # Prism isn't available on this Ruby (older than 3.3 without the gem).
7
- # `StaticCoverageExtractor.available?` will return false and callers
8
- # fall back to the previous "empty hashes" behavior.
9
- end
3
+ require "prism"
10
4
 
11
5
  module SimpleCov
12
6
  # Static enumeration of the branches and methods Ruby's `Coverage` library
13
- # WOULD have reported if a file had been loaded with `branches: true` /
7
+ # would have reported if a file had been loaded with `branches: true` /
14
8
  # `methods: true`. Used by `SimulateCoverage` to backfill data for files
15
- # added via `cover` / `track_files` that were never `require`'d during the
16
- # run — so unloaded files contribute to the branch/method denominators
17
- # symmetrically with their line coverage, instead of vanishing from the
18
- # totals (see #1059).
19
- #
20
- # Implementation uses Prism (stdlib in Ruby 3.3+, gem on older Rubies).
21
- # When Prism isn't available, `available?` returns false and SimulateCoverage
22
- # falls back to the previous behavior — older Rubies keep working, just
23
- # without the synthesized data.
9
+ # added via `cover` / `track_files` that were never required during the run,
10
+ # so unloaded files contribute to the branch/method denominators
11
+ # symmetrically with their line coverage (#1059).
24
12
  #
25
- # The emitted shape mirrors `Coverage.result[path]` for the same file:
26
- # branches are nested as `{condition_tuple => {arm_tuple => 0, ...}}` and
27
- # methods as `{["ClassName", :name, lines/cols] => 0}`. Position info
28
- # comes from Prism's reported source locations; it doesn't always match
29
- # `Coverage`'s byte-for-byte (the two parsers report slightly different
30
- # column conventions for some constructs), but lines are reliable and
31
- # downstream consumers that key off line numbers (the HTML formatter,
32
- # SonarQube, etc.) see the data they expect.
13
+ # The emitted shape mirrors `Coverage.result[path]` for the same file.
14
+ # Position info comes from Prism's reported source locations; it doesn't
15
+ # always match `Coverage`'s byte-for-byte, but lines are reliable and
16
+ # downstream consumers that key off line numbers see the data they expect.
33
17
  module StaticCoverageExtractor
34
- module_function
18
+ extend self
35
19
 
36
- # simplecov:disable branch
37
- # The Prism-unavailable arm of this ternary is unreachable when Prism
38
- # itself IS loadable i.e., on every engine that exercises the dogfood
39
- # report. Asserted-on by callers; tested indirectly via the
40
- # `available?`-returns-false fallback path in SimulateCoverage's spec.
41
- def available?
42
- defined?(::Prism) ? true : false
43
- end
44
- # simplecov:enable branch
45
-
46
- # Parse `source` (a string of Ruby) and return a hash of the form
47
- # `{"branches" => {...}, "methods" => {...}}` matching the shape that
48
- # `Coverage.result[path]` produces. Returns nil on parse failure or
49
- # when Prism isn't available; callers should treat that as "couldn't
50
- # extract — fall back to empty hashes."
20
+ # Parse `source` and return `{"branches" => {...}, "methods" => {...}}`
21
+ # matching the shape `Coverage.result[path]` produces. Returns nil on parse
22
+ # failure, which callers treat as "couldn't extract, fall back to empty
23
+ # hashes".
51
24
  def call(source)
52
- # simplecov:disable branch — `then` arm unreachable when Prism IS loadable
53
- return nil unless available?
54
-
55
- # simplecov:enable branch
56
-
57
- result = ::Prism.parse(source)
25
+ result = Prism.parse(source)
58
26
  return nil if result.failure?
59
27
 
60
28
  visitor = Visitor.new
61
29
  visitor.visit(result.value)
62
30
  {"branches" => visitor.branches, "methods" => visitor.methods}
63
- rescue StandardError
64
- # simplecov:disable line
65
- # Parser errors beyond the .failure? check, unsupported AST shapes,
66
- # or anything else: fall back to empty hashes rather than crashing
67
- # the whole report. Defensive; hard to trigger from a real source
68
- # input that Prism accepts at parse time.
31
+ rescue
32
+ # Parser errors beyond the .failure? check, unsupported AST shapes, or
33
+ # anything else: fall back to empty hashes rather than crashing the whole
34
+ # report.
69
35
  nil
70
- # simplecov:enable line
71
36
  end
72
37
 
73
- # Summarize a source file's REAL branch and method positions, for the
74
- # `:eval_generated` filter (SimpleCov.ignore_branches /
75
- # SimpleCov.ignore_methods, #1046). Returns a hash:
38
+ # Summarize a source file's real branch and method positions, for the
39
+ # `:eval_generated` filter (#1046). Answers:
76
40
  #
77
41
  # {
78
42
  # branches: Set[start_line, ...], # e.g., [3, 12, 20]
79
43
  # methods: Set[[name, start_line], ...] # e.g., [[:foo, 7], [:bar, 13]]
80
44
  # }
81
45
  #
82
- # Branch matching is start_line-only rather than by the full tuple.
83
- # Static extraction and Coverage can still disagree on a branch's exact
84
- # column positions (and, for some constructs, its type), so matching on
85
- # start_line alone is the conservative choice that tolerates those
86
- # differences. Coincidental line-sharing between a real branch and an
87
- # eval-generated one will keep both, which is an acceptable
88
- # false-negative for an opt-in filter. Method matching uses
89
- # (name, start_line) since a method name is unique at any line.
46
+ # Branch matching is start_line-only rather than by the full tuple. Static
47
+ # extraction and Coverage can still disagree on a branch's exact column
48
+ # positions, so matching on start_line alone tolerates those differences.
49
+ # Coincidental line-sharing between a real branch and an eval-generated one
50
+ # keeps both, an acceptable false-negative for an opt-in filter.
90
51
  #
91
- # Returns nil when Prism is unavailable or parsing fails, signaling
92
- # callers to keep every Coverage entry (no false drops).
52
+ # Returns nil when parsing fails, signaling callers to keep every Coverage
53
+ # entry.
93
54
  def real_source_positions(source)
94
55
  extracted = call(source)
95
56
  return nil unless extracted
96
57
 
97
58
  {
98
- branches: extracted["branches"].keys.to_set { |tuple| tuple[2] },
99
- methods: extracted["methods"].keys.to_set { |tuple| [tuple[1], tuple[2]] }
59
+ branches: extracted.fetch("branches").keys.to_set { |tuple| branch_start_line(*tuple) },
60
+ methods: extracted.fetch("methods").keys.to_set { |tuple| method_identity(*tuple) }
100
61
  }
101
62
  end
63
+
64
+ # Both keys carry their start line third, after the parts that vary between
65
+ # recordings. Read through a parameter list rather than an index, because
66
+ # every spelling of an index answers the same for a tuple of this fixed
67
+ # shape. Binding an argument has only the one spelling.
68
+ def branch_start_line(_type, _id, start_line, *)
69
+ start_line
70
+ end
71
+
72
+ def method_identity(_class_name, name, start_line, *)
73
+ [name, start_line]
74
+ end
102
75
  end
103
76
  end
104
77
 
105
- # simplecov:disable branch
106
- # The `else` arm (Prism missing) is unreachable on engines where the
107
- # dogfood report runs; the Visitor class only matters when Prism is
108
- # loadable.
109
- require_relative "static_coverage_extractor/visitor" if SimpleCov::StaticCoverageExtractor.available?
110
- # simplecov:enable branch
78
+ require_relative "static_coverage_extractor/visitor"
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class TestTracker
5
+ module Accessors
6
+ # Records the block's coverage delta as covered by `test_id`, returning the
7
+ # block's value. This is the seam test frameworks hang per-test tracking on. A
8
+ # no-op, with the block still running, unless `track_tests` is enabled and
9
+ # tracking has started, so callers never guard it.
10
+ def track_test(test_id, &)
11
+ tracker = test_tracker
12
+ return yield unless tracker
13
+
14
+ tracker.track(test_id, &)
15
+ end
16
+
17
+ def test_tracker
18
+ @test_tracker
19
+ end
20
+
21
+ # @api private. Reuses the tracker across repeated `start` calls for the same
22
+ # reason `Coverage` keeps counting across them: restarting must not lose what
23
+ # the process already recorded. The framework hooks are only wired up on the
24
+ # first.
25
+ def start_test_tracking
26
+ validate_test_tracking!
27
+ return unless track_tests? && test_tracker.nil?
28
+
29
+ @test_tracker = TestTracker.new(granularity: track_tests_granularity)
30
+ TestTracker.install_framework_hooks
31
+ end
32
+ end
33
+ end
34
+ end