simplecov 1.0.0 → 1.0.2

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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/lib/simplecov/cli/coverage.rb +5 -3
  3. data/lib/simplecov/cli/diff.rb +2 -1
  4. data/lib/simplecov/cli/dotfile.rb +9 -4
  5. data/lib/simplecov/cli/merge.rb +5 -3
  6. data/lib/simplecov/cli/report.rb +11 -6
  7. data/lib/simplecov/cli/run.rb +1 -1
  8. data/lib/simplecov/cli/serve.rb +17 -7
  9. data/lib/simplecov/cli/uncovered.rb +2 -1
  10. data/lib/simplecov/color.rb +6 -1
  11. data/lib/simplecov/combine/branches_combiner.rb +2 -1
  12. data/lib/simplecov/combine/files_combiner.rb +42 -2
  13. data/lib/simplecov/combine/methods_combiner.rb +27 -4
  14. data/lib/simplecov/combine/results_combiner.rb +2 -1
  15. data/lib/simplecov/configuration/coverage.rb +6 -2
  16. data/lib/simplecov/configuration/coverage_criteria.rb +3 -1
  17. data/lib/simplecov/configuration/formatting.rb +17 -7
  18. data/lib/simplecov/configuration/thresholds.rb +13 -9
  19. data/lib/simplecov/configuration.rb +22 -13
  20. data/lib/simplecov/coverage_statistics.rb +4 -1
  21. data/lib/simplecov/coverage_violations.rb +2 -1
  22. data/lib/simplecov/defaults.rb +3 -3
  23. data/lib/simplecov/directive.rb +8 -3
  24. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +1 -1
  25. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +1 -1
  26. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +1 -1
  27. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +1 -1
  28. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +3 -3
  29. data/lib/simplecov/exit_codes.rb +12 -0
  30. data/lib/simplecov/exit_handling.rb +3 -3
  31. data/lib/simplecov/file_list.rb +12 -5
  32. data/lib/simplecov/formatter/base.rb +2 -1
  33. data/lib/simplecov/formatter/html_formatter.rb +13 -7
  34. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +9 -3
  35. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +1 -1
  36. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +1 -1
  37. data/lib/simplecov/formatter/json_formatter.rb +4 -2
  38. data/lib/simplecov/formatter/multi_formatter.rb +7 -2
  39. data/lib/simplecov/formatter/simple_formatter.rb +3 -1
  40. data/lib/simplecov/process.rb +22 -0
  41. data/lib/simplecov/result_adapter.rb +45 -10
  42. data/lib/simplecov/result_processing.rb +4 -4
  43. data/lib/simplecov/simulate_coverage.rb +2 -2
  44. data/lib/simplecov/source_file/line.rb +2 -2
  45. data/lib/simplecov/source_file/method.rb +1 -1
  46. data/lib/simplecov/source_file/method_builder.rb +3 -1
  47. data/lib/simplecov/source_file/source_loader.rb +2 -2
  48. data/lib/simplecov/source_file.rb +2 -2
  49. data/lib/simplecov/static_coverage_extractor/condition_folding.rb +49 -0
  50. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +247 -0
  51. data/lib/simplecov/static_coverage_extractor/value_position.rb +82 -0
  52. data/lib/simplecov/static_coverage_extractor/visitor.rb +71 -46
  53. data/lib/simplecov/version.rb +1 -1
  54. data/lib/simplecov.rb +24 -11
  55. data/sig/simplecov.rbs +1638 -0
  56. metadata +7 -3
@@ -15,7 +15,7 @@ module SimpleCov
15
15
  end
16
16
 
17
17
  def report
18
- violations.each { |violation| warn SimpleCov::Color.colorize(message_for(violation), :red) }
18
+ violations.each { |violation| ExitCodes.print_error SimpleCov::Color.colorize(message_for(violation), :red) }
19
19
  end
20
20
 
21
21
  def exit_code
@@ -32,7 +32,7 @@ module SimpleCov
32
32
  end
33
33
 
34
34
  def report_violation(violation)
35
- warn format(
35
+ ExitCodes.print_error format(
36
36
  "%<criterion>s coverage (%<actual>s) is above the expected maximum coverage (%<expected>.2f%%). " \
37
37
  "Time to bump the threshold!",
38
38
  criterion: violation.fetch(:criterion).capitalize,
@@ -17,7 +17,7 @@ module SimpleCov
17
17
 
18
18
  def report
19
19
  violations.each do |violation|
20
- warn format(
20
+ ExitCodes.print_error format(
21
21
  "%<criterion>s coverage by file (%<actual>s) is below the expected minimum coverage " \
22
22
  "(%<expected>.2f%%) in %<filename>s.",
23
23
  criterion: violation.fetch(:criterion).capitalize,
@@ -16,7 +16,7 @@ module SimpleCov
16
16
 
17
17
  def report
18
18
  violations.each do |violation|
19
- warn format(
19
+ ExitCodes.print_error format(
20
20
  "%<criterion>s coverage by group (%<actual>s) is below the expected minimum coverage " \
21
21
  "(%<expected>.2f%%) in %<group_name>s.",
22
22
  criterion: violation.fetch(:criterion).capitalize,
@@ -34,7 +34,7 @@ module SimpleCov
34
34
  def report_violation(violation)
35
35
  criterion = violation.fetch(:criterion)
36
36
  actual = violation.fetch(:actual)
37
- warn format(
37
+ ExitCodes.print_error format(
38
38
  "%<criterion>s coverage (%<actual>s) is below the expected minimum coverage (%<expected>.2f%%).",
39
39
  criterion: criterion.capitalize,
40
40
  actual: SimpleCov::Color.colorize_percent(actual),
@@ -47,9 +47,9 @@ module SimpleCov
47
47
  worst = worst_files_for(criterion)
48
48
  return if worst.empty?
49
49
 
50
- warn " Lowest-coverage files (#{criterion}):"
50
+ ExitCodes.print_error " Lowest-coverage files (#{criterion}):"
51
51
  worst.each do |path, percent|
52
- warn format(
52
+ ExitCodes.print_error format(
53
53
  " %<percent>s %<path>s",
54
54
  percent: SimpleCov::Color.colorize_percent(percent, format("%6.2f%%", percent)),
55
55
  path: path
@@ -1,12 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
+ # Exit statuses SimpleCov sets when coverage checks fail, and the output
5
+ # helper the enforcement machinery reports through.
4
6
  module ExitCodes
5
7
  SUCCESS = 0
6
8
  EXCEPTION = 1
7
9
  MINIMUM_COVERAGE = 2
8
10
  MAXIMUM_COVERAGE_DROP = 3
9
11
  MAXIMUM_COVERAGE = 4
12
+
13
+ # Threshold-violation reports and exit-status notices are the output of
14
+ # the enforcement feature, not Ruby warnings: routing them through
15
+ # `Kernel#warn` made `-W0` swallow the explanation for a failing exit
16
+ # code, let `Warning.warn` hooks (warning trackers, raise-on-warning
17
+ # test setups) intercept them mid-`at_exit`, and fed colorized text to
18
+ # warning logs. `print_errors false` remains the intended opt-out.
19
+ def self.print_error(message)
20
+ $stderr.puts message # rubocop:disable Style/StderrPuts
21
+ end
10
22
  end
11
23
  end
12
24
 
@@ -56,7 +56,7 @@ module SimpleCov
56
56
  def warn_about_deferred_report
57
57
  return unless print_errors
58
58
 
59
- warn SimpleCov::Color.colorize(
59
+ ExitCodes.print_error SimpleCov::Color.colorize(
60
60
  "Skipping SimpleCov report — this process tracked no application code and a newer " \
61
61
  "report already exists at #{coverage_path}. This usually means SimpleCov.start ran in a " \
62
62
  "parent process (e.g. a Rakefile or Rails' Bundler.require) that shelled out to the test " \
@@ -97,7 +97,7 @@ module SimpleCov
97
97
  # @api private
98
98
  def exit_and_report_previous_error(exit_status)
99
99
  if print_errors
100
- warn SimpleCov::Color.colorize(
100
+ ExitCodes.print_error SimpleCov::Color.colorize(
101
101
  "Stopped processing SimpleCov as a previous error not related to SimpleCov has been detected",
102
102
  :yellow
103
103
  )
@@ -124,7 +124,7 @@ module SimpleCov
124
124
  return unless exit_status.positive?
125
125
 
126
126
  if print_errors
127
- warn SimpleCov::Color.colorize(
127
+ ExitCodes.print_error SimpleCov::Color.colorize(
128
128
  "SimpleCov failed with exit #{exit_status} due to a coverage related error", :red
129
129
  )
130
130
  end
@@ -27,8 +27,8 @@ module SimpleCov
27
27
  # returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
28
28
  # (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
29
29
  def coverage_statistics(criterion = nil)
30
- @coverage_statistics ||= compute_coverage_statistics
31
- criterion ? @coverage_statistics[criterion] : @coverage_statistics
30
+ stats = (@coverage_statistics ||= compute_coverage_statistics)
31
+ criterion ? stats[criterion] : stats
32
32
  end
33
33
 
34
34
  def coverage_statistics_by_file
@@ -67,7 +67,11 @@ module SimpleCov
67
67
 
68
68
  # Finds the least covered file and returns that file's name
69
69
  def least_covered_file
70
- min_by(&:covered_percent).filename
70
+ # `covered_percent` is nil only for an unmeasured criterion, and :line
71
+ # is always measured, so the `|| 0.0` arm never fires at runtime; it
72
+ # (and the cast) exist to satisfy min_by's Comparable requirement.
73
+ least_covered = min_by { |file| file.covered_percent || 0.0 }
74
+ (_ = least_covered).filename
71
75
  end
72
76
 
73
77
  # Returns the overall amount of relevant lines of code across all files in this list
@@ -137,7 +141,10 @@ module SimpleCov
137
141
  # enabled set so disabled criteria don't surface in totals, JSON,
138
142
  # or the HTML report.
139
143
  def compute_coverage_statistics_by_file
140
- seed = enabled_criteria_for_reporting.to_h { |criterion| [criterion, []] }
144
+ seed = enabled_criteria_for_reporting.to_h do |criterion|
145
+ bucket = [] #: Array[CoverageStatistics]
146
+ [criterion, bucket]
147
+ end
141
148
  @files.each_with_object(seed) do |file, together|
142
149
  file.coverage_statistics.each do |criterion, stats|
143
150
  together[criterion] << stats if together.key?(criterion)
@@ -153,7 +160,7 @@ module SimpleCov
153
160
  # criterion is enabled; the JRuby-gated branch/method criteria are
154
161
  # reported when they pass their own engine-support check.
155
162
  def enabled_criteria_for_reporting
156
- criteria = []
163
+ criteria = [] #: Array[SimpleCov::criterion]
157
164
  criteria << :line if SimpleCov.coverage_criterion_enabled?(:line) ||
158
165
  SimpleCov.coverage_criterion_enabled?(:oneshot_line)
159
166
  criteria << :branch if SimpleCov.branch_coverage?
@@ -45,7 +45,8 @@ module SimpleCov
45
45
  # confusing than the absolute form. See issue #197.
46
46
  def displayable_output_path
47
47
  directory = relative_or_absolute_output_path
48
- entry_point_filename ? File.join(directory, entry_point_filename) : directory
48
+ entry_point = entry_point_filename
49
+ entry_point ? File.join(directory, entry_point) : directory
49
50
  end
50
51
 
51
52
  def relative_or_absolute_output_path
@@ -31,8 +31,10 @@ module SimpleCov
31
31
 
32
32
  copy_static_assets
33
33
  # stderr, not stdout: this is a status message, not the program's
34
- # output. Keeps the line out of pipelines like `rspec -f json`.
35
- warn output_message(result) unless @silent
34
+ # output. Keeps the line out of pipelines like `rspec -f json`. And
35
+ # $stderr.puts, not `warn`: a status line should not reach
36
+ # `Warning.warn` hooks or vanish under `-W0` (see #1225).
37
+ $stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
36
38
  end
37
39
 
38
40
  # Generate HTML from a pre-existing coverage.json file without
@@ -65,14 +67,18 @@ module SimpleCov
65
67
  # from opening the existing path for writing.
66
68
  def atomic_write(dest, content)
67
69
  temp = "#{dest}.#{Process.pid}.#{rand(2**32).to_s(36)}"
68
- File.binwrite(temp, content)
69
- File.rename(temp, dest)
70
- ensure
71
- FileUtils.rm_f(temp)
70
+ begin
71
+ File.binwrite(temp, content)
72
+ File.rename(temp, dest)
73
+ ensure
74
+ FileUtils.rm_f(temp)
75
+ end
72
76
  end
73
77
 
74
78
  def public_dir
75
- File.join(__dir__, "html_formatter/public/")
79
+ # `.to_s` collapses `__dir__`'s nil arm (only possible under eval,
80
+ # which can't happen for a file on disk) so the path is a String.
81
+ File.join(__dir__.to_s, "html_formatter/public/")
76
82
  end
77
83
  end
78
84
  end
@@ -42,14 +42,20 @@ module SimpleCov
42
42
  end
43
43
 
44
44
  def record_by_file(violation)
45
- file_bucket = bucket(:minimum_coverage_by_file)[violation.fetch(:project_filename)] ||= {}
45
+ by_file = bucket(:minimum_coverage_by_file)
46
+ file_bucket = by_file[violation.fetch(:project_filename)] ||= {} #: Hash[untyped, untyped]
46
47
  file_bucket[key_for(violation)] = expected_actual(violation)
47
48
  end
48
49
 
49
50
  def format_minimum_by_group
50
51
  violations = SimpleCov::CoverageViolations.minimum_by_group(@result, SimpleCov.minimum_coverage_by_group)
52
+ return if violations.empty?
53
+
54
+ # `bucket` lazily creates the errors key, so only touch it when
55
+ # there is a violation to record.
56
+ by_group = bucket(:minimum_coverage_by_group)
51
57
  violations.each do |violation|
52
- group_bucket = bucket(:minimum_coverage_by_group)[violation.fetch(:group_name)] ||= {}
58
+ group_bucket = by_group[violation.fetch(:group_name)] ||= {} #: Hash[untyped, untyped]
53
59
  group_bucket[key_for(violation)] = expected_actual(violation)
54
60
  end
55
61
  end
@@ -68,7 +74,7 @@ module SimpleCov
68
74
  end
69
75
 
70
76
  def bucket(name)
71
- @errors[name] ||= {}
77
+ @errors[name] ||= {} #: Hash[untyped, untyped]
72
78
  end
73
79
 
74
80
  def key_for(violation)
@@ -95,7 +95,7 @@ module SimpleCov
95
95
  end
96
96
 
97
97
  def format_coverage_statistics(statistics)
98
- result = {}
98
+ result = {} #: Hash[Symbol, untyped]
99
99
  result[:lines] = format_line_statistic(statistics[:line]) if statistics[:line]
100
100
  result[:branches] = format_single_statistic(statistics[:branch]) if statistics[:branch]
101
101
  result[:methods] = format_single_statistic(statistics[:method]) if statistics[:method]
@@ -13,7 +13,7 @@ module SimpleCov
13
13
  end
14
14
 
15
15
  def call
16
- result = @include_source ? format_source_code : {}
16
+ result = @include_source ? format_source_code : {} #: Hash[Symbol, untyped]
17
17
  result.merge!(line_coverage_section) if line_coverage_enabled?
18
18
  result.merge!(branch_coverage_section) if SimpleCov.branch_coverage?
19
19
  result.merge!(method_coverage_section) if SimpleCov.method_coverage?
@@ -28,8 +28,10 @@ module SimpleCov
28
28
  warn_if_concurrent_overwrite(path, result)
29
29
  File.write(path, JSON.pretty_generate(self.class.build_hash(result)))
30
30
  # stderr, not stdout: this is a status message, not the program's
31
- # output. Keeps the line out of pipelines like `rspec -f json`.
32
- warn output_message(result) unless @silent
31
+ # output. Keeps the line out of pipelines like `rspec -f json`. And
32
+ # $stderr.puts, not `warn`: a status line should not reach
33
+ # `Warning.warn` hooks or vanish under `-W0` (see #1225).
34
+ $stderr.puts output_message(result) unless @silent # rubocop:disable Style/StderrPuts
33
35
  end
34
36
 
35
37
  private
@@ -12,16 +12,21 @@ module SimpleCov
12
12
  formatters.map do |formatter|
13
13
  formatter.new.format(result)
14
14
  rescue StandardError => e
15
- warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{e.backtrace.first})")
15
+ warn("Formatter #{formatter} failed with #{e.class}: #{e.message} (#{(_ = e.backtrace).first})")
16
16
  nil
17
17
  end
18
18
  end
19
19
  end
20
20
 
21
21
  def self.new(formatters = nil)
22
+ # Normalize eagerly and capture the list in the closure. Array()
23
+ # is pure for every accepted input shape (nil, single formatter,
24
+ # array, or another MultiFormatter class), so this is equivalent
25
+ # to the historical lazy per-instance memoization.
26
+ formatter_list = Array(formatters)
22
27
  Class.new do
23
28
  define_method :formatters do
24
- @formatters ||= Array(formatters)
29
+ formatter_list
25
30
  end
26
31
  include InstanceMethods
27
32
  end
@@ -15,7 +15,9 @@ module SimpleCov
15
15
 
16
16
  def format_group(name, files)
17
17
  header = "Group: #{name}\n#{'=' * 40}\n"
18
- body = files.map { |file| "#{file.filename} (coverage: #{file.covered_percent.floor(2)}%)\n" }.join
18
+ # `covered_percent` is nilable across criteria, but line stats are
19
+ # always measured, so the no-argument call can't return nil here.
20
+ body = files.map { |file| "#{file.filename} (coverage: #{(_ = file.covered_percent).floor(2)}%)\n" }.join
19
21
  "#{header}#{body}\n"
20
22
  end
21
23
  end
@@ -9,7 +9,24 @@
9
9
  # correctly with other libraries doing the same — they each prepend
10
10
  # their own module and chain via `super`.
11
11
 
12
+ # Reopened here for the fork-related at_exit plumbing that only matters
13
+ # when subprocess support is loaded.
12
14
  module SimpleCov
15
+ class << self
16
+ # Forked children inherit at_exit state that is wrong for them:
17
+ # @at_exit_hook_installed may describe a hook the parent already
18
+ # consumed before forking (Minitest autorun runs the suite inside the
19
+ # parent's at_exit, after SimpleCov's own hook has fired), and
20
+ # external_at_exit may point at a Minitest.after_run deferral that is
21
+ # pid-pinned to the parent. Reset both so the at_fork proc's
22
+ # SimpleCov.start installs a fresh hook that actually fires at child
23
+ # exit. See issue #1227.
24
+ def reset_inherited_at_exit_state!
25
+ @at_exit_hook_installed = false
26
+ self.external_at_exit = false
27
+ end
28
+ end
29
+
13
30
  # Prepended onto Process's singleton class so every fork — direct or
14
31
  # via Kernel#fork / IO.popen — re-runs SimpleCov's at_fork callback in
15
32
  # the child.
@@ -27,6 +44,11 @@ module SimpleCov
27
44
  # the user installed, so `final_result_process?` can keep forked
28
45
  # workers from each producing the final report. See issue #1171.
29
46
  SimpleCov.mark_forked_subprocess!
47
+ # Without this, a child forked under Minitest autorun measures
48
+ # coverage and then silently discards it: every inherited exit
49
+ # route is dead in the child (see the method's comment). A custom
50
+ # at_fork runs afterwards and can still override. See issue #1227.
51
+ SimpleCov.reset_inherited_at_exit_state!
30
52
  SimpleCov.at_fork.call(::Process.pid)
31
53
  end
32
54
  pid
@@ -27,13 +27,14 @@ module SimpleCov
27
27
 
28
28
  # Pre-0.18 resultsets pointed each filename straight at a line-coverage
29
29
  # array; everything since uses the `{lines:, branches:, methods:}`
30
- # shape. Newer entries also need their methods table massaged before
31
- # downstream code merges across processes.
30
+ # shape. Newer entries also need their methods and branches tables
31
+ # massaged before downstream code reports or merges them.
32
32
  def adapt_one(file_name, cover_statistic)
33
33
  return {"lines" => cover_statistic} if cover_statistic.is_a?(Array)
34
34
 
35
35
  adapt_oneshot_lines_if_needed(file_name, cover_statistic)
36
36
  normalize_method_keys(cover_statistic)
37
+ aggregate_duplicated_branches(cover_statistic)
37
38
  cover_statistic
38
39
  end
39
40
 
@@ -59,19 +60,53 @@ module SimpleCov
59
60
  SINGLETON_WRAPPER_PATTERN = /\A#<Class:([A-Z_][\w:]*)>\z/
60
61
  private_constant :SINGLETON_WRAPPER_PATTERN
61
62
 
63
+ # Ruby's method coverage records one entry per RECEIVER, not per source
64
+ # location: a block handed to `define_method` / `define_singleton_method`
65
+ # from a shared code path (a module's `included` hook, a builder) yields
66
+ # a separate `[receiver, name, location]` entry for every class it's
67
+ # defined on, all pointing at the same source. A file-based report can
68
+ # only express "was the method at this location ever executed", so
69
+ # entries are aggregated by (name, location), summing hits — otherwise
70
+ # each receiver whose copy never ran shows as a phantom uncovered method
71
+ # on a line whose line coverage is 100% (issue #1234). The first entry's
72
+ # (normalized) receiver is kept for display.
62
73
  def normalize_method_keys(cover_statistic)
63
74
  methods = cover_statistic[:methods]
64
75
  return unless methods
65
76
 
66
- cover_statistic[:methods] = methods.each_with_object({}) do |(key, count), normalized|
67
- normalized_key = key.dup
68
- normalized_key[0] = key[0].to_s
69
- .gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
70
- .sub(SINGLETON_WRAPPER_PATTERN, '\1')
71
- # Keys may collide after normalization (anonymous classes sharing a
72
- # method name, or singleton + instance forms of a module_function method).
73
- normalized[normalized_key] = normalized.fetch(normalized_key, 0) + count
77
+ aggregated = {} #: Hash[untyped, [untyped, Integer]]
78
+ methods.each_with_object(aggregated) do |(key, count), memo|
79
+ identity = key[1..] #: Array[untyped]
80
+ retained_key, existing = memo[identity] || [normalize_method_key(key), 0]
81
+ memo[identity] = [retained_key, existing + count]
74
82
  end
83
+ cover_statistic[:methods] = aggregated.values.to_h
84
+ end
85
+
86
+ def normalize_method_key(key)
87
+ normalized_key = key.dup
88
+ normalized_key[0] = key[0].to_s
89
+ .gsub(ADDRESS_PATTERN, ADDRESS_PLACEHOLDER)
90
+ .sub(SINGLETON_WRAPPER_PATTERN, '\1')
91
+ normalized_key
92
+ end
93
+
94
+ # Ruby's eval coverage records a fresh set of branch entries for every
95
+ # COMPILE of an eval'd string: a template rendered through multiple view
96
+ # classes (e.g. hanami-view compiles each template once per view) yields
97
+ # several `[:if, id, location]` conditions at identical coordinates in
98
+ # the same file, each counting only the renders that flowed through that
99
+ # compile. Reported as-is they inflate the branch denominator and turn a
100
+ # side covered under a different compile into a phantom miss (issue
101
+ # #1235). Aggregate them by (type, location) — combining a branches hash
102
+ # with an empty one dedups within it, since BranchesCombiner keys arms
103
+ # on location identity. Regular (non-eval) source can never produce two
104
+ # conditions at the same location, so this is a no-op outside eval.
105
+ def aggregate_duplicated_branches(cover_statistic)
106
+ branches = cover_statistic[:branches]
107
+ return unless branches
108
+
109
+ cover_statistic[:branches] = Combine::BranchesCombiner.combine(branches, {})
75
110
  end
76
111
 
77
112
  def adapt_oneshot_lines_if_needed(file_name, cover_statistic)
@@ -66,7 +66,7 @@ module SimpleCov
66
66
 
67
67
  # Applies the configured filters to the given array of SimpleCov::SourceFile items
68
68
  def filtered(files)
69
- result = files.clone
69
+ result = files.to_a.dup
70
70
  filters.each do |filter|
71
71
  result = result.reject { |source_file| filter.matches?(source_file) }
72
72
  end
@@ -170,9 +170,9 @@ module SimpleCov
170
170
  # off); with merging on the merged result reports dropped source files,
171
171
  # so the per-process slice stays quiet to avoid one warning per worker.
172
172
  def process_coverage_result(report:)
173
- @result = SimpleCov::UselessResultsRemover.call(Coverage.result)
174
- @result = SimpleCov::ResultAdapter.call(@result)
175
- result, not_loaded_files = add_not_loaded_files(@result)
173
+ raw = SimpleCov::UselessResultsRemover.call(Coverage.result)
174
+ adapted = SimpleCov::ResultAdapter.call(raw)
175
+ result, not_loaded_files = add_not_loaded_files(adapted)
176
176
  @result = SimpleCov::Result.new(result, not_loaded_files: not_loaded_files, report: report)
177
177
  end
178
178
  end
@@ -39,8 +39,8 @@ module SimpleCov
39
39
  source_lines = read_lines(absolute_path)
40
40
  lines = coverage_stub(absolute_path, source_lines) ||
41
41
  LinesClassifier.new.classify(source_lines)
42
- synthesized = StaticCoverageExtractor.call(source_lines.join) ||
43
- {"branches" => {}, "methods" => {}}
42
+ empty = {"branches" => {}, "methods" => {}} #: Hash[String, Hash[untyped, untyped]]
43
+ synthesized = StaticCoverageExtractor.call(source_lines.join) || empty
44
44
 
45
45
  {
46
46
  "lines" => lines,
@@ -37,12 +37,12 @@ module SimpleCov
37
37
 
38
38
  # Returns true if this is a line that should have been covered, but was not
39
39
  def missed?
40
- !never? && !skipped? && coverage.zero?
40
+ !never? && !skipped? && coverage.to_i.zero?
41
41
  end
42
42
 
43
43
  # Returns true if this is a line that has been covered
44
44
  def covered?
45
- !never? && !skipped? && coverage.positive?
45
+ !never? && !skipped? && coverage.to_i.positive?
46
46
  end
47
47
 
48
48
  # Returns true if this line is not relevant for coverage
@@ -34,7 +34,7 @@ module SimpleCov
34
34
  end
35
35
 
36
36
  def lines
37
- @lines ||= start_line && end_line ? source_file.lines[(start_line - 1)..(end_line - 1)] : []
37
+ @lines ||= start_line && end_line ? source_file.lines[(start_line - 1)..(end_line - 1)] || [] : []
38
38
  end
39
39
 
40
40
  # Whether this method's source range intersects the given inclusive line range.
@@ -12,7 +12,9 @@ module SimpleCov
12
12
  end
13
13
 
14
14
  def call
15
- methods = @source_file.coverage_data.fetch("methods", {}).filter_map do |info, hit_count|
15
+ none = {} #: Hash[untyped, Integer]
16
+ raw_methods = @source_file.coverage_data.fetch("methods", none)
17
+ methods = raw_methods.filter_map do |info, hit_count|
16
18
  info = RubyDataParser.call(info)
17
19
  next if eval_generated_to_ignore?(info)
18
20
 
@@ -14,12 +14,12 @@ module SimpleCov
14
14
  module_function
15
15
 
16
16
  def call(filename)
17
- lines = []
17
+ lines = [] #: Array[String]
18
18
  # The default encoding is UTF-8
19
19
  File.open(filename, "rb:UTF-8") do |file|
20
20
  current_line = file.gets
21
21
 
22
- if shebang?(current_line)
22
+ if current_line && shebang?(current_line)
23
23
  lines << current_line
24
24
  current_line = file.gets
25
25
  end
@@ -51,8 +51,8 @@ module SimpleCov
51
51
  # returns the `{line:, branch:, method:}` Hash; pass a criterion symbol
52
52
  # (`:line` / `:branch` / `:method`) to get that one CoverageStatistics.
53
53
  def coverage_statistics(criterion = nil)
54
- @coverage_statistics ||= Statistics.new(self).call
55
- criterion ? @coverage_statistics[criterion] : @coverage_statistics
54
+ stats = (@coverage_statistics ||= Statistics.new(self).call)
55
+ criterion ? stats[criterion] : stats
56
56
  end
57
57
 
58
58
  # Returns all source lines for this file as instances of SimpleCov::SourceFile::Line,
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
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.
11
+ module ConditionFolding
12
+ # Prism node types for the literals that fold. `while` / `until` do
13
+ # NOT fold (`while true` is a real branch), so only the if-like
14
+ # visitors consult this. Regexp and Range literals are excluded on
15
+ # purpose: as conditions they mean `=~ $_` / flip-flop, which
16
+ # Coverage does branch on.
17
+ STATIC_CONDITION_TYPES = [
18
+ ::Prism::IntegerNode, ::Prism::FloatNode, ::Prism::RationalNode,
19
+ ::Prism::ImaginaryNode, ::Prism::SymbolNode, ::Prism::StringNode,
20
+ ::Prism::TrueNode, ::Prism::FalseNode, ::Prism::NilNode
21
+ ].freeze
22
+
23
+ private
24
+
25
+ # Parentheses are transparent to the fold (`if (1)` folds like
26
+ # `if 1`), so see through a single parenthesized expression. Compound
27
+ # forms (`!true`, `true || x`) are deliberately not folded: `!` never
28
+ # folds, and `||` / `&&` constant-propagation diverges across Ruby
29
+ # versions, so matching it would trade a rare, version-specific gain
30
+ # for real risk.
31
+ def static_condition?(node)
32
+ node = unwrap_parentheses(node)
33
+ STATIC_CONDITION_TYPES.any? { |type| node.is_a?(type) }
34
+ end
35
+
36
+ def unwrap_parentheses(node)
37
+ # @type var current: untyped
38
+ current = node
39
+ while current.is_a?(::Prism::ParenthesesNode)
40
+ body = current.body
41
+ break unless body.is_a?(::Prism::StatementsNode) && body.body.size == 1
42
+
43
+ current = body.body.first
44
+ end
45
+ current
46
+ end
47
+ end
48
+ end
49
+ end