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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 26a19b5934dcf245f7e9cd50f8bc86224c4a7e8f02be43d813596d1de36e637d
4
- data.tar.gz: b20bbfed696e333c5b41647b96202cf86b0a9375e529871052ecf3522cb37ae0
3
+ metadata.gz: 1f159cd5427690b20f11c1e4a3f1a90c23c16f5f65e2043205bf132a5abc0218
4
+ data.tar.gz: d5a28a2c529ff2b59652bd0c1ba1f9d649c7c24fd02521905c6d2cbe6b1eeae3
5
5
  SHA512:
6
- metadata.gz: 5f97c41bad62008a68ad3fb02919be7728e1f0c1f61049ff2d47f57455ef4d86530271ad4a7f7704b022ffd44366f0c41ebe4f051631ce5d67acff7b9e31a9c2
7
- data.tar.gz: 5aeb18029dcb5aafad9dc878aa218113cbe26643db1bf8a1f5b1eabe09855ba0a91605a68c874bbc11f38d969d2c972090a6888ef1692d6f8c7d8bce0124d0fb
6
+ metadata.gz: 792c138d327b5d305ebe273dfd5a97d70eb8104102557d5ae6452a26d5cc64e1db7720c3de5e2c556a78b22561c53778579909ddef7f4615e1c447f041fa921c
7
+ data.tar.gz: 9f6479ceabbd5f6fc24585a2134a0f4e1270dbd988922d4d387e5695055b811655999b3d95c2a9b31c960cf0c22aa884d5bf94eae93ca6fb97868bbec0de3aab
@@ -28,7 +28,7 @@ module SimpleCov
28
28
  end
29
29
 
30
30
  def parse(args, stderr:)
31
- opts = {input: SimpleCov::CLI.default_input, json: false, no_color: false}
31
+ opts = {input: SimpleCov::CLI.default_input, json: false, no_color: false} #: Hash[Symbol, untyped]
32
32
  rest =
33
33
  OptionParser.new do |o|
34
34
  o.on("--input PATH") { |v| opts[:input] = v }
@@ -45,7 +45,8 @@ module SimpleCov
45
45
  return stderr.puts("simplecov coverage: #{opts[:input]} not found") && nil unless File.exist?(opts[:input])
46
46
 
47
47
  data = JSON.parse(File.read(opts[:input]))
48
- match = lookup(data.fetch("coverage", {}), opts[:path])
48
+ none = {} #: Hash[String, untyped]
49
+ match = lookup(data.fetch("coverage", none), opts[:path])
49
50
  return match if match
50
51
 
51
52
  stderr.puts("simplecov coverage: no entry for #{opts[:path]} in #{opts[:input]}")
@@ -65,7 +66,8 @@ module SimpleCov
65
66
  def emit(match, opts, stdout)
66
67
  filename, payload = match
67
68
  if opts[:json]
68
- stdout.puts(JSON.pretty_generate(filename => payload))
69
+ entry = {filename => payload} #: Hash[untyped, untyped]
70
+ stdout.puts(JSON.pretty_generate(entry))
69
71
  else
70
72
  print_human(filename, payload, stdout, SimpleCov::CLI.color_enabled?(opts, stdout))
71
73
  end
@@ -68,7 +68,8 @@ module SimpleCov
68
68
  end
69
69
 
70
70
  def load_coverage(path, stderr)
71
- return normalize_keys(JSON.parse(File.read(path)).fetch("coverage", {})) if File.exist?(path)
71
+ no_coverage = {} #: Hash[String, untyped]
72
+ return normalize_keys(JSON.parse(File.read(path)).fetch("coverage", no_coverage)) if File.exist?(path)
72
73
 
73
74
  stderr.puts("simplecov diff: #{path} not found")
74
75
  nil
@@ -57,8 +57,8 @@ module SimpleCov
57
57
  end
58
58
 
59
59
  def with_simplecov_loaded
60
- previous_no_defaults = ENV.fetch("SIMPLECOV_NO_DEFAULTS", nil)
61
- previous_cli = ENV.fetch("SIMPLECOV_CLI", nil)
60
+ previous_no_defaults = ENV.fetch("SIMPLECOV_NO_DEFAULTS", nil) #: String?
61
+ previous_cli = ENV.fetch("SIMPLECOV_CLI", nil) #: String?
62
62
  ENV["SIMPLECOV_NO_DEFAULTS"] = "1"
63
63
  # SIMPLECOV_CLI lets a project's `.simplecov` opt some config into
64
64
  # CLI-only behavior — e.g. simplecov itself sets `coverage_dir`
@@ -67,6 +67,8 @@ module SimpleCov
67
67
  require "simplecov"
68
68
  yield
69
69
  ensure
70
+ # @type var previous_no_defaults: String?
71
+ # @type var previous_cli: String?
70
72
  ENV["SIMPLECOV_NO_DEFAULTS"] = previous_no_defaults
71
73
  ENV["SIMPLECOV_CLI"] = previous_cli
72
74
  end
@@ -79,20 +81,23 @@ module SimpleCov
79
81
  def load_with_start_neutered(path)
80
82
  klass = SimpleCov.singleton_class
81
83
  names = %i[start_tracking install_at_exit_hook]
82
- stash = names.to_h { |name| [name, klass.instance_method(name)] }
84
+ stash = names.to_h { |name| [name, klass.instance_method(name)] } #: Hash[Symbol, UnboundMethod]
83
85
  # define_method over an existing method emits a "method redefined"
84
86
  # warning under $VERBOSE; the override and restore are intentional.
85
87
  silence_verbose { names.each { |name| klass.define_method(name) { nil } } }
86
88
  load path
87
89
  ensure
90
+ # @type var stash: Hash[Symbol, UnboundMethod]
91
+ # @type var klass: Class
88
92
  silence_verbose { stash.each { |name, method| klass.define_method(name, method) } }
89
93
  end
90
94
 
91
95
  def silence_verbose
92
- previous = $VERBOSE
96
+ previous = $VERBOSE #: bool?
93
97
  $VERBOSE = nil
94
98
  yield
95
99
  ensure
100
+ # @type var previous: bool?
96
101
  $VERBOSE = previous
97
102
  end
98
103
  end
@@ -57,7 +57,8 @@ module SimpleCov
57
57
  # generic "no mergeable results" into a message that points at
58
58
  # the specific input causing the failure.
59
59
  def parse_inputs(files, stderr)
60
- files.each_with_object({}) do |path, memo|
60
+ parsed = {} #: Hash[String, Hash[String, untyped]]
61
+ files.each_with_object(parsed) do |path, memo|
61
62
  data = parse_input(path, stderr) or return nil
62
63
 
63
64
  memo[path] = data
@@ -85,8 +86,9 @@ module SimpleCov
85
86
  # mistake for "no merge happened." Surface the overlap so the
86
87
  # operator can rename the workers or accept the merge knowingly.
87
88
  def warn_about_duplicate_command_names(parsed, stderr)
88
- files_per_command = parsed.each_with_object({}) do |(path, data), memo|
89
- data.each_key { |command_name| (memo[command_name] ||= []) << path }
89
+ files_per_command = {} #: Hash[String, Array[String]]
90
+ parsed.each do |path, data|
91
+ data.each_key { |command_name| (files_per_command[command_name] ||= []) << path }
90
92
  end
91
93
  files_per_command.each do |command_name, paths|
92
94
  next if paths.size < 2
@@ -45,8 +45,9 @@ module SimpleCov
45
45
  end
46
46
 
47
47
  def emit_text(stdout, data, color)
48
- emit_totals(stdout, "All Files", data.fetch("total", {}), color)
49
- data.fetch("groups", {}).each { |name, group| emit_totals(stdout, name, group, color) }
48
+ none = {} #: Hash[String, untyped]
49
+ emit_totals(stdout, "All Files", data.fetch("total", none), color)
50
+ data.fetch("groups", none).each { |name, group| emit_totals(stdout, name, group, color) }
50
51
  end
51
52
 
52
53
  def emit_totals(stdout, label, totals, color)
@@ -66,17 +67,21 @@ module SimpleCov
66
67
  end
67
68
 
68
69
  def emit_json(stdout, data)
69
- payload = {"All Files" => collect_section(data.fetch("total", {}))}
70
- data.fetch("groups", {}).each { |name, group| payload[name] = collect_section(group) }
70
+ none = {} #: Hash[String, untyped]
71
+ payload = {"All Files" => collect_section(data.fetch("total", none))}
72
+ data.fetch("groups", none).each { |name, group| payload[name] = collect_section(group) }
71
73
  stdout.puts(JSON.pretty_generate(payload))
72
74
  end
73
75
 
74
76
  def collect_section(totals)
75
- SECTIONS.each_with_object({}) do |(_, key), out|
77
+ sections = {} #: Hash[String, untyped]
78
+ SECTIONS.each_with_object(sections) do |(_, key), out|
76
79
  section = totals[key]
77
80
  next unless section.is_a?(Hash) && section["total"].to_i.positive?
78
81
 
79
- out[key] = {"percent" => section["percent"], "covered" => section["covered"], "total" => section["total"]}
82
+ out[key.to_s] = {
83
+ "percent" => section["percent"], "covered" => section["covered"], "total" => section["total"]
84
+ }
80
85
  end
81
86
  end
82
87
  end
@@ -8,7 +8,7 @@ module SimpleCov
8
8
  # without a test_helper that already calls SimpleCov.start (e.g.
9
9
  # plain `bundle exec rake test` on an unconfigured library).
10
10
  module Run
11
- AUTOSTART = File.expand_path("../autostart", __dir__)
11
+ AUTOSTART = File.expand_path("../autostart", __dir__.to_s)
12
12
 
13
13
  module_function
14
14
 
@@ -36,16 +36,26 @@ module SimpleCov
36
36
  return error(stderr, "#{dir} doesn't exist; run your test suite first") unless File.directory?(dir)
37
37
 
38
38
  require "socket"
39
- server = TCPServer.new(opts[:host], opts[:port])
40
- announce(stdout, server, dir)
41
- serve_loop(server, dir, stdout)
42
- 0
43
- ensure
44
- server&.close
39
+ with_server(opts) do |server|
40
+ announce(stdout, server, dir)
41
+ serve_loop(server, dir, stdout)
42
+ 0
43
+ end
44
+ end
45
+
46
+ def with_server(opts)
47
+ # The receiver cast works around an rbs stdlib gap: TCPSocket's
48
+ # explicit `self.new` shadows TCPServer#initialize's (host, port) form.
49
+ server = (_ = TCPServer).new(opts[:host], opts[:port]) #: TCPServer
50
+ begin
51
+ yield server
52
+ ensure
53
+ server.close
54
+ end
45
55
  end
46
56
 
47
57
  def parse(args)
48
- opts = {port: 0, host: "127.0.0.1"}
58
+ opts = {port: 0, host: "127.0.0.1"} #: Hash[Symbol, untyped]
49
59
  OptionParser.new do |o|
50
60
  o.on("--port N", Integer) { |v| opts[:port] = v }
51
61
  o.on("--host HOST") { |v| opts[:host] = v }
@@ -33,7 +33,8 @@ module SimpleCov
33
33
  def report(opts, keys, stdout, stderr)
34
34
  return 1 unless (data = Report.load_data(opts[:input], stderr))
35
35
 
36
- files = rank(data.fetch("coverage", {}), opts[:threshold], keys).first(opts[:top])
36
+ none = {} #: Hash[String, untyped]
37
+ files = rank(data.fetch("coverage", none), opts[:threshold], keys).first(opts[:top])
37
38
  return stdout.puts(empty_message(opts[:json])) || 0 if files.empty?
38
39
 
39
40
  emit(stdout, files, opts)
@@ -34,7 +34,12 @@ module SimpleCov
34
34
  # doesn't get ANSI sequences. See the module-level comment for
35
35
  # precedence.
36
36
  def enabled?(stream = $stderr)
37
- config = SimpleCov.color
37
+ # `SimpleCov.color` only exists once the full library is loaded.
38
+ # The standalone CLI (`exe/simplecov`) loads `simplecov/color`
39
+ # without `simplecov` itself to stay lightweight, so treat a
40
+ # missing config the same as its `:auto` default: fall through to
41
+ # the env vars and tty check below.
42
+ config = SimpleCov.color if SimpleCov.respond_to?(:color)
38
43
  return config if [true, false].include?(config)
39
44
  return false if env_set?("NO_COLOR")
40
45
  return true if env_set?("FORCE_COLOR")
@@ -24,7 +24,8 @@ module SimpleCov
24
24
  # @return [Hash]
25
25
  #
26
26
  def combine(coverage_a, coverage_b)
27
- combined = [coverage_a, coverage_b].each_with_object({}) do |coverage, memo|
27
+ merged = {} #: Hash[untyped, [untyped, Hash[untyped, untyped]]]
28
+ combined = [coverage_a, coverage_b].each_with_object(merged) do |coverage, memo|
28
29
  coverage.each do |condition, branches_inside|
29
30
  condition_key = tuple_identity(condition)
30
31
  condition_tuple, merged_branches = memo[condition_key] ||= [condition, {}]
@@ -9,22 +9,62 @@ module SimpleCov
9
9
  module FilesCombiner
10
10
  module_function
11
11
 
12
+ empty_table = {} #: Hash[untyped, untyped]
13
+ EMPTY_TABLE = empty_table.freeze
14
+ private_constant :EMPTY_TABLE
15
+
16
+ # Branch/method tuples drawn from a simulated (never-loaded) file
17
+ # when the other side of the merge was actually executed.
18
+ NO_SYNTHESIZED = {"branches" => EMPTY_TABLE, "methods" => EMPTY_TABLE}.freeze
19
+
12
20
  #
13
21
  # Combines the results for 2 coverages of a file.
14
22
  #
15
23
  # @return [Hash]
16
24
  #
17
25
  def combine(coverage_a, coverage_b)
26
+ source_a, source_b = reconcile_synthesized(coverage_a, coverage_b)
27
+
18
28
  combination = {"lines" => Combine.combine(LinesCombiner, coverage_a["lines"], coverage_b["lines"])}
19
29
  if SimpleCov.branch_coverage?
20
- combined_branches = Combine.combine(BranchesCombiner, coverage_a["branches"], coverage_b["branches"])
30
+ combined_branches = Combine.combine(BranchesCombiner, source_a["branches"], source_b["branches"])
21
31
  combination["branches"] = combined_branches || {}
22
32
  end
23
33
  if SimpleCov.method_coverage?
24
- combination["methods"] = Combine.combine(MethodsCombiner, coverage_a["methods"], coverage_b["methods"])
34
+ combination["methods"] = Combine.combine(MethodsCombiner, source_a["methods"], source_b["methods"])
25
35
  end
26
36
  combination
27
37
  end
38
+
39
+ # When exactly one side of the merge was actually executed, its branch
40
+ # and method tuples are authoritative and the other side's are dropped.
41
+ # A simulated entry (SimulateCoverage backfills tracked-but-unloaded
42
+ # files) synthesizes those tuples statically, so a location that drifts
43
+ # from what Coverage emits would otherwise be unioned in by position
44
+ # and survive as a phantom, permanently-missed branch (see #1233). This
45
+ # contains any such drift to denominator inflation for files no process
46
+ # loaded, rather than a false miss on a covered file. Lines are never
47
+ # dropped: a simulated file's line shape is correct and carries the
48
+ # unloaded-file denominator (#1059).
49
+ #
50
+ # Returns the two coverages to draw branch/method tuples from, blanking
51
+ # the non-executed side only when the other side was executed. When
52
+ # both sides were executed (two real runs) or neither was (all
53
+ # simulated), both are returned unchanged and combine normally.
54
+ def reconcile_synthesized(coverage_a, coverage_b)
55
+ executed_a = executed?(coverage_a)
56
+ executed_b = executed?(coverage_b)
57
+ return [coverage_a, coverage_b] if executed_a == executed_b
58
+
59
+ executed_a ? [coverage_a, NO_SYNTHESIZED] : [NO_SYNTHESIZED, coverage_b]
60
+ end
61
+
62
+ # A file some process actually loaded has at least one executed line;
63
+ # a simulated (never-loaded) file's lines are all `nil` or `0`.
64
+ def executed?(coverage)
65
+ lines = Array(coverage["lines"]) #: Array[Integer?]
66
+ lines.any? { |count| count&.positive? }
67
+ end
28
68
  end
29
69
  end
30
70
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../source_file/ruby_data_parser"
4
+
3
5
  module SimpleCov
4
6
  module Combine
5
7
  #
@@ -12,14 +14,35 @@ module SimpleCov
12
14
  #
13
15
  # Return merged methods or the existing methods if other is missing.
14
16
  #
15
- # Method coverage is a flat hash mapping method identifiers to hit counts.
16
- # Combining sums the hit counts for matching methods and preserves methods
17
- # that only appear in one result.
17
+ # Method coverage maps `[class, name, start_line, start_col, end_line,
18
+ # end_col]` keys to hit counts. Keys are matched on their SOURCE
19
+ # identity (name, location), ignoring the class element — because
20
+ # Ruby records one entry per receiver: the same `define_method` block
21
+ # defined onto different classes in different processes arrives with
22
+ # different (normalized) receivers for the same source method, and
23
+ # matching on the full key would keep both, letting a never-called
24
+ # receiver's 0 shadow a covered method after merge (issue #1234).
25
+ # Combining sums the hit counts for matching methods and preserves
26
+ # methods that only appear in one result.
18
27
  #
19
28
  # @return [Hash]
20
29
  #
21
30
  def combine(coverage_a, coverage_b)
22
- coverage_a.merge(coverage_b) { |_key, a_count, b_count| a_count + b_count }
31
+ merged = {} #: Hash[untyped, [untyped, Integer]]
32
+ [coverage_a, coverage_b].each_with_object(merged) do |coverage, memo|
33
+ coverage.each do |key, count|
34
+ method_key = source_identity(key)
35
+ retained_key, existing = memo[method_key] || [key, 0]
36
+ memo[method_key] = [retained_key, existing + count]
37
+ end
38
+ end
39
+
40
+ merged.values.to_h
41
+ end
42
+
43
+ def source_identity(key)
44
+ _class_name, *identity = SourceFile::RubyDataParser.call(key)
45
+ identity
23
46
  end
24
47
  end
25
48
  end
@@ -21,7 +21,8 @@ module SimpleCov
21
21
  # @return [Hash]
22
22
  #
23
23
  def combine(*results)
24
- results.reduce({}) do |combined_results, next_result|
24
+ initial = {} #: Hash[untyped, untyped]
25
+ results.reduce(initial) do |combined_results, next_result|
25
26
  combine_result_sets(combined_results, next_result)
26
27
  end
27
28
  end
@@ -42,7 +42,9 @@ module SimpleCov
42
42
  #
43
43
  def coverage(criterion, primary: false, enabled: true, oneshot: false, **thresholds, &block)
44
44
  criterion = enable_coverage_criterion(criterion, enabled: enabled, oneshot: oneshot)
45
- primary_coverage(criterion) if primary
45
+ # The cast admits :eval, which primary_coverage rejects at runtime
46
+ # (it is enable-only and never in the enabled-criteria set).
47
+ primary_coverage(_ = criterion) if primary
46
48
 
47
49
  configurator = CoverageCriterion.new(self, criterion)
48
50
  apply_threshold_options(configurator, thresholds)
@@ -164,7 +166,9 @@ module SimpleCov
164
166
 
165
167
  # Make this criterion the report's primary (leading) criterion.
166
168
  def primary
167
- @config.primary_coverage(@criterion)
169
+ # @criterion is Symbol-wide because this receiver is also built for
170
+ # :eval; primary_coverage validates at runtime.
171
+ @config.primary_coverage(_ = @criterion)
168
172
  end
169
173
  end
170
174
  end
@@ -134,7 +134,9 @@ module SimpleCov
134
134
  def default_primary_coverage
135
135
  return DEFAULT_COVERAGE_CRITERION if coverage_criterion_enabled?(DEFAULT_COVERAGE_CRITERION)
136
136
 
137
- coverage_criteria.first
137
+ # Set#first types as nilable, but an empty criteria set is rejected at
138
+ # start_tracking before any caller can observe a nil here.
139
+ _ = coverage_criteria.first
138
140
  end
139
141
 
140
142
  def raise_if_criterion_disabled(criterion)
@@ -17,22 +17,32 @@ module SimpleCov
17
17
  # overhead.
18
18
  #
19
19
  def formatter(formatter = :__no_arg__)
20
- return @formatter if formatter == :__no_arg__
21
-
22
- @formatter = formatter || nil # normalize `false` to `nil`
20
+ case formatter
21
+ when :__no_arg__
22
+ @formatter
23
+ else
24
+ @formatter = formatter || nil # normalize `false` to `nil`
25
+ end
23
26
  end
24
27
 
25
28
  # Sets the configured formatters. Pass `[]` to opt out of
26
29
  # formatting entirely; see `formatter` for the rationale.
27
30
  def formatters(formatters = :__no_arg__)
28
- return Array(formatter) if formatters == :__no_arg__
29
-
30
- self.formatters = formatters
31
- formatters
31
+ case formatters
32
+ when :__no_arg__
33
+ configured = formatter
34
+ configured ? [configured] : []
35
+ else
36
+ self.formatters = formatters
37
+ formatters
38
+ end
32
39
  end
33
40
 
34
41
  # Sets the configured formatters. Equivalent to `formatters [...]`.
42
+ # Accepts a single formatter as well as an Array, matching the pre-1.0 behavior
43
+ # where `MultiFormatter.new` normalized its input.
35
44
  def formatters=(formatters)
45
+ formatters = Array(formatters)
36
46
  @formatter = formatters.empty? ? nil : SimpleCov::Formatter::MultiFormatter.new(formatters)
37
47
  end
38
48
 
@@ -123,7 +123,11 @@ module SimpleCov
123
123
  # so downstream code only has one shape to handle.
124
124
  def partition_per_file_thresholds(coverage)
125
125
  coverage.each_key { |key| validate_per_file_key(key) }
126
- defaults, raw = coverage.partition { |key, _| key.is_a?(Symbol) }.map(&:to_h)
126
+ pairs = coverage.partition { |key, _| key.is_a?(Symbol) }
127
+ # The assertions restate what the partition predicate guarantees:
128
+ # Symbol keys carry per-criterion Numeric defaults, the rest are paths.
129
+ defaults = pairs[0].to_h #: coverage_thresholds
130
+ raw = pairs[1].to_h #: Hash[String | Regexp, Numeric | coverage_thresholds]
127
131
  overrides = raw.transform_values { |value| value.is_a?(Numeric) ? {primary_coverage => value} : value }
128
132
  [defaults, overrides]
129
133
  end
@@ -143,11 +147,11 @@ module SimpleCov
143
147
  # `minimum_coverage_by_file` argument so the deprecation warning can be
144
148
  # copy-pasted verbatim into the user's config.
145
149
  def per_file_coverage_replacement(defaults, overrides)
146
- by_criterion = Hash.new { |hash, criterion| hash[criterion] = [] }
147
- defaults.each { |criterion, percent| by_criterion[criterion] << "minimum_per_file #{percent}" }
150
+ by_criterion = {} #: Hash[Symbol, Array[String]]
151
+ defaults.each { |criterion, percent| (by_criterion[criterion] ||= []) << "minimum_per_file #{percent}" }
148
152
  overrides.each do |target, criteria|
149
153
  criteria.each do |criterion, percent|
150
- by_criterion[criterion] << "minimum_per_file #{percent}, only: #{target.inspect}"
154
+ (by_criterion[criterion] ||= []) << "minimum_per_file #{percent}, only: #{target.inspect}"
151
155
  end
152
156
  end
153
157
  render_coverage_blocks(by_criterion)
@@ -155,11 +159,11 @@ module SimpleCov
155
159
 
156
160
  # Same, for a (deprecated) `minimum_coverage_by_group` argument.
157
161
  def per_group_coverage_replacement(coverage)
158
- by_criterion = Hash.new { |hash, criterion| hash[criterion] = [] }
159
- coverage.each do |group_name, group_coverage|
160
- group_coverage = {primary_coverage => group_coverage} if group_coverage.is_a?(Numeric)
161
- group_coverage.each do |criterion, percent|
162
- by_criterion[criterion] << "minimum_per_group #{percent}, only: #{group_name.inspect}"
162
+ by_criterion = {} #: Hash[Symbol, Array[String]]
163
+ coverage.each do |group_name, thresholds|
164
+ normalized = (thresholds.is_a?(Numeric) ? {primary_coverage => thresholds} : thresholds) #: coverage_thresholds
165
+ normalized.each do |criterion, percent|
166
+ (by_criterion[criterion] ||= []) << "minimum_per_group #{percent}, only: #{group_name.inspect}"
163
167
  end
164
168
  end
165
169
  render_coverage_blocks(by_criterion)
@@ -51,9 +51,10 @@ module SimpleCov
51
51
  #
52
52
  def coverage_path(path = nil)
53
53
  if path
54
- @coverage_path = File.expand_path(path)
54
+ expanded = File.expand_path(path)
55
+ @coverage_path = expanded
55
56
  @coverage_path_explicit = true
56
- FileUtils.mkdir_p @coverage_path
57
+ FileUtils.mkdir_p expanded
57
58
  end
58
59
 
59
60
  @coverage_path ||= File.expand_path(coverage_dir, root)
@@ -65,9 +66,8 @@ module SimpleCov
65
66
  # with SimpleCov.command_name("test:units").
66
67
  #
67
68
  def command_name(name = nil)
68
- @name = name unless name.nil?
69
- @name ||= SimpleCov::CommandGuesser.guess
70
- @name
69
+ @command_name = name unless name.nil?
70
+ @command_name ||= SimpleCov::CommandGuesser.guess
71
71
  end
72
72
 
73
73
  # Returns the hash of available profiles
@@ -80,17 +80,24 @@ module SimpleCov
80
80
  # prepending SimpleCov to each config method.
81
81
  #
82
82
  def configure(&block)
83
- block_context = block.binding.receiver
83
+ # Both locals are read in the ensure clause, where flow analysis
84
+ # cannot see the assignments below; anchor their types up front.
85
+ saved = nil # : Hash[Symbol, untyped]?
86
+ block_context = block.binding.receiver # : untyped
84
87
 
85
- # If the block was defined in our own context, instance_exec is sufficient
86
- return instance_exec(&block) if equal?(block_context)
88
+ # If the block was defined in our own context, instance_exec is
89
+ # sufficient. The `_ =` erases the block's self-binding, which RBS
90
+ # can express but `instance_exec`'s core signature cannot accept.
91
+ return instance_exec(&(_ = block)) if equal?(block_context)
87
92
 
88
93
  # Copy the caller's instance variables in so that references like @filter
89
94
  # inside the block resolve to the caller's values, not ours.
90
95
  saved = swap_ivars_from(block_context)
91
- instance_exec(&block)
96
+ instance_exec(&(_ = block))
92
97
  ensure
93
- restore_ivars(block_context, saved) if defined?(saved) && saved
98
+ # @type var block_context: untyped
99
+ # @type var saved: Hash[Symbol, untyped]?
100
+ restore_ivars(block_context, saved) if saved
94
101
  end
95
102
 
96
103
  #
@@ -100,7 +107,8 @@ module SimpleCov
100
107
  #
101
108
  def at_exit(&block)
102
109
  @at_exit = block if block
103
- return @at_exit if @at_exit
110
+ configured = @at_exit
111
+ return configured if configured
104
112
  return proc {} unless active_session?
105
113
 
106
114
  @at_exit = proc do
@@ -144,7 +152,8 @@ module SimpleCov
144
152
  # SimpleCov.root, capitalized with underscores → spaces.
145
153
  #
146
154
  def project_name(new_name = nil)
147
- return @project_name if defined?(@project_name) && @project_name && new_name.nil?
155
+ current = defined?(@project_name) ? @project_name : nil
156
+ return current if current && new_name.nil?
148
157
 
149
158
  @project_name = new_name if new_name.is_a?(String)
150
159
  @project_name ||= File.basename(root).capitalize.tr("_", " ")
@@ -156,7 +165,7 @@ module SimpleCov
156
165
  # of ours that would be clobbered. Returns the saved values for
157
166
  # later restoration.
158
167
  def swap_ivars_from(block_context)
159
- saved = {}
168
+ saved = {} # : Hash[Symbol, untyped]
160
169
  our_ivars = instance_variables
161
170
  block_context.instance_variables.each do |ivar|
162
171
  saved[ivar] = instance_variable_get(ivar) if our_ivars.include?(ivar)
@@ -14,9 +14,12 @@ module SimpleCov
14
14
  class CoverageStatistics
15
15
  attr_reader :total, :covered, :missed, :omitted, :strength, :percent
16
16
 
17
+ # Seed for the reduce in `.from`: covered, missed, omitted, strength.
18
+ ZERO_STATS = [0, 0, 0, 0.0].freeze #: [Integer, Integer, Integer, Float]
19
+
17
20
  def self.from(coverage_statistics)
18
21
  sum_covered, sum_missed, sum_omitted, sum_total_strength =
19
- coverage_statistics.reduce([0, 0, 0, 0.0]) do |(covered, missed, omitted, total_strength), stats|
22
+ coverage_statistics.reduce(ZERO_STATS) do |(covered, missed, omitted, total_strength), stats|
20
23
  [
21
24
  covered + stats.covered,
22
25
  missed + stats.missed,
@@ -46,7 +46,8 @@ module SimpleCov
46
46
  def minimum_by_group(result, thresholds)
47
47
  thresholds.flat_map do |group_name, minimums|
48
48
  group = lookup_group(result, group_name)
49
- group ? group_minimum_violations(group_name, group, minimums) : []
49
+ none = [] # : Array[Hash[Symbol, untyped]]
50
+ group ? group_minimum_violations(group_name, group, minimums) : none
50
51
  end
51
52
  end
52
53
 
@@ -41,12 +41,12 @@ loop do
41
41
  # it are intercepted and converted to configuration, with a deprecation
42
42
  # warning. See `SimpleCov.with_dot_simplecov_autoload` and issue #581.
43
43
  SimpleCov.with_dot_simplecov_autoload do
44
- load filename
45
- rescue LoadError, StandardError
44
+ load filename.to_s
45
+ rescue LoadError, StandardError => e
46
46
  # simplecov:disable — only fires when .simplecov is unreadable
47
47
  # or raises during load
48
48
  warn "Warning: Error occurred while trying to load #{filename}. " \
49
- "Error message: #{$ERROR_INFO.message}"
49
+ "Error message: #{e.message}"
50
50
  # simplecov:enable
51
51
  end
52
52
  break
@@ -55,8 +55,11 @@ module SimpleCov
55
55
  # An unclosed `disable` block extends to the end of the file.
56
56
  def self.disabled_ranges(src_lines)
57
57
  lines = src_lines.to_a
58
- ranges = CATEGORIES.to_h { |category| [category, []] }
59
- open_starts = {}
58
+ ranges = CATEGORIES.to_h do |category|
59
+ empty = [] # : Array[Range[Integer]]
60
+ [category, empty]
61
+ end
62
+ open_starts = {} # : Hash[Symbol, Integer]
60
63
 
61
64
  directives_in(lines).each { |directive| directive.apply(ranges, open_starts) }
62
65
  open_starts.each { |category, start| ranges[category] << (start..lines.size) }
@@ -91,7 +94,9 @@ module SimpleCov
91
94
 
92
95
  new(
93
96
  line_number: line_number,
94
- mode: match[:mode].to_sym,
97
+ # `to_s` is a no-op: the pattern requires the `mode` group, so it
98
+ # is present whenever `match` is; it also satisfies the type checker.
99
+ mode: match[:mode].to_s.to_sym,
95
100
  categories: parse_categories(match[:categories]),
96
101
  inline: inline?(lines, line_number, column + match.begin(0))
97
102
  )