simplecov 0.22.0 → 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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +173 -875
  4. data/exe/simplecov +8 -0
  5. data/lib/minitest/simplecov_plugin.rb +18 -7
  6. data/lib/simplecov/atomic_file.rb +67 -0
  7. data/lib/simplecov/autostart.rb +9 -0
  8. data/lib/simplecov/baseline/parser.rb +58 -0
  9. data/lib/simplecov/baseline.rb +170 -0
  10. data/lib/simplecov/cli/affected/changed_files.rb +57 -0
  11. data/lib/simplecov/cli/affected/selection.rb +117 -0
  12. data/lib/simplecov/cli/affected.rb +148 -0
  13. data/lib/simplecov/cli/annotations.rb +166 -0
  14. data/lib/simplecov/cli/badge/svg.rb +70 -0
  15. data/lib/simplecov/cli/badge.rb +62 -0
  16. data/lib/simplecov/cli/clean.rb +86 -0
  17. data/lib/simplecov/cli/command_helpers.rb +91 -0
  18. data/lib/simplecov/cli/completions/scripts.rb +114 -0
  19. data/lib/simplecov/cli/completions.rb +83 -0
  20. data/lib/simplecov/cli/coverage.rb +83 -0
  21. data/lib/simplecov/cli/coverage_file.rb +109 -0
  22. data/lib/simplecov/cli/dead_code/output.rb +100 -0
  23. data/lib/simplecov/cli/dead_code.rb +139 -0
  24. data/lib/simplecov/cli/diff/output.rb +45 -0
  25. data/lib/simplecov/cli/diff.rb +115 -0
  26. data/lib/simplecov/cli/dotfile.rb +156 -0
  27. data/lib/simplecov/cli/git.rb +41 -0
  28. data/lib/simplecov/cli/history/output.rb +135 -0
  29. data/lib/simplecov/cli/history.rb +67 -0
  30. data/lib/simplecov/cli/merge.rb +115 -0
  31. data/lib/simplecov/cli/open.rb +45 -0
  32. data/lib/simplecov/cli/patch/changed_lines.rb +149 -0
  33. data/lib/simplecov/cli/patch/output.rb +151 -0
  34. data/lib/simplecov/cli/patch.rb +175 -0
  35. data/lib/simplecov/cli/ratchet/output.rb +44 -0
  36. data/lib/simplecov/cli/ratchet.rb +83 -0
  37. data/lib/simplecov/cli/report.rb +91 -0
  38. data/lib/simplecov/cli/run.rb +35 -0
  39. data/lib/simplecov/cli/serve/report_preparer.rb +30 -0
  40. data/lib/simplecov/cli/serve/static_file_handler.rb +128 -0
  41. data/lib/simplecov/cli/serve.rb +88 -0
  42. data/lib/simplecov/cli/show/annotator.rb +87 -0
  43. data/lib/simplecov/cli/show/sweep.rb +36 -0
  44. data/lib/simplecov/cli/show.rb +133 -0
  45. data/lib/simplecov/cli/status/facts.rb +87 -0
  46. data/lib/simplecov/cli/status.rb +93 -0
  47. data/lib/simplecov/cli/tests/redundancy.rb +78 -0
  48. data/lib/simplecov/cli/tests.rb +148 -0
  49. data/lib/simplecov/cli/uncovered/misses.rb +33 -0
  50. data/lib/simplecov/cli/uncovered.rb +127 -0
  51. data/lib/simplecov/cli/usage.rb +153 -0
  52. data/lib/simplecov/cli/watch/live_report.rb +73 -0
  53. data/lib/simplecov/cli/watch/narrator.rb +55 -0
  54. data/lib/simplecov/cli/watch/poller.rb +45 -0
  55. data/lib/simplecov/cli/watch/session.rb +134 -0
  56. data/lib/simplecov/cli/watch/test_plan.rb +43 -0
  57. data/lib/simplecov/cli/watch.rb +85 -0
  58. data/lib/simplecov/cli.rb +114 -0
  59. data/lib/simplecov/color.rb +65 -0
  60. data/lib/simplecov/combine/branches_combiner.rb +44 -21
  61. data/lib/simplecov/combine/coverage_accumulator.rb +175 -0
  62. data/lib/simplecov/combine/identity_interner.rb +27 -0
  63. data/lib/simplecov/combine/interned_counts.rb +28 -0
  64. data/lib/simplecov/combine/lines_combiner.rb +35 -29
  65. data/lib/simplecov/combine/methods_combiner.rb +48 -0
  66. data/lib/simplecov/combine/results_combiner.rb +9 -49
  67. data/lib/simplecov/combine.rb +0 -24
  68. data/lib/simplecov/command_guesser.rb +99 -42
  69. data/lib/simplecov/configuration/baseline.rb +31 -0
  70. data/lib/simplecov/configuration/coverage.rb +184 -0
  71. data/lib/simplecov/configuration/coverage_criteria.rb +143 -0
  72. data/lib/simplecov/configuration/deprecations.rb +21 -0
  73. data/lib/simplecov/configuration/eval_coverage.rb +37 -0
  74. data/lib/simplecov/configuration/filters.rb +131 -0
  75. data/lib/simplecov/configuration/formatting.rb +129 -0
  76. data/lib/simplecov/configuration/groups.rb +48 -0
  77. data/lib/simplecov/configuration/history.rb +44 -0
  78. data/lib/simplecov/configuration/ignored_entries.rb +72 -0
  79. data/lib/simplecov/configuration/merging.rb +197 -0
  80. data/lib/simplecov/configuration/missed_caps.rb +51 -0
  81. data/lib/simplecov/configuration/production.rb +23 -0
  82. data/lib/simplecov/configuration/test_tracking.rb +48 -0
  83. data/lib/simplecov/configuration/thresholds.rb +150 -0
  84. data/lib/simplecov/configuration/view_coverage.rb +37 -0
  85. data/lib/simplecov/configuration.rb +100 -443
  86. data/lib/simplecov/configuration_error.rb +5 -0
  87. data/lib/simplecov/context_map/union.rb +81 -0
  88. data/lib/simplecov/context_map.rb +187 -0
  89. data/lib/simplecov/coverage_json.rb +21 -0
  90. data/lib/simplecov/coverage_statistics.rb +24 -28
  91. data/lib/simplecov/coverage_violations.rb +254 -0
  92. data/lib/simplecov/current_run.rb +61 -0
  93. data/lib/simplecov/defaults.rb +20 -28
  94. data/lib/simplecov/deprecation.rb +63 -0
  95. data/lib/simplecov/directive/erb.rb +44 -0
  96. data/lib/simplecov/directive/haml.rb +33 -0
  97. data/lib/simplecov/directive/indented_template.rb +37 -0
  98. data/lib/simplecov/directive/slim.rb +35 -0
  99. data/lib/simplecov/directive/template.rb +29 -0
  100. data/lib/simplecov/directive.rb +152 -0
  101. data/lib/simplecov/exit_codes/baseline_check.rb +46 -0
  102. data/lib/simplecov/exit_codes/check.rb +39 -0
  103. data/lib/simplecov/exit_codes/exit_code_handling.rb +24 -5
  104. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +17 -67
  105. data/lib/simplecov/exit_codes/maximum_missed_check.rb +26 -0
  106. data/lib/simplecov/exit_codes/maximum_missed_per_file_check.rb +34 -0
  107. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +27 -0
  108. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +20 -38
  109. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +28 -0
  110. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +37 -34
  111. data/lib/simplecov/exit_codes.rb +26 -0
  112. data/lib/simplecov/exit_handling.rb +163 -0
  113. data/lib/simplecov/file_list.rb +63 -48
  114. data/lib/simplecov/filter.rb +98 -42
  115. data/lib/simplecov/formatter/base.rb +100 -0
  116. data/lib/simplecov/formatter/baseline_formatter.rb +98 -0
  117. data/lib/simplecov/formatter/coverage_json_writer.rb +95 -0
  118. data/lib/simplecov/formatter/html_formatter/public/index.html +93 -0
  119. data/lib/simplecov/formatter/html_formatter/viewer_data_validator.rb +153 -0
  120. data/lib/simplecov/formatter/html_formatter.rb +88 -0
  121. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +122 -0
  122. data/lib/simplecov/formatter/json_formatter/production_section_formatter.rb +48 -0
  123. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +132 -0
  124. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +97 -0
  125. data/lib/simplecov/formatter/json_formatter.rb +40 -0
  126. data/lib/simplecov/formatter/multi_formatter.rb +8 -10
  127. data/lib/simplecov/formatter/simple_formatter.rb +12 -15
  128. data/lib/simplecov/formatter.rb +21 -1
  129. data/lib/simplecov/group_names.rb +33 -0
  130. data/lib/simplecov/history.rb +115 -0
  131. data/lib/simplecov/last_run.rb +16 -5
  132. data/lib/simplecov/lines_classifier.rb +67 -19
  133. data/lib/simplecov/load_global_config.rb +12 -4
  134. data/lib/simplecov/parallel_adapters/base.rb +50 -0
  135. data/lib/simplecov/parallel_adapters/generic.rb +33 -0
  136. data/lib/simplecov/parallel_adapters/parallel_tests.rb +72 -0
  137. data/lib/simplecov/parallel_adapters.rb +54 -0
  138. data/lib/simplecov/parallel_coordination.rb +124 -0
  139. data/lib/simplecov/parallel_result_merger/worker_payload.rb +33 -0
  140. data/lib/simplecov/parallel_result_merger.rb +171 -0
  141. data/lib/simplecov/process.rb +34 -12
  142. data/lib/simplecov/production/error.rb +11 -0
  143. data/lib/simplecov/production/file_sink.rb +130 -0
  144. data/lib/simplecov/production.rb +249 -0
  145. data/lib/simplecov/profiles/bundler_filter.rb +1 -1
  146. data/lib/simplecov/profiles/hidden_filter.rb +1 -1
  147. data/lib/simplecov/profiles/rails.rb +13 -10
  148. data/lib/simplecov/profiles/root_filter.rb +2 -5
  149. data/lib/simplecov/profiles/strict.rb +22 -0
  150. data/lib/simplecov/profiles/test_frameworks.rb +1 -4
  151. data/lib/simplecov/profiles.rb +26 -19
  152. data/lib/simplecov/report_deferral.rb +45 -0
  153. data/lib/simplecov/report_stamp.rb +25 -0
  154. data/lib/simplecov/result/filter_config.rb +19 -0
  155. data/lib/simplecov/result/missing_source_files_reporter.rb +48 -0
  156. data/lib/simplecov/result/serialization.rb +43 -0
  157. data/lib/simplecov/result/source_file_builder.rb +46 -0
  158. data/lib/simplecov/result.rb +123 -47
  159. data/lib/simplecov/result_adapter.rb +130 -11
  160. data/lib/simplecov/result_merger/contexts.rb +22 -0
  161. data/lib/simplecov/result_merger/legacy_format_adapter.rb +24 -0
  162. data/lib/simplecov/result_merger/resultset_file.rb +67 -0
  163. data/lib/simplecov/result_merger/resultset_run_identity.rb +65 -0
  164. data/lib/simplecov/result_merger/resultset_store.rb +62 -0
  165. data/lib/simplecov/result_merger/unloaded_files.rb +95 -0
  166. data/lib/simplecov/result_merger.rb +120 -124
  167. data/lib/simplecov/result_processing.rb +188 -0
  168. data/lib/simplecov/run_identity.rb +75 -0
  169. data/lib/simplecov/simulate_coverage.rb +72 -17
  170. data/lib/simplecov/source_file/branch.rb +8 -35
  171. data/lib/simplecov/source_file/branch_builder.rb +95 -0
  172. data/lib/simplecov/source_file/builder_context.rb +28 -0
  173. data/lib/simplecov/source_file/line.rb +17 -32
  174. data/lib/simplecov/source_file/line_builder.rb +37 -0
  175. data/lib/simplecov/source_file/method.rb +54 -0
  176. data/lib/simplecov/source_file/method_builder.rb +51 -0
  177. data/lib/simplecov/source_file/ruby_data_parser.rb +103 -0
  178. data/lib/simplecov/source_file/skip_chunks.rb +72 -0
  179. data/lib/simplecov/source_file/source_loader.rb +89 -0
  180. data/lib/simplecov/source_file/statistics.rb +76 -0
  181. data/lib/simplecov/source_file.rb +64 -260
  182. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +155 -0
  183. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +191 -0
  184. data/lib/simplecov/static_coverage_extractor/method_collector.rb +45 -0
  185. data/lib/simplecov/static_coverage_extractor/prism_compat.rb +46 -0
  186. data/lib/simplecov/static_coverage_extractor/value_position.rb +61 -0
  187. data/lib/simplecov/static_coverage_extractor/visitor.rb +163 -0
  188. data/lib/simplecov/static_coverage_extractor.rb +78 -0
  189. data/lib/simplecov/test_tracker/accessors.rb +34 -0
  190. data/lib/simplecov/test_tracker/constant_watch.rb +49 -0
  191. data/lib/simplecov/test_tracker/delta.rb +69 -0
  192. data/lib/simplecov/test_tracker/framework_hooks.rb +130 -0
  193. data/lib/simplecov/test_tracker.rb +162 -0
  194. data/lib/simplecov/unloaded_file_injector.rb +62 -0
  195. data/lib/simplecov/useless_results_remover.rb +17 -7
  196. data/lib/simplecov/version.rb +1 -1
  197. data/lib/simplecov/view_coverage/template_compiler.rb +83 -0
  198. data/lib/simplecov/view_coverage.rb +55 -0
  199. data/lib/simplecov-html.rb +5 -0
  200. data/lib/simplecov.rb +143 -394
  201. data/lib/simplecov_json_formatter.rb +5 -0
  202. data/man/simplecov.1 +354 -0
  203. data/sig/simplecov.rbs +1710 -0
  204. metadata +183 -61
  205. data/CHANGELOG.md +0 -191
  206. data/doc/alternate-formatters.md +0 -71
  207. data/doc/commercial-services.md +0 -25
  208. data/doc/editor-integration.md +0 -18
  209. data/lib/simplecov/combine/files_combiner.rb +0 -24
  210. data/lib/simplecov/default_formatter.rb +0 -20
@@ -2,53 +2,33 @@
2
2
 
3
3
  module SimpleCov
4
4
  class SourceFile
5
- #
6
- # Representing single branch that has been detected in coverage report.
7
- # Give us support methods that handle needed calculations.
8
5
  class Branch
9
6
  attr_reader :start_line, :end_line, :coverage, :type
10
7
 
11
- # rubocop:disable Metrics/ParameterLists
12
8
  def initialize(start_line:, end_line:, coverage:, inline:, type:)
13
9
  @start_line = start_line
14
- @end_line = end_line
15
- @coverage = coverage
16
- @inline = inline
17
- @type = type
18
- @skipped = false
10
+ @end_line = end_line
11
+ @coverage = coverage
12
+ @inline = inline
13
+ @type = type
14
+ @skipped = false
19
15
  end
20
- # rubocop:enable Metrics/ParameterLists
21
16
 
22
17
  def inline?
23
18
  @inline
24
19
  end
25
20
 
26
- #
27
- # Return true if there is relevant count defined > 0
28
- #
29
- # @return [Boolean]
30
- #
31
21
  def covered?
32
22
  !skipped? && coverage.positive?
33
23
  end
34
24
 
35
- #
36
- # Check if branche missed or not
37
- #
38
- # @return [Boolean]
39
- #
40
25
  def missed?
41
26
  !skipped? && coverage.zero?
42
27
  end
43
28
 
44
- # The line on which we want to report the coverage
45
- #
46
- # Usually we choose the line above the start of the branch (so that it shows up
47
- # at if/else) because that
48
- # * highlights the condition
49
- # * makes it distinguishable if the first line of the branch is an inline branch
50
- # (see the nested_branches fixture)
51
- #
29
+ # Usually the line above the start of the branch, so that it shows up at the
30
+ # if/else. That highlights the condition, and makes it distinguishable when
31
+ # the first line of the branch is itself an inline branch.
52
32
  def report_line
53
33
  if inline?
54
34
  start_line
@@ -57,12 +37,10 @@ module SimpleCov
57
37
  end
58
38
  end
59
39
 
60
- # Flags the branch as skipped
61
40
  def skipped!
62
41
  @skipped = true
63
42
  end
64
43
 
65
- # Returns true if the branch was marked skipped by virtue of nocov comments.
66
44
  def skipped?
67
45
  @skipped
68
46
  end
@@ -71,11 +49,6 @@ module SimpleCov
71
49
  start_line <= line_range.end && end_line >= line_range.begin
72
50
  end
73
51
 
74
- #
75
- # Return array with coverage count and badge
76
- #
77
- # @return [Array]
78
- #
79
52
  def report
80
53
  [type, coverage]
81
54
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
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.
9
+ class BranchBuilder
10
+ def initialize(source_file)
11
+ @source_file = source_file
12
+ end
13
+
14
+ def call
15
+ coverage_branch_data = @source_file.coverage_data["branches"] || {}
16
+ branches = coverage_branch_data.flat_map do |condition, coverage_branches|
17
+ next [] if eval_generated_condition_to_ignore?(condition)
18
+
19
+ build_branches_from(condition, coverage_branches)
20
+ end
21
+
22
+ process_skipped(branches)
23
+ end
24
+
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).
32
+ def eval_generated_condition_to_ignore?(condition)
33
+ return false unless SimpleCov.ignored_branch?(:eval_generated)
34
+
35
+ positions = @source_file.real_source_positions
36
+ # simplecov:disable branch — nil branch fires only when Prism is unavailable
37
+ return false unless positions
38
+
39
+ # simplecov:enable branch
40
+
41
+ _type, _id, start_line, * = RubyDataParser.call(condition)
42
+ !positions.fetch(:branches).include?(start_line)
43
+ end
44
+
45
+ def build_branches_from(condition, branches)
46
+ _condition_type, _condition_id, *condition_range = RubyDataParser.call(condition)
47
+
48
+ branches.filter_map do |branch_data, hit_count|
49
+ build_branch(RubyDataParser.call(branch_data), hit_count, condition_range)
50
+ end
51
+ end
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].
55
+ def build_branch(branch_data, hit_count, condition_range)
56
+ type, _id, start_line, start_col, end_line, end_col = branch_data
57
+ return nil if implicit_else_to_ignore?(type, [start_line, start_col, end_line, end_col], condition_range)
58
+
59
+ Branch.new(
60
+ start_line: start_line,
61
+ end_line: end_line,
62
+ coverage: hit_count,
63
+ inline: start_line.eql?(condition_range.first),
64
+ type: type
65
+ )
66
+ end
67
+
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).
74
+ def implicit_else_to_ignore?(type, branch_range, condition_range)
75
+ return false unless type.equal?(:else)
76
+ return false unless SimpleCov.ignored_branch?(:implicit_else)
77
+
78
+ branch_range.eql?(condition_range)
79
+ end
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.
85
+ def process_skipped(branches)
86
+ chunks = @source_file.skip_chunks_for(:branch)
87
+
88
+ # `# simplecov:disable branch` directive. Honour both.
89
+ branches.each do |branch|
90
+ branch.skipped! if chunks.any? { |chunk| branch.overlaps_with?(chunk) || chunk.include?(branch.report_line) }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
5
+ module BuilderContext
6
+ def skip_chunks_for(criterion)
7
+ (@skip_chunks ||= SkipChunks.new(filename, src)).chunks_for(criterion)
8
+ end
9
+
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.
14
+ #
15
+ # Nil for a template too, without parsing: every entry a template reports
16
+ # is eval-generated by construction, so the filter has nothing to
17
+ # distinguish there, and a template whose text happens to parse as Ruby
18
+ # (a Haml `#id` tag reads as a comment) would otherwise lose its real
19
+ # branches to a confident wrong parse.
20
+ def real_source_positions
21
+ return @real_source_positions if instance_variable_defined?(:@real_source_positions)
22
+ return @real_source_positions = nil if Directive::Template.template?(filename)
23
+
24
+ @real_source_positions = StaticCoverageExtractor.real_source_positions(src.join)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -2,70 +2,55 @@
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)
27
14
  raise ArgumentError, "Only Integer accepted for line_number" unless line_number.is_a?(Integer)
28
- raise ArgumentError, "Only Integer and nil accepted for coverage" unless coverage.is_a?(Integer) || coverage.nil?
15
+ unless coverage.is_a?(Integer) || coverage.nil?
16
+ raise ArgumentError, "Only Integer and nil accepted for coverage"
17
+ end
29
18
 
30
- @src = src
19
+ @src = src
31
20
  @line_number = line_number
32
- @coverage = coverage
33
- @skipped = false
21
+ @coverage = coverage
22
+ @skipped = false
34
23
  end
35
24
 
36
- # 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.
37
28
  def missed?
38
- !never? && !skipped? && coverage.zero?
29
+ !never? && !skipped? && !covered?
39
30
  end
40
31
 
41
- # Returns true if this is a line that has been covered
42
32
  def covered?
43
- !never? && !skipped? && coverage.positive?
33
+ !skipped? && coverage.to_i.positive?
44
34
  end
45
35
 
46
- # Returns true if this line is not relevant for coverage
47
36
  def never?
48
37
  !skipped? && coverage.nil?
49
38
  end
50
39
 
51
- # Flags this line as skipped
52
40
  def skipped!
53
41
  @skipped = true
54
42
  end
55
43
 
56
- # Returns true if this line was skipped, false otherwise. Lines are skipped if they are wrapped with
57
- # # :nocov: comment lines.
58
44
  def skipped?
59
45
  skipped
60
46
  end
61
47
 
62
- # The status of this line - either covered, missed, skipped or never. Useful i.e. for direct use
63
- # as a css class in report generation
64
48
  def status
65
49
  return "skipped" if skipped?
66
50
  return "never" if never?
67
51
  return "missed" if missed?
68
- return "covered" if covered?
52
+
53
+ "covered"
69
54
  end
70
55
  end
71
56
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
5
+ # Builds the `SourceFile::Line` objects for a source file from the raw
6
+ # line-coverage array, applying the disabled block ranges via `skipped!`.
7
+ class LineBuilder
8
+ def initialize(source_file)
9
+ @source_file = source_file
10
+ end
11
+
12
+ def call
13
+ lines = build_lines
14
+ mark_skipped(lines, @source_file.skip_chunks_for(:line))
15
+ lines
16
+ end
17
+
18
+ private
19
+
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.
23
+ def build_lines
24
+ line_coverage = @source_file.coverage_data["lines"] || []
25
+ @source_file.src.map.with_index(1) do |src, i|
26
+ Line.new(src, i, line_coverage.at(i - 1))
27
+ end
28
+ end
29
+
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.
32
+ def mark_skipped(lines, chunks)
33
+ chunks.each { |chunk| lines[(chunk.begin - 1)..(chunk.end - 1)].each(&:skipped!) }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
5
+ class Method
6
+ attr_reader :source_file, :coverage, :class_name, :method_name,
7
+ :start_line, :start_col, :end_line, :end_col
8
+
9
+ def initialize(source_file, info, coverage)
10
+ @source_file = source_file
11
+ @class_name, @method_name, @start_line, @start_col, @end_line, @end_col = info
12
+ @coverage = coverage
13
+ end
14
+
15
+ def covered?
16
+ !skipped? && coverage.positive?
17
+ end
18
+
19
+ # Criterion-level skips (nocov chunks, `# simplecov:disable` /
20
+ # `# simplecov:disable method` regions) arrive via `skipped!` from
21
+ # MethodBuilder. Deliberately NOT derived from the lines' skip
22
+ # state: a line-only directive around a def must not remove the
23
+ # method from method totals. Without line info there is nothing to
24
+ # report against, so such a method stays skipped.
25
+ def skipped?
26
+ return @skipped if instance_variable_defined?(:@skipped)
27
+
28
+ @skipped = lines.empty?
29
+ end
30
+
31
+ def skipped!
32
+ @skipped = true
33
+ end
34
+
35
+ def missed?
36
+ !skipped? && coverage.zero?
37
+ end
38
+
39
+ def lines
40
+ @lines ||= (start_line && end_line) ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
41
+ end
42
+
43
+ def overlaps_with?(line_range)
44
+ return false unless start_line && end_line
45
+
46
+ start_line <= line_range.end && end_line >= line_range.begin
47
+ end
48
+
49
+ def to_s
50
+ "#{class_name}##{method_name}"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
5
+ # `# simplecov:disable` / `# :nocov:` chunks as skipped.
6
+ class MethodBuilder
7
+ def initialize(source_file)
8
+ @source_file = source_file
9
+ end
10
+
11
+ def call
12
+ none = {} #: Hash[untyped, Integer]
13
+ raw_methods = @source_file.coverage_data.fetch("methods", none)
14
+ methods = raw_methods.filter_map do |info, hit_count|
15
+ info = RubyDataParser.call(info)
16
+ next if eval_generated_to_ignore?(info)
17
+
18
+ Method.new(@source_file, info, hit_count)
19
+ end
20
+
21
+ process_skipped(methods)
22
+ end
23
+
24
+ private
25
+
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).
29
+ def eval_generated_to_ignore?(info)
30
+ return false unless SimpleCov.ignored_method?(:eval_generated)
31
+
32
+ positions = @source_file.real_source_positions
33
+ # simplecov:disable branch — nil branch fires only when Prism is unavailable
34
+ return false unless positions
35
+
36
+ # simplecov:enable branch
37
+
38
+ _class_name, name, start_line, * = info
39
+ !positions.fetch(:methods).include?([name, start_line])
40
+ end
41
+
42
+ def process_skipped(methods)
43
+ chunks = @source_file.skip_chunks_for(:method)
44
+
45
+ methods.each do |method|
46
+ method.skipped! if chunks.any? { |chunk| method.overlaps_with?(chunk) }
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ripper"
4
+
5
+ module SimpleCov
6
+ class SourceFile
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
+ module RubyDataParser
14
+ extend self
15
+
16
+ # String parses are memoized: `Combine::BranchesCombiner` and
17
+ # `Combine::MethodsCombiner` derive a merge identity from every key of
18
+ # both sides on every pairwise merge, so collating N resultsets parses
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:
24
+ # every caller destructures without mutating, and sharing one array
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.
27
+ def call(structure)
28
+ return structure if structure.is_a?(Array)
29
+
30
+ parse_cache[structure] ||= parse_array_string(structure).each(&:freeze).freeze
31
+ end
32
+
33
+ def parse_cache
34
+ @parse_cache ||= {} #: Hash[String, untyped]
35
+ end
36
+
37
+ # Parses a string like '[:if, 0, 3, 4, 3, 21]' or
38
+ # '["ClassName", :method1, 2, 2, 5, 5]' back into a Ruby array.
39
+ def parse_array_string(str)
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.
43
+ sexp = Ripper.sexp(str) || Ripper.sexp(quote_inspected_class_segments(str))
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
+
55
+ Array(array_node.fetch(1)).map { |element| parse_element(element) }
56
+ end
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.
61
+ def parse_element(node)
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`
69
+ else
70
+ raise ArgumentError, "unexpected element: #{node}"
71
+ end
72
+ end
73
+
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)}"
78
+ end
79
+
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)
84
+ end
85
+
86
+ # Undoes the same backslash-prefix escapes the previous hand-rolled parser
87
+ # undid: `\X` -> `X` for any X.
88
+ def unescape_ruby(raw)
89
+ raw.gsub(/\\(.)/) { Regexp.last_match(1) }
90
+ end
91
+
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.
98
+ def quote_inspected_class_segments(str)
99
+ str.gsub(/#<(?:[^<>]|\g<0>)+>/) { |segment| %("#{segment.gsub('"', '\\"')}") }
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ class SourceFile
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.
8
+ class SkipChunks
9
+ @nocov_warned = Set.new
10
+ class << self
11
+ attr_reader :nocov_warned
12
+ end
13
+
14
+ def initialize(filename, src)
15
+ @filename = filename
16
+ @src = src
17
+ end
18
+
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)
25
+ nocov_chunks + directive_chunks.fetch(criterion)
26
+ end
27
+
28
+ def nocov_chunks
29
+ @nocov_chunks ||= build_nocov_chunks
30
+ end
31
+
32
+ def directive_chunks
33
+ @directive_chunks ||= Directive.disabled_ranges(ruby_lines)
34
+ end
35
+
36
+ private
37
+
38
+ # The Ruby the file holds, which for a template means the Ruby inside its
39
+ # tags or script lines with its own comments turned into Ruby comments.
40
+ # Both kinds of marker are found there, so a toggle in a template's
41
+ # comment syntax gets the same treatment, and the same deprecation
42
+ # warning, as one in a Ruby file.
43
+ def ruby_lines
44
+ @ruby_lines ||= Directive::Template.ruby_lines(@filename, @src)
45
+ end
46
+
47
+ # An uneven number of nocovs is assumed to run to the end of the file. It
48
+ # cannot be handled inside the each_slice because JRuby behaves differently
49
+ # there (jruby/jruby#6048).
50
+ def build_nocov_chunks
51
+ no_cov_lines = ruby_lines.filter_map.with_index(1) do |line_src, index|
52
+ index if LinesClassifier.no_cov_line?(line_src)
53
+ end
54
+
55
+ warn_nocov_deprecation(no_cov_lines.first) if no_cov_lines.any?
56
+
57
+ no_cov_lines << @src.size if no_cov_lines.size.odd?
58
+
59
+ no_cov_lines.each_slice(2).map { |chunk_start, chunk_end| chunk_start..chunk_end }
60
+ end
61
+
62
+ # at the `# simplecov:disable` / `# simplecov:enable` replacement.
63
+ def warn_nocov_deprecation(first_line_number)
64
+ return unless self.class.nocov_warned.add?(@filename)
65
+
66
+ token = SimpleCov.current_nocov_token
67
+ warn "#{@filename}:#{first_line_number}: [DEPRECATION] `# :#{token}:` is deprecated and will be removed " \
68
+ "in a future release. Replace with `# simplecov:disable` / `# simplecov:enable` block comments."
69
+ end
70
+ end
71
+ end
72
+ end