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,11 +2,10 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Builds the `SourceFile::Branch` objects for a source file from
6
- # the raw branch data Ruby's Coverage library reports. Applies the
7
- # `ignore_branches :eval_generated` / `:implicit_else` filters and
8
- # marks branches inside `# simplecov:disable` / `# :nocov:` chunks
9
- # as skipped.
5
+ # Builds the `SourceFile::Branch` objects for a source file from the raw
6
+ # branch data Ruby's Coverage library reports, applying the
7
+ # `ignore_branches` filters and marking branches inside disabled chunks as
8
+ # skipped.
10
9
  class BranchBuilder
11
10
  def initialize(source_file)
12
11
  @source_file = source_file
@@ -23,17 +22,13 @@ module SimpleCov
23
22
  process_skipped(branches)
24
23
  end
25
24
 
26
- private
27
-
28
- # Detect a Coverage-reported branch condition that originates from
29
- # `eval`/`module_eval`/`class_eval`/`instance_eval` rather than from
30
- # the file's literal source. Coverage attributes such branches to the
31
- # caller's `__FILE__`/`__LINE__`, so a Rails `delegate :foo, to: :bar`
32
- # call surfaces inside the source file as if there were branches at
33
- # the `delegate` line. Prism never sees those branches in the static
34
- # source, so a condition whose start_line isn't in the real-source
35
- # branch set must be eval-generated. Only consulted when the user has
36
- # opted in via `SimpleCov.ignore_branches :eval_generated`. See #1046.
25
+ private
26
+
27
+ # Coverage attributes an eval'd branch to the caller's `__FILE__` /
28
+ # `__LINE__`, so a Rails `delegate :foo, to: :bar` call surfaces inside the
29
+ # source file as if there were branches at the `delegate` line. Prism never
30
+ # sees those branches in the static source, so a condition whose start_line
31
+ # isn't in the real-source branch set must be eval-generated (#1046).
37
32
  def eval_generated_condition_to_ignore?(condition)
38
33
  return false unless SimpleCov.ignored_branch?(:eval_generated)
39
34
 
@@ -44,15 +39,10 @@ module SimpleCov
44
39
  # simplecov:enable branch
45
40
 
46
41
  _type, _id, start_line, * = RubyDataParser.call(condition)
47
- !positions[:branches].include?(start_line)
42
+ !positions.fetch(:branches).include?(start_line)
48
43
  end
49
44
 
50
45
  def build_branches_from(condition, branches)
51
- # the format handed in from the coverage data is like this:
52
- #
53
- # [:then, 4, 6, 6, 6, 10]
54
- #
55
- # which is [type, id, start_line, start_col, end_line, end_col]
56
46
  _condition_type, _condition_id, *condition_range = RubyDataParser.call(condition)
57
47
 
58
48
  branches.filter_map do |branch_data, hit_count|
@@ -60,54 +50,45 @@ module SimpleCov
60
50
  end
61
51
  end
62
52
 
53
+ # The coverage data hands in `[:then, 4, 6, 6, 6, 10]`, which is
54
+ # [type, id, start_line, start_col, end_line, end_col].
63
55
  def build_branch(branch_data, hit_count, condition_range)
64
56
  type, _id, start_line, start_col, end_line, end_col = branch_data
65
57
  return nil if implicit_else_to_ignore?(type, [start_line, start_col, end_line, end_col], condition_range)
66
58
 
67
- SourceFile::Branch.new(
59
+ Branch.new(
68
60
  start_line: start_line,
69
61
  end_line: end_line,
70
62
  coverage: hit_count,
71
- inline: start_line == condition_range.first,
63
+ inline: start_line.eql?(condition_range.first),
72
64
  type: type
73
65
  )
74
66
  end
75
67
 
76
- # Detect synthetic `:else` branches that Ruby's Coverage library reports
77
- # for constructs with no literal `else` keyword in source (`case/in` /
78
- # `case/when` without else, `||=`, `&&=`, `if`/`unless` without else,
79
- # and the postfix `return if cond` shape). The signal is structural:
80
- # a synthetic else reuses its parent condition's *full source range*
81
- # (start_line, start_col, end_line, end_col all identical), while an
82
- # explicit `else` arm carries a narrower range — its own keyword/body
83
- # position rather than the whole conditional. Comparing the full range
84
- # (not just `start_line`) is what distinguishes a ternary's explicit
85
- # else on the same line as the condition — `arg == 42 ? :yes : :no`,
86
- # where the else's columns differ from the parent's — from a postfix
87
- # `return if cond` where the synthetic else inherits the full range.
88
- # Only consulted when the user has opted in via
89
- # `SimpleCov.ignore_branches :implicit_else`. See #1033.
68
+ # Ruby's Coverage reports a synthetic `:else` for constructs with no literal
69
+ # `else` keyword. The signal is structural: a synthetic else reuses its
70
+ # parent condition's full source range, while an explicit `else` arm carries
71
+ # a narrower one. Comparing the full range rather than just `start_line` is
72
+ # what distinguishes a ternary's explicit else on the condition's own line
73
+ # from a postfix `return if cond` (#1033).
90
74
  def implicit_else_to_ignore?(type, branch_range, condition_range)
91
- return false unless type == :else
75
+ return false unless type.equal?(:else)
92
76
  return false unless SimpleCov.ignored_branch?(:implicit_else)
93
77
 
94
- branch_range == condition_range
78
+ branch_range.eql?(condition_range)
95
79
  end
96
80
 
81
+ # A non-inline branch's source range starts on its arm body, but
82
+ # `report_line` is the condition line above it, which is where the user sees
83
+ # the branch and would naturally place an inline disable directive. Honour
84
+ # both.
97
85
  def process_skipped(branches)
98
86
  chunks = @source_file.skip_chunks_for(:branch)
99
- return branches if chunks.empty?
100
87
 
101
- # A non-inline branch's source range starts on its arm body (e.g. the
102
- # `:yes` line of `if cond / :yes / else / :no / end`), but `report_line`
103
- # is the condition line above it — that's where the user sees the
104
- # branch in the report and where they would naturally place an inline
105
88
  # `# simplecov:disable branch` directive. Honour both.
106
89
  branches.each do |branch|
107
90
  branch.skipped! if chunks.any? { |chunk| branch.overlaps_with?(chunk) || chunk.include?(branch.report_line) }
108
91
  end
109
-
110
- branches
111
92
  end
112
93
  end
113
94
  end
@@ -2,24 +2,17 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Helpers shared by the per-criterion builders (LineBuilder /
6
- # BranchBuilder / MethodBuilder). Mixed into SourceFile so each
7
- # builder can ask the file for its skip-chunk ranges and Prism-derived
8
- # real source positions without duplicating the memoization.
9
5
  module BuilderContext
10
- # Skip-chunk lookup for the named criterion (`:line`, `:branch`,
11
- # `:method`).
12
6
  def skip_chunks_for(criterion)
13
- (@skip_chunks ||= SkipChunks.new(filename, src)).for(criterion)
7
+ (@skip_chunks ||= SkipChunks.new(filename, src)).chunks_for(criterion)
14
8
  end
15
9
 
16
- # Memoized set of real source positions (branch start lines, method
17
- # name+line pairs) extracted via Prism. Returns nil when Prism is
18
- # unavailable or parsing fails, signaling callers to keep every
19
- # Coverage entry (no false drops). The `defined?` guard preserves a
20
- # nil memoization across calls.
10
+ # Memoized set of real source positions extracted via Prism. Nil when Prism is
11
+ # unavailable or parsing fails, signaling callers to keep every Coverage entry
12
+ # rather than risk false drops. The `defined?` guard preserves a nil
13
+ # memoization across calls.
21
14
  def real_source_positions
22
- return @real_source_positions if defined?(@real_source_positions)
15
+ return @real_source_positions if instance_variable_defined?(:@real_source_positions)
23
16
 
24
17
  @real_source_positions = StaticCoverageExtractor.real_source_positions(src.join)
25
18
  end
@@ -2,25 +2,12 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Representation of a single line in a source file including
6
- # this specific line's source code, line_number and code coverage,
7
- # with the coverage being either nil (coverage not applicable, e.g. comment
8
- # line), 0 (line not covered) or >1 (the amount of times the line was
9
- # executed)
10
5
  class Line
11
- # The source code for this line. Aliased as :source
12
- attr_reader :src
13
- # The line number in the source file. Aliased as :line, :number
14
- attr_reader :line_number
15
- # The coverage data for this line: either nil (never), 0 (missed) or >=1 (times covered)
16
- attr_reader :coverage
17
- # Whether this line was skipped
18
- attr_reader :skipped
6
+ attr_reader :src, :line_number, :coverage, :skipped
19
7
 
20
- # Lets grab some fancy aliases, shall we?
21
- alias source src
22
- alias line line_number
23
- alias number line_number
8
+ alias_method :source, :src
9
+ alias_method :line, :line_number
10
+ alias_method :number, :line_number
24
11
 
25
12
  def initialize(src, line_number, coverage)
26
13
  raise ArgumentError, "Only String accepted for source" unless src.is_a?(String)
@@ -29,48 +16,41 @@ module SimpleCov
29
16
  raise ArgumentError, "Only Integer and nil accepted for coverage"
30
17
  end
31
18
 
32
- @src = src
19
+ @src = src
33
20
  @line_number = line_number
34
- @coverage = coverage
35
- @skipped = false
21
+ @coverage = coverage
22
+ @skipped = false
36
23
  end
37
24
 
38
- # Returns true if this is a line that should have been covered, but was not
25
+ # Asking `covered?` rather than reading the count a second time keeps the two
26
+ # answers from ever disagreeing, and `never?` ahead of it is what keeps a
27
+ # line with no coverage at all out of the missed column.
39
28
  def missed?
40
- !never? && !skipped? && coverage.to_i.zero?
29
+ !never? && !skipped? && !covered?
41
30
  end
42
31
 
43
- # Returns true if this is a line that has been covered
44
32
  def covered?
45
- !never? && !skipped? && coverage.to_i.positive?
33
+ !skipped? && coverage.to_i.positive?
46
34
  end
47
35
 
48
- # Returns true if this line is not relevant for coverage
49
36
  def never?
50
37
  !skipped? && coverage.nil?
51
38
  end
52
39
 
53
- # Flags this line as skipped
54
40
  def skipped!
55
41
  @skipped = true
56
42
  end
57
43
 
58
- # Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
59
- # # :nocov: comment lines.
60
44
  def skipped?
61
45
  skipped
62
46
  end
63
47
 
64
- # The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
65
- # as a css class in report generation
66
48
  def status
67
49
  return "skipped" if skipped?
68
50
  return "never" if never?
69
51
  return "missed" if missed?
70
52
 
71
- # simplecov:disable — defensive: covered? is the only state left after the three above
72
- "covered" if covered?
73
- # simplecov:enable
53
+ "covered"
74
54
  end
75
55
  end
76
56
  end
@@ -2,11 +2,8 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Builds the `SourceFile::Line` objects for a source file from the
6
- # raw line-coverage array. Each line carries its source text, its
7
- # 1-based line number, and the Coverage hit count (or nil for
8
- # never-counted lines). Applies `# simplecov:disable` /
9
- # `# :nocov:` block ranges via `skipped!`.
5
+ # Builds the `SourceFile::Line` objects for a source file from the raw
6
+ # line-coverage array, applying the disabled block ranges via `skipped!`.
10
7
  class LineBuilder
11
8
  def initialize(source_file)
12
9
  @source_file = source_file
@@ -18,23 +15,20 @@ module SimpleCov
18
15
  lines
19
16
  end
20
17
 
21
- private
18
+ private
22
19
 
23
- # When `:line` coverage is disabled, the Ruby Coverage module
24
- # doesn't emit "lines" data, so look up `nil` (never-counted) for
25
- # every position. The source rows are still useful — e.g. for the
26
- # HTML report's source view — even without per-line hits.
20
+ # When `:line` coverage is disabled the Coverage module emits no "lines" data,
21
+ # so every position looks up nil. The source rows are still useful for the
22
+ # HTML report's source view even without per-line hits.
27
23
  def build_lines
28
24
  line_coverage = @source_file.coverage_data["lines"] || []
29
25
  @source_file.src.map.with_index(1) do |src, i|
30
- SourceFile::Line.new(src, i, line_coverage[i - 1])
26
+ Line.new(src, i, line_coverage.at(i - 1))
31
27
  end
32
28
  end
33
29
 
34
- # The array the lines are kept in is 0-based whereas the line
35
- # numbers in the chunks are 1-based (more understandable elsewhere),
36
- # so each range needs to be shifted down by one to slice into the
37
- # `lines` array.
30
+ # The lines array is 0-based while the chunks' line numbers are 1-based, so
31
+ # each range is shifted down by one to slice into it.
38
32
  def mark_skipped(lines, chunks)
39
33
  chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
40
34
  end
@@ -2,11 +2,9 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Represents a single method detected in coverage data.
6
- # Provides helpers similar to Branch for coverage status.
7
5
  class Method
8
6
  attr_reader :source_file, :coverage, :class_name, :method_name,
9
- :start_line, :start_col, :end_line, :end_col
7
+ :start_line, :start_col, :end_line, :end_col
10
8
 
11
9
  def initialize(source_file, info, coverage)
12
10
  @source_file = source_file
@@ -25,12 +23,11 @@ module SimpleCov
25
23
  # method from method totals. Without line info there is nothing to
26
24
  # report against, so such a method stays skipped.
27
25
  def skipped?
28
- return @skipped if defined?(@skipped)
26
+ return @skipped if instance_variable_defined?(:@skipped)
29
27
 
30
28
  @skipped = lines.empty?
31
29
  end
32
30
 
33
- # Flag the method as skipped directly, without going through its lines.
34
31
  def skipped!
35
32
  @skipped = true
36
33
  end
@@ -40,10 +37,9 @@ module SimpleCov
40
37
  end
41
38
 
42
39
  def lines
43
- @lines ||= start_line && end_line ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
40
+ @lines ||= (start_line && end_line) ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
44
41
  end
45
42
 
46
- # Whether this method's source range intersects the given inclusive line range.
47
43
  def overlaps_with?(line_range)
48
44
  return false unless start_line && end_line
49
45
 
@@ -2,9 +2,6 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Builds the `SourceFile::Method` objects for a source file from
6
- # the raw method-coverage data. Applies the
7
- # `ignore_methods :eval_generated` filter and marks methods inside
8
5
  # `# simplecov:disable` / `# :nocov:` chunks as skipped.
9
6
  class MethodBuilder
10
7
  def initialize(source_file)
@@ -18,20 +15,17 @@ module SimpleCov
18
15
  info = RubyDataParser.call(info)
19
16
  next if eval_generated_to_ignore?(info)
20
17
 
21
- SourceFile::Method.new(@source_file, info, hit_count)
18
+ Method.new(@source_file, info, hit_count)
22
19
  end
23
20
 
24
21
  process_skipped(methods)
25
22
  end
26
23
 
27
- private
24
+ private
28
25
 
29
- # See `BranchBuilder#eval_generated_condition_to_ignore?` for the
30
- # rationale. Coverage reports an eval'd `def` at the eval caller's
31
- # line and name, so a method whose `(name, start_line)` is absent
32
- # from the real-source `def` set is eval-generated. Only consulted
33
- # when the user has opted in via
34
- # `SimpleCov.ignore_methods :eval_generated`. See #1046.
26
+ # Coverage reports an eval'd `def` at the eval caller's line and name, so a
27
+ # method whose `(name, start_line)` is absent from the real-source `def` set
28
+ # is eval-generated (#1046).
35
29
  def eval_generated_to_ignore?(info)
36
30
  return false unless SimpleCov.ignored_method?(:eval_generated)
37
31
 
@@ -42,18 +36,15 @@ module SimpleCov
42
36
  # simplecov:enable branch
43
37
 
44
38
  _class_name, name, start_line, * = info
45
- !positions[:methods].include?([name, start_line])
39
+ !positions.fetch(:methods).include?([name, start_line])
46
40
  end
47
41
 
48
42
  def process_skipped(methods)
49
43
  chunks = @source_file.skip_chunks_for(:method)
50
- return methods if chunks.empty?
51
44
 
52
45
  methods.each do |method|
53
46
  method.skipped! if chunks.any? { |chunk| method.overlaps_with?(chunk) }
54
47
  end
55
-
56
- methods
57
48
  end
58
49
  end
59
50
  end
@@ -4,106 +4,99 @@ require "ripper"
4
4
 
5
5
  module SimpleCov
6
6
  class SourceFile
7
- # `Coverage.result` reports condition and method keys as Ruby
8
- # arrays. When the resultset is round-tripped through JSON those
9
- # array keys become their stringified inspect form, so this parser
10
- # walks the literal back into a real Array without using `eval` (see
11
- # #801). The grammar covers symbols, strings, integers, unary minus,
12
- # and constant paths — every shape Coverage ever emits.
7
+ # `Coverage.result` reports condition and method keys as Ruby arrays. When
8
+ # the resultset is round-tripped through JSON those array keys become their
9
+ # stringified inspect form, so this parser walks the literal back into a
10
+ # real Array without using `eval` (#801). The grammar covers symbols,
11
+ # strings, integers, unary minus, and constant paths: every shape Coverage
12
+ # ever emits.
13
13
  module RubyDataParser
14
- module_function
14
+ extend self
15
15
 
16
- # Tests use the real data structures (except for integration tests)
17
- # so no need to put them through here.
18
- #
19
16
  # String parses are memoized: `Combine::BranchesCombiner` and
20
17
  # `Combine::MethodsCombiner` derive a merge identity from every key of
21
18
  # both sides on every pairwise merge, so collating N resultsets parses
22
- # each key string N-1 times and Ripper dominates the wall time of a
23
- # large collate.
24
- # Key strings repeat across folds and within report building, while the
25
- # set of unique keys is bounded by the project's branch and method
26
- # count, so a permanent cache stays small. Cached arrays are frozen
27
- # element-wise (String class names included, not just the tuple):
19
+ # each key string N-1 times, and Ripper dominates the wall time of a
20
+ # large collate. The set of unique keys is bounded by the project's
21
+ # branch and method count, so a permanent cache stays small.
22
+ #
23
+ # Cached arrays are frozen element-wise, String class names included:
28
24
  # every caller destructures without mutating, and sharing one array
29
- # across callers must stay that way. The cache key needs no such
30
- # care `Hash#[]=` dups and freezes String keys on its own.
25
+ # across callers must stay that way. The cache key needs no such care,
26
+ # since `Hash#[]=` dups and freezes String keys on its own.
31
27
  def call(structure)
32
28
  return structure if structure.is_a?(Array)
33
29
 
34
- string = structure.to_s
35
- parse_cache[string] ||= parse_array_string(string).each(&:freeze).freeze
30
+ parse_cache[structure] ||= parse_array_string(structure).each(&:freeze).freeze
36
31
  end
37
32
 
38
33
  def parse_cache
39
34
  @parse_cache ||= {} #: Hash[String, untyped]
40
35
  end
41
36
 
42
- # Parse a string like '[:if, 0, 3, 4, 3, 21]' or
37
+ # Parses a string like '[:if, 0, 3, 4, 3, 21]' or
43
38
  # '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
44
39
  def parse_array_string(str)
45
- # Try plain Ripper first; only pre-quote `#<...>` inspect segments
46
- # if the input isn't already valid Ruby (otherwise we corrupt
47
- # `"#<Class:Foo>"` strings that *are* valid Ruby literals — exactly
48
- # the shape simplecov-on-simplecov method-coverage keys take).
40
+ # Plain Ripper first: pre-quoting `#<...>` inspect segments would corrupt
41
+ # `"#<Class:Foo>"` strings that are valid Ruby literals, exactly the
42
+ # shape simplecov-on-simplecov method-coverage keys take.
49
43
  sexp = Ripper.sexp(str) || Ripper.sexp(quote_inspected_class_segments(str))
50
- # simplecov:disable defensive: Ripper.sexp returning nil from both passes requires malformed input
51
- array_node = sexp&.dig(1, 0)
52
- # simplecov:enable
53
- raise ArgumentError, "expected array literal: #{str.inspect}" unless array_node && array_node[0] == :array
44
+ # Ripper wraps what it parsed in a `:program` node holding the
45
+ # statements, and this input is supposed to be one array literal and
46
+ # nothing else. Both passes answer nil for input Ruby can't parse at
47
+ # all, so there may be no statement to unwrap.
48
+ _program, statements = sexp
49
+ first_statement, *extra = statements
50
+ array_node = Array(first_statement)
51
+ unless extra.empty? && array_node.first.equal?(:array)
52
+ raise ArgumentError, "expected array literal: #{str.inspect}"
53
+ end
54
54
 
55
- Array(array_node[1]).map { |element| parse_element(element) }
55
+ Array(array_node.fetch(1)).map { |element| parse_element(element) }
56
56
  end
57
57
 
58
+ # Each `when` already knows the node's shape, so the elements read straight
59
+ # out of it rather than through a second dispatch. A symbol's text sits
60
+ # where a string's does, one node deeper.
58
61
  def parse_element(node)
59
- case node[0]
60
- when :@int, :unary then parse_integer_node(node)
61
- when :symbol_literal, :dyna_symbol then parse_symbol_node(node)
62
- when :string_literal then unescape_ruby(string_literal_text(node[1]))
63
- when :var_ref then node.dig(1, 1) # `Foo`
64
- when :const_path_ref then "#{parse_element(node[1])}::#{node[2][1]}" # `Foo::Bar`
62
+ case node.fetch(0)
63
+ when :@int then Integer(node.fetch(1))
64
+ when :unary then -Integer(node.fetch(2).fetch(1)) # `-2`
65
+ when :symbol_literal, :dyna_symbol then literal_text(node).to_sym
66
+ when :string_literal then literal_text(node)
67
+ when :var_ref then node.fetch(1).fetch(1) # `Foo`
68
+ when :const_path_ref then const_path(node) # `Foo::Bar`
65
69
  else
66
- # simplecov:disable — defensive fallback for unexpected Ripper node shapes
67
- raise ArgumentError, "unexpected element: #{node.inspect}"
68
- # simplecov:enable
70
+ raise ArgumentError, "unexpected element: #{node}"
69
71
  end
70
72
  end
71
73
 
72
- def parse_integer_node(node)
73
- node[0] == :@int ? node[1].to_i : -node[2][1].to_i
74
- end
75
-
76
- def parse_symbol_node(node)
77
- if node[0] == :symbol_literal
78
- node.dig(1, 1, 1).to_sym
79
- else
80
- unescape_ruby(string_literal_text(node[1])).to_sym
81
- end
74
+ # A constant path's namespace is an element in its own right, so
75
+ # `Foo::Bar::Baz` comes back together one segment at a time.
76
+ def const_path(node)
77
+ "#{parse_element(node.fetch(1))}::#{node.fetch(2).fetch(1)}"
82
78
  end
83
79
 
84
- # Concatenate the text fragments of a `:string_content` node. Ripper
85
- # may emit zero, one, or many `:@tstring_content` children depending
86
- # on the literal.
87
- def string_literal_text(string_content)
88
- Array(string_content[1..]).map { |child| child[1] }.join
80
+ # Ripper may emit zero, one, or many fragments under a literal depending
81
+ # on its form, and the escapes it leaves in the source text come back out.
82
+ def literal_text(node)
83
+ unescape_ruby(node.fetch(1)[1..].map { |fragment| fragment.fetch(1) }.join)
89
84
  end
90
85
 
91
- # Undo the same backslash-prefix escapes the previous hand-rolled
92
- # parser undid: `\X` `X` for any X.
86
+ # Undoes the same backslash-prefix escapes the previous hand-rolled parser
87
+ # undid: `\X` -> `X` for any X.
93
88
  def unescape_ruby(raw)
94
- raw.gsub(/\\(.)/) { ::Regexp.last_match(1) }
89
+ raw.gsub(/\\(.)/) { Regexp.last_match(1) }
95
90
  end
96
91
 
97
- # Method coverage keys can contain inspect-format class references
98
- # like `#<Class:Foo>` or `#<Class:0x...>`, which aren't valid Ruby
99
- # syntax. Wrap them in quotes so Ripper can parse the surrounding
100
- # array literal; downstream we treat them as opaque strings. The
101
- # pattern recurses because singleton methods on instances nest one
102
- # inspect segment inside another (`#<Class:#<Object:0x...>>`), and
103
- # stopping at the first `>` leaves a dangling `>` that fails both
104
- # Ripper passes and crashes the merge.
92
+ # Method coverage keys can contain inspect-format class references like
93
+ # `#<Class:Foo>`, which aren't valid Ruby syntax. Wrapping them in quotes
94
+ # lets Ripper parse the surrounding array literal. The pattern recurses
95
+ # because singleton methods on instances nest one inspect segment inside
96
+ # another (`#<Class:#<Object:0x...>>`), and stopping at the first `>`
97
+ # leaves a dangling `>` that fails both Ripper passes.
105
98
  def quote_inspected_class_segments(str)
106
- str.gsub(/#<(?:[^<>]|\g<0>)*>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
99
+ str.gsub(/#<(?:[^<>]|\g<0>)+>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
107
100
  end
108
101
  end
109
102
  end
@@ -2,15 +2,9 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- # Computes the set of line ranges that should be excluded from a
6
- # SourceFile's coverage for each criterion. Two sources contribute:
7
- #
8
- # * The deprecated `# :nocov:` block toggle (lines wrapped between
9
- # even-numbered pairs of nocov markers are excluded from line and
10
- # branch coverage).
11
- # * `# simplecov:disable` / `# simplecov:enable` block directives,
12
- # which can be scoped per-criterion (`# simplecov:disable branch`,
13
- # etc.) — see `SimpleCov::Directive`.
5
+ # The line ranges excluded from a SourceFile's coverage per criterion. Two
6
+ # sources contribute: the deprecated nocov block toggle, and the
7
+ # `SimpleCov::Directive` block directives, which can be scoped per criterion.
14
8
  class SkipChunks
15
9
  @nocov_warned = Set.new
16
10
  class << self
@@ -22,16 +16,15 @@ module SimpleCov
22
16
  @src = src
23
17
  end
24
18
 
25
- # Every criterion honors the deprecated all-criteria `# :nocov:`
26
- # chunks plus its own per-criterion directive ranges. Methods
27
- # included: they carry a source range, and nocov has always meant
28
- # "exclude everything here".
29
- def for(criterion)
19
+ # Every criterion honors the deprecated all-criteria nocov chunks plus its
20
+ # own per-criterion directive ranges, methods included: they carry a
21
+ # source range, and nocov has always meant "exclude everything here". The
22
+ # ranges are 1-based line numbers, so consumers subtract 1 to index into
23
+ # the zero-based lines array.
24
+ def chunks_for(criterion)
30
25
  nocov_chunks + directive_chunks.fetch(criterion)
31
26
  end
32
27
 
33
- # Ranges of 1-based line numbers (see `with_index(1)` below);
34
- # consumers subtract 1 to index into the zero-based lines array.
35
28
  def nocov_chunks
36
29
  @nocov_chunks ||= build_nocov_chunks
37
30
  end
@@ -40,25 +33,23 @@ module SimpleCov
40
33
  @directive_chunks ||= Directive.disabled_ranges(@src)
41
34
  end
42
35
 
43
- private
36
+ private
44
37
 
38
+ # An uneven number of nocovs is assumed to run to the end of the file. It
39
+ # cannot be handled inside the each_slice because JRuby behaves differently
40
+ # there (jruby/jruby#6048).
45
41
  def build_nocov_chunks
46
- no_cov_lines = @src.map.with_index(1).select { |line_src, _index| LinesClassifier.no_cov_line?(line_src) }
42
+ no_cov_lines = @src.filter_map.with_index(1) do |line_src, index|
43
+ index if LinesClassifier.no_cov_line?(line_src)
44
+ end
47
45
 
48
- warn_nocov_deprecation(no_cov_lines.first.last) if no_cov_lines.any?
46
+ warn_nocov_deprecation(no_cov_lines.first) if no_cov_lines.any?
49
47
 
50
- # If we have an uneven number of nocovs we assume they go to the
51
- # end of the file, the source doesn't really matter. Can't deal
52
- # with this within the each_slice due to differing behavior in
53
- # JRuby: jruby/jruby#6048
54
- no_cov_lines << ["", @src.size] if no_cov_lines.size.odd?
48
+ no_cov_lines << @src.size if no_cov_lines.size.odd?
55
49
 
56
- no_cov_lines.each_slice(2).map do |(_line_src_start, index_start), (_line_src_end, index_end)|
57
- index_start..index_end
58
- end
50
+ no_cov_lines.each_slice(2).map { |chunk_start, chunk_end| chunk_start..chunk_end }
59
51
  end
60
52
 
61
- # Emit a one-time-per-file deprecation warning pointing the user
62
53
  # at the `# simplecov:disable` / `# simplecov:enable` replacement.
63
54
  def warn_nocov_deprecation(first_line_number)
64
55
  return unless self.class.nocov_warned.add?(@filename)