simplecov 1.1.0 → 1.2.0

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