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,39 +2,27 @@
2
2
 
3
3
  module SimpleCov
4
4
  module StaticCoverageExtractor
5
- # Detects the `if` / `unless` / ternary conditions CRuby folds away.
6
- # When a condition is a statically-known-truthy/falsy literal the
7
- # compiler eliminates the dead arm and Coverage emits NO branch, so the
8
- # extractor must not synthesize one either — otherwise the arm is a
9
- # phantom that no loaded run can ever hit, the same unmergeable-tuple
10
- # failure mode as #1226 / #1233.
5
+ # Detects the `if` / `unless` / ternary conditions CRuby folds away. When a
6
+ # condition is a statically-known-truthy/falsy literal the compiler
7
+ # eliminates the dead arm and Coverage emits no branch, so the extractor
8
+ # must not synthesize one either: the arm would be a phantom no loaded run
9
+ # can hit, the same unmergeable-tuple failure as #1226 / #1233.
11
10
  module ConditionFolding
12
- # CRuby 3.4 rebuilt the fold on the Prism compiler, and the
13
- # parse.y-based fold it replaced differed in three observable ways,
14
- # each pinned by the runtime tuple equivalence battery on CI:
15
- # `__FILE__` folded on 3.2/3.3 but no longer does; parentheses were
16
- # transparent for every literal on 3.2 (opacity starts at 3.3); and
17
- # on 3.2 the dead arm's branch table entries survive the fold —
18
- # parse.y instrumented branches before eliminating dead code — while
19
- # its `def`s still never register.
11
+ # CRuby 3.4 rebuilt the fold on the Prism compiler, and the parse.y fold
12
+ # it replaced still differs on 3.3, where `__FILE__` folds.
20
13
  FOLDS_SOURCE_FILE = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
21
- PARENS_ALWAYS_TRANSPARENT = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.3")
22
- DEAD_ARM_BRANCHES_SURVIVE = PARENS_ALWAYS_TRANSPARENT
23
- # Container literals in discarded position are eliminated from 3.3
24
- # on, but the contents rule differs: 3.3's compile.c elides a
25
- # container whose contents are merely effect-free (`[x]`, `[self]`),
26
- # while the Prism compiler (3.4+) demands fully static literals
27
- # (`[1]` goes, `[x]` stays). See `static_container_literal?`.
14
+ # Container literals in discarded position are eliminated from 3.3 on, but
15
+ # 3.3's compile.c elides a container whose contents are merely
16
+ # effect-free (`[x]`), while the Prism compiler demands fully static
17
+ # literals (`[1]` goes, `[x]` stays).
28
18
  CONTAINER_CONTENTS_NEED_STATIC_LITERALS = !FOLDS_SOURCE_FILE
29
19
 
30
- # Prism node types for the literals that fold. `while` / `until` do
31
- # NOT fold (`while true` is a real branch), so only the if-like
32
- # visitors consult this. Regexp and Range literals are excluded on
33
- # purpose: as conditions they mean `=~ $_` / flip-flop, which
34
- # Coverage does branch on. `[]`, `{}`, and interpolated strings do
35
- # not fold either, and `->` folds while a `lambda` call does not —
36
- # the compiler only folds what it can prove at compile time, and a
37
- # method named `lambda` proves nothing.
20
+ # The literals that fold. `while` / `until` do NOT fold (`while true` is a
21
+ # real branch), so only the if-like visitors consult this. Regexp and
22
+ # Range literals are excluded on purpose: as conditions they mean
23
+ # `=~ $_` / flip-flop, which Coverage does branch on. `[]`, `{}`, and
24
+ # interpolated strings do not fold either, and `->` folds while a
25
+ # `lambda` call does not: a method named `lambda` proves nothing.
38
26
  # simplecov:disable branch — which arm runs is fixed by the running Ruby's version
39
27
  STATIC_CONDITION_TYPES = [
40
28
  ::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
@@ -45,27 +33,18 @@ module SimpleCov
45
33
  ].freeze
46
34
  # simplecov:enable branch
47
35
 
48
- # The literals whose fold eliminates the *then* side: `if false` /
49
- # `if nil` keep only the else arm. Every other folded literal is
50
- # truthy and keeps only the then arm.
51
36
  FALSY_CONDITION_TYPES = [::Prism::FalseNode, ::Prism::NilNode].freeze
52
37
 
53
- # The literals whose fold does NOT see through parentheses: CRuby
54
- # folds `if nil`, `if "x"`, and `if -> {}` but keeps a real branch
55
- # for `if (nil)`, `if ("x")`, and `if (-> {})` — verified against
56
- # Coverage, and pinned by the runtime tuple equivalence battery —
57
- # while every other literal folds parenthesized or not. `__FILE__`
58
- # is opaque like other strings on the Rubies that fold it at all.
59
- # Consulted only when PARENS_ALWAYS_TRANSPARENT is false.
38
+ # CRuby folds `if nil`, `if "x"`, and `if -> {}` but keeps a real branch
39
+ # for `if (nil)`, `if ("x")`, and `if (-> {})`, while every other literal
40
+ # folds parenthesized or not.
60
41
  PAREN_OPAQUE_TYPES = [
61
42
  ::Prism::NilNode, ::Prism::StringNode, ::Prism::LambdaNode, ::Prism::SourceFileNode
62
43
  ].freeze
63
44
 
64
- # The scalar literals the compiler treats as fully static: a
65
- # multi-statement paren condition (`if (1; 2)`) folds by its last
45
+ # A multi-statement paren condition (`if (1; 2)`) folds by its last
66
46
  # expression only when every leading statement is eliminated when
67
- # discarded, and these bare or composing an Array/Hash/Range
68
- # always are. Pinned against real Coverage on 3.2 through 4.0.
47
+ # discarded, and these always are, bare or composing an Array/Hash/Range.
69
48
  STATIC_LITERAL_LEAF_TYPES = [
70
49
  ::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
71
50
  ::Prism::ImaginaryNode, ::Prism::StringNode, ::Prism::SymbolNode,
@@ -73,100 +52,53 @@ module SimpleCov
73
52
  ::Prism::SourceLineNode, ::Prism::SourceFileNode, ::Prism::SourceEncodingNode, ::Prism::RegularExpressionNode
74
53
  ].freeze
75
54
 
76
- # Non-literal reads that are also eliminated when discarded.
77
- # `self` is eliminated by every supported compiler; local/ivar/
78
- # defined? elimination arrived with the Prism-era compilers.
79
- # Anything that can raise or run hooks (constants, globals, calls,
80
- # writes) is never eliminated and keeps the branch real.
81
- PRISM_ERA_ELIMINABLE_READS = [
82
- ::Prism::LocalVariableReadNode, ::Prism::InstanceVariableReadNode, ::Prism::DefinedNode
83
- ].freeze
84
-
85
- # simplecov:disable branch — which arm runs is fixed by the running Ruby's version
55
+ # Non-literal reads that are also eliminated when discarded. Anything that
56
+ # can raise or run hooks is never eliminated and keeps the branch real.
86
57
  ELIMINABLE_READ_TYPES = [
87
- ::Prism::SelfNode,
88
- *(PRISM_ERA_ELIMINABLE_READS unless PARENS_ALWAYS_TRANSPARENT)
58
+ ::Prism::SelfNode, ::Prism::LocalVariableReadNode,
59
+ ::Prism::InstanceVariableReadNode, ::Prism::DefinedNode
89
60
  ].freeze
90
- # simplecov:enable branch
91
61
 
92
- private
62
+ private
93
63
 
94
- # A truthy verdict keeps the first arm live, a falsy one the
95
- # second. `visit` is nil-safe, so a missing arm just visits
96
- # nothing. On 3.2 the dead arm's branch table entries survive the
97
- # fold (parse.y instrumented branches before eliminating dead
98
- # code, even inside dead `def` bodies and lambdas) while its
99
- # methods never register, so the dead arm is visited with method
100
- # collection suppressed.
64
+ # `visit` is nil-safe, so a missing arm just visits nothing.
101
65
  def visit_folded_arms(verdict, truthy_arm, falsy_arm)
102
- live, dead = verdict == :truthy ? [truthy_arm, falsy_arm] : [falsy_arm, truthy_arm]
103
- visit(live)
104
- # simplecov:disable branch — the taken arm is fixed by the running Ruby's version
105
- visit_dead_arm(dead) if DEAD_ARM_BRANCHES_SURVIVE
106
- # simplecov:enable branch
66
+ visit(verdict.equal?(:truthy) ? truthy_arm : falsy_arm)
107
67
  end
108
68
 
109
- # simplecov:disable 3.2-only; unreachable on the modern dogfood Ruby
110
- def visit_dead_arm(arm)
111
- # Save/restore rather than set/clear: a fold nested inside this
112
- # dead arm re-enters here (possibly with a nil arm) and must not
113
- # clear suppression for the rest of the outer dead arm.
114
- previous = @suppress_methods
115
- begin
116
- @suppress_methods = true
117
- visit(arm)
118
- ensure
119
- @suppress_methods = previous
120
- end
121
- end
122
- # simplecov:enable
123
-
124
- # The compiler's verdict on a condition: `:truthy` or `:falsy` when
125
- # it folds, nil when it doesn't. The verdict decides which arm
126
- # survives — the compiler eliminates the other arm's entire subtree,
127
- # so everything inside it (nested branches, methods) must go
128
- # unvisited too, not just the folded condition's own tuple.
69
+ # `:truthy` or `:falsy` when the compiler folds, nil when it doesn't. The
70
+ # compiler eliminates the losing arm's entire subtree, so everything
71
+ # inside it must go unvisited too, not just the folded condition's tuple.
129
72
  #
130
- # Parentheses are transparent to the fold for most literals
131
- # (`if (1)` folds like `if 1`) but not all see
132
- # PAREN_OPAQUE_TYPES. Compound forms (`!true`, `true || x`) are
133
- # deliberately not folded: `!` never folds, and `||` / `&&`
134
- # constant-propagation diverges across Ruby versions, so matching
135
- # it would trade a rare, version-specific gain for real risk.
73
+ # Compound forms (`!true`, `true || x`) are deliberately not folded: `!`
74
+ # never folds, and `||` / `&&` constant-propagation diverges across Ruby
75
+ # versions, so matching it would trade a rare gain for real risk.
136
76
  def folded_condition(node)
137
77
  unwrapped = unwrap_parentheses(node)
138
78
  return nil unless foldable?(node, unwrapped)
139
79
 
140
- FALSY_CONDITION_TYPES.any? { |type| unwrapped.is_a?(type) } ? :falsy : :truthy
80
+ (FALSY_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }) ? :falsy : :truthy
141
81
  end
142
82
 
143
- # A folding literal, minus the ones parentheses shield from the
144
- # fold (`unwrapped` differing from `node` is what says parentheses
145
- # were seen through).
83
+ # `unwrapped` differing from `node` is what says parentheses were seen
84
+ # through.
146
85
  def foldable?(node, unwrapped)
147
- return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.is_a?(type) }
148
-
149
- # simplecov:disable — which of these lines runs is fixed by the
150
- # running Ruby's version: on 3.2 the early return always fires and
151
- # the opacity check below it is dead code, so it must be excluded
152
- # from line coverage too, not only branch coverage.
153
- return true if PARENS_ALWAYS_TRANSPARENT
86
+ return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }
154
87
 
155
- unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.is_a?(type) }
156
- # simplecov:enable
88
+ unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.instance_of?(type) }
157
89
  end
158
90
 
159
- # Parentheses are transparent to the fold, and a multi-statement
160
- # body (`if (1; 2)`) folds by its LAST expression — but only when
161
- # the compiler eliminates every leading statement. Stopping at
162
- # multi-statement bodies synthesized a phantom then/else pair no
163
- # real run can ever hit.
91
+ # A multi-statement body (`if (1; 2)`) folds by its LAST expression, but
92
+ # only when the compiler eliminates every leading statement. Stopping at
93
+ # multi-statement bodies synthesized a phantom then/else pair.
164
94
  def unwrap_parentheses(node)
165
95
  # @type var current: untyped
166
96
  current = node
167
- while current.is_a?(::Prism::ParenthesesNode)
97
+ while current.instance_of?(Prism::ParenthesesNode)
98
+ # Empty parentheses carry no statements node at all, so a
99
+ # body that is one always holds at least one statement.
168
100
  body = current.body
169
- break unless body.is_a?(::Prism::StatementsNode) && !body.body.empty?
101
+ break unless body.instance_of?(Prism::StatementsNode)
170
102
 
171
103
  statements = body.body
172
104
  break unless statements.take(statements.size - 1).all? { |leading| eliminable_when_discarded?(leading) }
@@ -176,40 +108,26 @@ module SimpleCov
176
108
  current
177
109
  end
178
110
 
179
- # Whether the compiler compiles `node` to nothing when its value is
180
- # discarded.
181
111
  def eliminable_when_discarded?(node)
182
112
  return true if static_container_literal?(node)
183
- return true if ELIMINABLE_READ_TYPES.any? { |type| node.is_a?(type) }
113
+ return true if ELIMINABLE_READ_TYPES.any? { |type| node.instance_of?(type) }
184
114
 
185
- node.is_a?(::Prism::ParenthesesNode) &&
186
- node.body.is_a?(::Prism::StatementsNode) &&
115
+ node.instance_of?(Prism::ParenthesesNode) &&
116
+ node.body.instance_of?(Prism::StatementsNode) &&
187
117
  node.body.body.all? { |statement| eliminable_when_discarded?(statement) }
188
118
  end
189
119
 
190
- # A scalar literal leaf, or an Array/Hash/Range whose contents pass
191
- # the running compiler's contents rule (see
192
- # CONTAINER_CONTENTS_NEED_STATIC_LITERALS). Container elimination
193
- # only exists from 3.3 on.
194
120
  def static_container_literal?(node)
195
- return true if STATIC_LITERAL_LEAF_TYPES.any? { |type| node.is_a?(type) }
196
- # simplecov:disable — which of these lines runs is fixed by the
197
- # running Ruby's version: parse.y (3.2) never eliminates
198
- # container literals, so there the early return always fires and
199
- # the dispatch below is dead code.
200
- return false if PARENS_ALWAYS_TRANSPARENT
121
+ return true if STATIC_LITERAL_LEAF_TYPES.any? { |type| node.instance_of?(type) }
201
122
 
202
123
  static_container?(node)
203
- # simplecov:enable
204
124
  end
205
125
 
206
- # The container dispatch, version-free: the predicate specs
207
- # exercise it directly on every supported Ruby, 3.2 included.
208
126
  def static_container?(node)
209
127
  case node
210
- when ::Prism::ArrayNode then static_array_literal?(node)
211
- when ::Prism::HashNode then static_hash_literal?(node)
212
- when ::Prism::RangeNode then static_range_literal?(node)
128
+ when Prism::ArrayNode then static_array_literal?(node)
129
+ when Prism::HashNode then static_hash_literal?(node)
130
+ when Prism::RangeNode then static_range_literal?(node)
213
131
  else false
214
132
  end
215
133
  end
@@ -220,7 +138,7 @@ module SimpleCov
220
138
 
221
139
  def static_hash_literal?(node)
222
140
  node.elements.all? do |element|
223
- element.is_a?(::Prism::AssocNode) &&
141
+ element.instance_of?(Prism::AssocNode) &&
224
142
  container_contents_eliminable?(element.key) && container_contents_eliminable?(element.value)
225
143
  end
226
144
  end
@@ -230,9 +148,7 @@ module SimpleCov
230
148
  end
231
149
 
232
150
  def container_contents_eliminable?(node)
233
- # simplecov:disable branch — which arm runs is fixed by the running Ruby's version
234
151
  CONTAINER_CONTENTS_NEED_STATIC_LITERALS ? static_container_literal?(node) : eliminable_when_discarded?(node)
235
- # simplecov:enable branch
236
152
  end
237
153
  end
238
154
  end
@@ -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