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
@@ -2,36 +2,35 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Reads a source file into an array of lines, honoring the source's
6
- # shebang and `coding:` magic comment when present. Always
7
- # transcodes to UTF-8 with invalid/undef bytes replaced both for
8
- # JRuby compatibility and to keep encoding shenanigans in one place
9
- # (see #866).
5
+ # Reads a source file into an array of lines, honoring the source's shebang
6
+ # and `coding:` magic comment when present. Always transcodes to UTF-8 with
7
+ # invalid and undefined bytes replaced, both for JRuby compatibility and to
8
+ # keep encoding shenanigans in one place (#866).
10
9
  module SourceLoader
11
10
  SHEBANG_REGEX = /\A#!/
12
11
  RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX = /\A#\s*(?:-\*-)?\s*(?:en)?coding:\s*(\S+)\s*(?:-\*-)?\s*\z/
13
12
 
14
- module_function
13
+ extend self
15
14
 
16
15
  def call(filename)
17
16
  lines = [] #: Array[String]
18
- # The default encoding is UTF-8
19
- File.open(filename, "rb:UTF-8") do |file|
20
- current_line = scrub_invalid(file.gets)
21
-
22
- if current_line && shebang?(current_line)
23
- lines << current_line
24
- current_line = scrub_invalid(file.gets)
25
- end
26
-
27
- read_lines(file, lines, current_line)
17
+ open_file(filename, "rb:UTF-8") do |file|
18
+ read_lines(file, lines, scrub_invalid(file.gets))
28
19
  end
29
20
  end
30
21
 
31
- # A line read as UTF-8 can still carry invalid bytes (a Latin-1
32
- # source file without a magic comment, say). Replace them before
33
- # any regex sees the line: the shebang and magic-comment checks
34
- # would otherwise raise ArgumentError and take the report down.
22
+ # mutant:disable Ruby 4.0 removed Kernel#open's leading-pipe
23
+ # command mode, so no test can tell `File.open` from `open` here
24
+ # any more. The explicit receiver is kept: on older rubies it is
25
+ # what refuses to run a filename as a command.
26
+ def open_file(name, mode, &)
27
+ File.open(name, mode, &)
28
+ end
29
+
30
+ # A line read as UTF-8 can still carry invalid bytes, from a Latin-1 source
31
+ # file without a magic comment, say. They are replaced before any regex sees
32
+ # the line: the shebang and magic-comment checks would otherwise raise
33
+ # ArgumentError and take the report down.
35
34
  def scrub_invalid(line)
36
35
  return line if line.nil? || line.valid_encoding?
37
36
 
@@ -45,11 +44,17 @@ module SimpleCov
45
44
  def read_lines(file, lines, current_line)
46
45
  return lines unless current_line
47
46
 
47
+ if shebang?(current_line)
48
+ lines << current_line
49
+ current_line = scrub_invalid(file.gets)
50
+ return lines unless current_line
51
+ end
52
+
48
53
  set_encoding_based_on_magic_comment(file, current_line)
49
54
  lines.concat([current_line], ensure_remove_undefs(file.readlines))
50
55
  end
51
56
 
52
- # Encoding magic comment must be placed at first line except for
57
+ # An encoding magic comment must be placed on the first line, except after a
53
58
  # shebang.
54
59
  def set_encoding_based_on_magic_comment(file, line)
55
60
  if (match = RUBY_FILE_ENCODING_MAGIC_COMMENT_REGEX.match(line))
@@ -57,21 +62,26 @@ module SimpleCov
57
62
  end
58
63
  end
59
64
 
60
- # Guarantee every line leaves the loader as valid UTF-8, replacing
61
- # what can't be represented: transcode non-UTF-8 lines (setting
62
- # invalid/undef options on `file.set_encoding` doesn't work
63
- # properly, and this also works around a JRuby incompatibility)
64
- # and scrub UTF-8-tagged lines that carry invalid bytes, which
65
- # would otherwise raise from every regex the classifier runs.
65
+ # Setting invalid/undef options on `file.set_encoding` doesn't work
66
+ # properly, and transcoding here also works around a JRuby incompatibility.
66
67
  def ensure_remove_undefs(file_lines)
67
- file_lines.each do |line|
68
- # simplecov:disable — defensive: only fires for non-UTF-8 source files
69
- if line.encoding == Encoding::UTF_8
70
- line.scrub! unless line.valid_encoding?
71
- else
72
- line.encode!("UTF-8", invalid: :replace, undef: :replace)
73
- end
74
- # simplecov:enable
68
+ file_lines.each { |line| make_utf8(line) }
69
+ end
70
+
71
+ # mutant:disable — on CRuby the two arms answer alike, because
72
+ # converting to the encoding a string already carries scrubs it
73
+ # when `invalid: :replace` is given. The arms are kept apart for
74
+ # the engines where that conversion is the documented no-op it
75
+ # reads as, which is the same reason the transcode is here rather
76
+ # than on `file.set_encoding`.
77
+ # Encodings are singletons, so identity is the whole of the question. A line
78
+ # already tagged UTF-8 has nothing to transcode, only invalid bytes to
79
+ # replace, and scrubbing a line that has none of those leaves it alone.
80
+ def make_utf8(line)
81
+ if line.encoding.equal?(Encoding::UTF_8)
82
+ line.scrub!
83
+ else
84
+ line.encode!("UTF-8", invalid: :replace, undef: :replace)
75
85
  end
76
86
  end
77
87
  end
@@ -2,10 +2,9 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Builds the `CoverageStatistics` triple (`:line`, `:branch`, `:method`)
6
- # for a SourceFile, regardless of which criteria were actually enabled
7
- # during the run disabled or empty criteria collapse to 0/0/0 so
8
- # downstream consumers don't have to special-case enable-state.
5
+ # Builds the `CoverageStatistics` triple for a SourceFile regardless of which
6
+ # criteria were enabled during the run: disabled or empty criteria collapse to
7
+ # 0/0/0 so downstream consumers don't have to special-case enable-state.
9
8
  class Statistics
10
9
  def initialize(source_file)
11
10
  @source_file = source_file
@@ -19,7 +18,7 @@ module SimpleCov
19
18
  }
20
19
  end
21
20
 
22
- private
21
+ private
23
22
 
24
23
  def line_statistics
25
24
  {
@@ -31,13 +30,15 @@ module SimpleCov
31
30
  }
32
31
  end
33
32
 
34
- # Files added via track_files but never loaded have no branch/method
35
- # data. Report 0% instead of the empty-set default of 100% (see #902).
33
+ # Files added via track_files but never loaded have no branch or method data,
34
+ # and report 0% instead of the empty-set default of 100% (#902). A file with
35
+ # missed entries and none covered already computes to 0%, so only a file that
36
+ # really has covered entries keeps its computed percentage.
36
37
  def branch_statistics
37
38
  sf = @source_file
38
39
  covered = sf.covered_branches
39
40
  missed = sf.missed_branches
40
- percent = 0.0 if sf.not_loaded? && covered.empty? && missed.empty?
41
+ percent = 0.0 if sf.not_loaded? && covered.empty?
41
42
 
42
43
  {branch: coverage_statistics(covered, missed, percent: percent)}
43
44
  end
@@ -46,7 +47,7 @@ module SimpleCov
46
47
  sf = @source_file
47
48
  covered = sf.covered_methods
48
49
  missed = sf.missed_methods
49
- percent = 0.0 if sf.not_loaded? && covered.empty? && missed.empty?
50
+ percent = 0.0 if sf.not_loaded? && covered.empty?
50
51
 
51
52
  {method: coverage_statistics(covered, missed, percent: percent)}
52
53
  end
@@ -12,17 +12,10 @@ require_relative "source_file/method_builder"
12
12
  require_relative "source_file/statistics"
13
13
 
14
14
  module SimpleCov
15
- #
16
- # Representation of a source file including it's coverage data, source code,
17
- # source lines and featuring helpers to interpret that data.
18
- #
19
15
  class SourceFile
20
16
  include BuilderContext
21
17
 
22
- # The full path to this source file (e.g. /User/colszowka/projects/simplecov/lib/simplecov/source_file.rb)
23
- attr_reader :filename
24
- # The array of coverage data received from the Coverage.result
25
- attr_reader :coverage_data
18
+ attr_reader :filename, :coverage_data
26
19
 
27
20
  def initialize(filename, coverage_data, loaded: true)
28
21
  @filename = filename
@@ -30,72 +23,57 @@ module SimpleCov
30
23
  @loaded = loaded
31
24
  end
32
25
 
33
- # The path to this source file relative to the projects directory
34
26
  def project_filename
35
27
  @filename.delete_prefix(SimpleCov.root).sub(%r{\A[/\\]}, "")
36
28
  end
37
29
 
38
- # The source code for this file. Aliased as :source.
39
- # Intentionally read lazily to suppress reading unused source code.
30
+ # Read lazily to suppress reading unused source code.
40
31
  def src
41
32
  @src ||= SourceLoader.call(filename)
42
33
  end
43
- alias source src
34
+ alias_method :source, :src
44
35
 
45
- # Returns a hash keyed by every supported coverage criterion. Each
46
- # value is a CoverageStatistics, even for criteria that weren't
47
- # enabled during the run — those collapse to 0/0/0. Consumers
48
- # (FileList, formatters) decide which keys to surface based on
49
- # `SimpleCov.coverage_criterion_enabled?`.
50
36
  # The per-criterion coverage statistics for this file. With no argument
51
- # returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
52
- # (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
37
+ # answers the `{line:, branch:, method:}` Hash, and with a criterion symbol
38
+ # that one CoverageStatistics. Every criterion is present even when it
39
+ # wasn't enabled during the run, collapsed to 0/0/0; `FileList` is the
40
+ # layer that filters to the enabled set.
53
41
  def coverage_statistics(criterion = nil)
54
42
  stats = (@coverage_statistics ||= Statistics.new(self).call)
55
43
  criterion ? stats[criterion] : stats
56
44
  end
57
45
 
58
- # Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
59
- # and thus including coverage data. Aliased as :source_lines
60
46
  def lines
61
47
  @lines ||= LineBuilder.new(self).call
62
48
  end
63
- alias source_lines lines
49
+ alias_method :source_lines, :lines
64
50
 
65
- # Returns all covered lines as SimpleCov::SourceFile::Line
66
51
  def covered_lines
67
52
  @covered_lines ||= lines.select(&:covered?)
68
53
  end
69
54
 
70
- # Returns all lines that should have been, but were not covered
71
- # as instances of SimpleCov::SourceFile::Line
72
55
  def missed_lines
73
56
  @missed_lines ||= lines.select(&:missed?)
74
57
  end
75
58
 
76
- # Returns all lines that are not relevant for coverage as
77
- # SimpleCov::SourceFile::Line instances
78
59
  def never_lines
79
60
  @never_lines ||= lines.select(&:never?)
80
61
  end
81
62
 
82
- # Returns all lines that were skipped as SimpleCov::SourceFile::Line instances
83
63
  def skipped_lines
84
64
  @skipped_lines ||= lines.select(&:skipped?)
85
65
  end
86
66
 
87
- # Returns the number of relevant lines (covered + missed)
88
67
  def lines_of_code
89
68
  coverage_statistics[:line]&.total || 0
90
69
  end
91
70
 
92
- # Access SimpleCov::SourceFile::Line source lines by line number
71
+ # Answers nil for a number this file has no line for.
93
72
  def line(number)
94
- lines[number - 1]
73
+ lines.at(number - 1)
95
74
  end
96
75
 
97
- # The coverage for this file in percent, for the given criterion (line by
98
- # default). Returns nil if the criterion was not measured.
76
+ # Nil if the criterion was not measured.
99
77
  def covered_percent(criterion = :line)
100
78
  coverage_statistics(criterion)&.percent
101
79
  end
@@ -104,15 +82,16 @@ module SimpleCov
104
82
  coverage_statistics(criterion)&.strength
105
83
  end
106
84
 
85
+ # Every line the file has is irrelevant, a file with no lines at all
86
+ # included.
107
87
  def no_lines?
108
- lines.empty? || (lines.length == never_lines.size)
88
+ lines.all?(&:never?)
109
89
  end
110
90
 
111
91
  def relevant_lines
112
92
  lines.size - never_lines.size - skipped_lines.size
113
93
  end
114
94
 
115
- # Return all the branches inside current source file
116
95
  def branches
117
96
  @branches ||= BranchBuilder.new(self).call
118
97
  end
@@ -121,33 +100,28 @@ module SimpleCov
121
100
  total_branches.empty?
122
101
  end
123
102
 
124
- # DEPRECATED: use `covered_percent(:branch)`.
125
103
  def branches_coverage_percent
126
- SimpleCov::Deprecation.warn("`SimpleCov::SourceFile#branches_coverage_percent` is deprecated. " \
127
- "Use `covered_percent(:branch)`.")
104
+ Deprecation.warn("`SimpleCov::SourceFile#branches_coverage_percent` is deprecated. " \
105
+ "Use `covered_percent(:branch)`.")
128
106
  covered_percent(:branch)
129
107
  end
130
108
 
131
- # Return the relevant branches to source file
132
109
  def total_branches
133
110
  @total_branches ||= covered_branches + missed_branches
134
111
  end
135
112
 
136
- # Return hash with key of line number and branch coverage count as value
137
113
  def branches_report
138
114
  @branches_report ||=
139
115
  branches.reject(&:skipped?).group_by(&:report_line).transform_values { |bs| bs.map(&:report) }
140
116
  end
141
117
 
142
- # Select the covered branches. We use a tree schema here because
143
- # some conditions like `case` may have an additional `else` that
144
- # isn't declared in code but is given by default by the coverage
145
- # report.
118
+ # A tree schema, because some conditions like `case` may have an additional
119
+ # `else` that isn't declared in code but is given by default by the
120
+ # coverage report.
146
121
  def covered_branches
147
122
  @covered_branches ||= branches.select(&:covered?)
148
123
  end
149
124
 
150
- # Select the missed branches with coverage equal to zero
151
125
  def missed_branches
152
126
  @missed_branches ||= branches.select(&:missed?)
153
127
  end
@@ -156,12 +130,10 @@ module SimpleCov
156
130
  branches_report.fetch(line_number, [])
157
131
  end
158
132
 
159
- # Check if any branches missing on given line number
160
133
  def line_with_missed_branch?(line_number)
161
134
  branches_for_line(line_number).any? { |_type, count| count.zero? }
162
135
  end
163
136
 
164
- # Return all methods detected in this source file
165
137
  def methods
166
138
  @methods ||= MethodBuilder.new(self).call
167
139
  end
@@ -174,14 +146,12 @@ module SimpleCov
174
146
  @missed_methods ||= methods.select(&:missed?)
175
147
  end
176
148
 
177
- # DEPRECATED: use `covered_percent(:method)`.
178
149
  def methods_coverage_percent
179
- SimpleCov::Deprecation.warn("`SimpleCov::SourceFile#methods_coverage_percent` is deprecated. " \
180
- "Use `covered_percent(:method)`.")
150
+ Deprecation.warn("`SimpleCov::SourceFile#methods_coverage_percent` is deprecated. " \
151
+ "Use `covered_percent(:method)`.")
181
152
  covered_percent(:method)
182
153
  end
183
154
 
184
- # Whether this file was added via track_files but never loaded/required.
185
155
  def not_loaded?
186
156
  !@loaded
187
157
  end
@@ -2,39 +2,32 @@
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 it
12
+ # replaced differed in three observable ways: `__FILE__` folded on
13
+ # 3.2/3.3; parentheses were transparent for every literal on 3.2; and on
14
+ # 3.2 the dead arm's branch table entries survive the fold while its
15
+ # `def`s still never register.
20
16
  FOLDS_SOURCE_FILE = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.4")
21
17
  PARENS_ALWAYS_TRANSPARENT = Gem::Version.new(RUBY_VERSION) < Gem::Version.new("3.3")
22
18
  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?`.
19
+ # Container literals in discarded position are eliminated from 3.3 on, but
20
+ # 3.3's compile.c elides a container whose contents are merely
21
+ # effect-free (`[x]`), while the Prism compiler demands fully static
22
+ # literals (`[1]` goes, `[x]` stays).
28
23
  CONTAINER_CONTENTS_NEED_STATIC_LITERALS = !FOLDS_SOURCE_FILE
29
24
 
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.
25
+ # The literals that fold. `while` / `until` do NOT fold (`while true` is a
26
+ # real branch), so only the if-like visitors consult this. Regexp and
27
+ # Range literals are excluded on purpose: as conditions they mean
28
+ # `=~ $_` / flip-flop, which Coverage does branch on. `[]`, `{}`, and
29
+ # interpolated strings do not fold either, and `->` folds while a
30
+ # `lambda` call does not: a method named `lambda` proves nothing.
38
31
  # simplecov:disable branch — which arm runs is fixed by the running Ruby's version
39
32
  STATIC_CONDITION_TYPES = [
40
33
  ::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
@@ -45,27 +38,19 @@ module SimpleCov
45
38
  ].freeze
46
39
  # simplecov:enable branch
47
40
 
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
41
  FALSY_CONDITION_TYPES = [::Prism::FalseNode, ::Prism::NilNode].freeze
52
42
 
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.
43
+ # CRuby folds `if nil`, `if "x"`, and `if -> {}` but keeps a real branch
44
+ # for `if (nil)`, `if ("x")`, and `if (-> {})`, while every other literal
45
+ # folds parenthesized or not. Consulted only when parentheses are not
46
+ # always transparent.
60
47
  PAREN_OPAQUE_TYPES = [
61
48
  ::Prism::NilNode, ::Prism::StringNode, ::Prism::LambdaNode, ::Prism::SourceFileNode
62
49
  ].freeze
63
50
 
64
- # The scalar literals the compiler treats as fully static: a
65
- # multi-statement paren condition (`if (1; 2)`) folds by its last
51
+ # A multi-statement paren condition (`if (1; 2)`) folds by its last
66
52
  # 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.
53
+ # discarded, and these always are, bare or composing an Array/Hash/Range.
69
54
  STATIC_LITERAL_LEAF_TYPES = [
70
55
  ::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
71
56
  ::Prism::ImaginaryNode, ::Prism::StringNode, ::Prism::SymbolNode,
@@ -73,11 +58,9 @@ module SimpleCov
73
58
  ::Prism::SourceLineNode, ::Prism::SourceFileNode, ::Prism::SourceEncodingNode, ::Prism::RegularExpressionNode
74
59
  ].freeze
75
60
 
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.
61
+ # `self` is eliminated by every supported compiler; local/ivar/defined?
62
+ # elimination arrived with the Prism-era compilers. Anything that can
63
+ # raise or run hooks is never eliminated and keeps the branch real.
81
64
  PRISM_ERA_ELIMINABLE_READS = [
82
65
  ::Prism::LocalVariableReadNode, ::Prism::InstanceVariableReadNode, ::Prism::DefinedNode
83
66
  ].freeze
@@ -89,28 +72,21 @@ module SimpleCov
89
72
  ].freeze
90
73
  # simplecov:enable branch
91
74
 
92
- private
75
+ private
93
76
 
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.
77
+ # On 3.2 the dead arm's branch table entries survive the fold (parse.y
78
+ # instrumented branches before eliminating dead code) while its methods
79
+ # never register, so the dead arm is visited with method collection
80
+ # suppressed.
101
81
  def visit_folded_arms(verdict, truthy_arm, falsy_arm)
102
- live, dead = verdict == :truthy ? [truthy_arm, falsy_arm] : [falsy_arm, truthy_arm]
82
+ live, dead = verdict.equal?(:truthy) ? [truthy_arm, falsy_arm] : [falsy_arm, truthy_arm]
103
83
  visit(live)
104
- # simplecov:disable branch — the taken arm is fixed by the running Ruby's version
105
84
  visit_dead_arm(dead) if DEAD_ARM_BRANCHES_SURVIVE
106
- # simplecov:enable branch
107
85
  end
108
86
 
109
- # simplecov:disable — 3.2-only; unreachable on the modern dogfood Ruby
110
87
  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.
88
+ # Save/restore rather than set/clear: a fold nested inside this dead arm
89
+ # re-enters here and must not clear suppression for the rest of it.
114
90
  previous = @suppress_methods
115
91
  begin
116
92
  @suppress_methods = true
@@ -119,54 +95,42 @@ module SimpleCov
119
95
  @suppress_methods = previous
120
96
  end
121
97
  end
122
- # simplecov:enable
123
98
 
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.
99
+ # `:truthy` or `:falsy` when the compiler folds, nil when it doesn't. The
100
+ # compiler eliminates the losing arm's entire subtree, so everything
101
+ # inside it must go unvisited too, not just the folded condition's tuple.
129
102
  #
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.
103
+ # Compound forms (`!true`, `true || x`) are deliberately not folded: `!`
104
+ # never folds, and `||` / `&&` constant-propagation diverges across Ruby
105
+ # versions, so matching it would trade a rare gain for real risk.
136
106
  def folded_condition(node)
137
107
  unwrapped = unwrap_parentheses(node)
138
108
  return nil unless foldable?(node, unwrapped)
139
109
 
140
- FALSY_CONDITION_TYPES.any? { |type| unwrapped.is_a?(type) } ? :falsy : :truthy
110
+ (FALSY_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }) ? :falsy : :truthy
141
111
  end
142
112
 
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).
113
+ # `unwrapped` differing from `node` is what says parentheses were seen
114
+ # through.
146
115
  def foldable?(node, unwrapped)
147
- return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.is_a?(type) }
116
+ return false unless STATIC_CONDITION_TYPES.any? { |type| unwrapped.instance_of?(type) }
148
117
 
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
118
  return true if PARENS_ALWAYS_TRANSPARENT
154
119
 
155
- unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.is_a?(type) }
156
- # simplecov:enable
120
+ unwrapped.equal?(node) || PAREN_OPAQUE_TYPES.none? { |type| unwrapped.instance_of?(type) }
157
121
  end
158
122
 
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.
123
+ # A multi-statement body (`if (1; 2)`) folds by its LAST expression, but
124
+ # only when the compiler eliminates every leading statement. Stopping at
125
+ # multi-statement bodies synthesized a phantom then/else pair.
164
126
  def unwrap_parentheses(node)
165
127
  # @type var current: untyped
166
128
  current = node
167
- while current.is_a?(::Prism::ParenthesesNode)
129
+ while current.instance_of?(Prism::ParenthesesNode)
130
+ # Empty parentheses carry no statements node at all, so a
131
+ # body that is one always holds at least one statement.
168
132
  body = current.body
169
- break unless body.is_a?(::Prism::StatementsNode) && !body.body.empty?
133
+ break unless body.instance_of?(Prism::StatementsNode)
170
134
 
171
135
  statements = body.body
172
136
  break unless statements.take(statements.size - 1).all? { |leading| eliminable_when_discarded?(leading) }
@@ -176,40 +140,27 @@ module SimpleCov
176
140
  current
177
141
  end
178
142
 
179
- # Whether the compiler compiles `node` to nothing when its value is
180
- # discarded.
181
143
  def eliminable_when_discarded?(node)
182
144
  return true if static_container_literal?(node)
183
- return true if ELIMINABLE_READ_TYPES.any? { |type| node.is_a?(type) }
145
+ return true if ELIMINABLE_READ_TYPES.any? { |type| node.instance_of?(type) }
184
146
 
185
- node.is_a?(::Prism::ParenthesesNode) &&
186
- node.body.is_a?(::Prism::StatementsNode) &&
147
+ node.instance_of?(Prism::ParenthesesNode) &&
148
+ node.body.instance_of?(Prism::StatementsNode) &&
187
149
  node.body.body.all? { |statement| eliminable_when_discarded?(statement) }
188
150
  end
189
151
 
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
152
  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.
153
+ return true if STATIC_LITERAL_LEAF_TYPES.any? { |type| node.instance_of?(type) }
200
154
  return false if PARENS_ALWAYS_TRANSPARENT
201
155
 
202
156
  static_container?(node)
203
- # simplecov:enable
204
157
  end
205
158
 
206
- # The container dispatch, version-free: the predicate specs
207
- # exercise it directly on every supported Ruby, 3.2 included.
208
159
  def static_container?(node)
209
160
  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)
161
+ when Prism::ArrayNode then static_array_literal?(node)
162
+ when Prism::HashNode then static_hash_literal?(node)
163
+ when Prism::RangeNode then static_range_literal?(node)
213
164
  else false
214
165
  end
215
166
  end
@@ -220,7 +171,7 @@ module SimpleCov
220
171
 
221
172
  def static_hash_literal?(node)
222
173
  node.elements.all? do |element|
223
- element.is_a?(::Prism::AssocNode) &&
174
+ element.instance_of?(Prism::AssocNode) &&
224
175
  container_contents_eliminable?(element.key) && container_contents_eliminable?(element.value)
225
176
  end
226
177
  end
@@ -230,9 +181,7 @@ module SimpleCov
230
181
  end
231
182
 
232
183
  def container_contents_eliminable?(node)
233
- # simplecov:disable branch — which arm runs is fixed by the running Ruby's version
234
184
  CONTAINER_CONTENTS_NEED_STATIC_LITERALS ? static_container_literal?(node) : eliminable_when_discarded?(node)
235
- # simplecov:enable branch
236
185
  end
237
186
  end
238
187
  end