simplecov 1.0.0 → 1.0.1

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 (51) 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/combine/branches_combiner.rb +2 -1
  11. data/lib/simplecov/combine/results_combiner.rb +2 -1
  12. data/lib/simplecov/configuration/coverage.rb +6 -2
  13. data/lib/simplecov/configuration/coverage_criteria.rb +3 -1
  14. data/lib/simplecov/configuration/formatting.rb +17 -7
  15. data/lib/simplecov/configuration/thresholds.rb +13 -9
  16. data/lib/simplecov/configuration.rb +22 -13
  17. data/lib/simplecov/coverage_statistics.rb +4 -1
  18. data/lib/simplecov/coverage_violations.rb +2 -1
  19. data/lib/simplecov/defaults.rb +3 -3
  20. data/lib/simplecov/directive.rb +8 -3
  21. data/lib/simplecov/exit_codes/maximum_coverage_drop_check.rb +1 -1
  22. data/lib/simplecov/exit_codes/maximum_overall_coverage_check.rb +1 -1
  23. data/lib/simplecov/exit_codes/minimum_coverage_by_file_check.rb +1 -1
  24. data/lib/simplecov/exit_codes/minimum_coverage_by_group_check.rb +1 -1
  25. data/lib/simplecov/exit_codes/minimum_overall_coverage_check.rb +3 -3
  26. data/lib/simplecov/exit_codes.rb +12 -0
  27. data/lib/simplecov/exit_handling.rb +3 -3
  28. data/lib/simplecov/file_list.rb +12 -5
  29. data/lib/simplecov/formatter/base.rb +2 -1
  30. data/lib/simplecov/formatter/html_formatter.rb +13 -7
  31. data/lib/simplecov/formatter/json_formatter/errors_formatter.rb +9 -3
  32. data/lib/simplecov/formatter/json_formatter/result_hash_formatter.rb +1 -1
  33. data/lib/simplecov/formatter/json_formatter/source_file_formatter.rb +1 -1
  34. data/lib/simplecov/formatter/json_formatter.rb +4 -2
  35. data/lib/simplecov/formatter/multi_formatter.rb +7 -2
  36. data/lib/simplecov/formatter/simple_formatter.rb +3 -1
  37. data/lib/simplecov/process.rb +22 -0
  38. data/lib/simplecov/result_adapter.rb +2 -1
  39. data/lib/simplecov/result_processing.rb +4 -4
  40. data/lib/simplecov/simulate_coverage.rb +2 -2
  41. data/lib/simplecov/source_file/line.rb +2 -2
  42. data/lib/simplecov/source_file/method.rb +1 -1
  43. data/lib/simplecov/source_file/method_builder.rb +3 -1
  44. data/lib/simplecov/source_file/source_loader.rb +2 -2
  45. data/lib/simplecov/source_file.rb +2 -2
  46. data/lib/simplecov/static_coverage_extractor/location_conventions.rb +158 -0
  47. data/lib/simplecov/static_coverage_extractor/visitor.rb +9 -45
  48. data/lib/simplecov/version.rb +1 -1
  49. data/lib/simplecov.rb +24 -11
  50. data/sig/simplecov.rbs +1638 -0
  51. metadata +5 -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: 14c29bfd96c91bb39e5893969a5f35a320e0d3b23b3ae47161aabc21a182f69f
4
+ data.tar.gz: 2436a11c76e48df0df5c7432e1c1163f0bf36f9090e04d035017c99769e74b7d
5
5
  SHA512:
6
- metadata.gz: 5f97c41bad62008a68ad3fb02919be7728e1f0c1f61049ff2d47f57455ef4d86530271ad4a7f7704b022ffd44366f0c41ebe4f051631ce5d67acff7b9e31a9c2
7
- data.tar.gz: 5aeb18029dcb5aafad9dc878aa218113cbe26643db1bf8a1f5b1eabe09855ba0a91605a68c874bbc11f38d969d2c972090a6888ef1692d6f8c7d8bce0124d0fb
6
+ metadata.gz: 13bdde3f26f987d864caf3c2a861c479cac8e03c12fcf12752aad70b0ca55793e1dc529490d5ad9fc6250c780d6131fd78264ddaf0ca3158ae4cea5ffdb3b3b5
7
+ data.tar.gz: 21239f67622ae18b306783d6a355dd728247128ece00935601334b82f4f7f8948150768d0ac949c1db5c859c23187b4a9f568b7a45a49bf5a7a1ab40a8178951
@@ -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)
@@ -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, {}]
@@ -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
  )
@@ -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