simplecov 1.0.0.rc3 → 1.0.0.rc4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eda0f749acc9b248635b6b1128c601b28bd77304994f78ebbbec0fc9097a2969
4
- data.tar.gz: 8d55b00892b8f2cd47f7d3960c52e5d177c4951a77cbcd4af42282ae7ec745fa
3
+ metadata.gz: e60ecd48ca2790532fba8cdf76aaf6f29f18dd08c27fdc4d349988d2d60d6cd6
4
+ data.tar.gz: 600d7ca6449e36825de427633b8b6890cd77f292ad661c4a86b325e3424081bd
5
5
  SHA512:
6
- metadata.gz: 4cafbbdb4eb3ee18f0c6fad85b865e5522d7e5b86d8d154a42b716a61bfbf57a734d2a0381f95d0d59cd6b914d910f559054725ba92ed6c3f1388421299d824e
7
- data.tar.gz: 79c267679cbf76017bbbc2541ab239a226145c2b798fc75731492f7600edafc9efe7c56e3e5a769392b4a0dff7b1a3a8d808af8a9d341b6ab26b6c19b1676a6a
6
+ metadata.gz: 49db7a5cf640b63fd877832f67580a15ea384a79ce6f4778be807474170b69dde752c58f60729ce5ba2e2a9d3266a53704ff70cbf76b4f70c697d28f4553e200
7
+ data.tar.gz: e69d632cb5f3e0a4b8d7e65c9bbfae5bb6b779df1b37c28227374ca799a9a57910438a0bc6c32b1e34785e04db70e19ce53db33771ddf7d2bb5d4cf9c9dccb5f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ 1.0.0.rc4 (2026-06-26)
2
+ ======================
3
+
4
+ ## Enhancements
5
+ * Added `SimpleCov.finalize_merge` to separate storing mergeable worker resultsets from owning final report finalization. Parallel workers that write to explicit custom coverage destinations can now store their shard `.resultset.json` files without waiting on sibling shards they cannot see; an explicit `SimpleCov.collate` cleanup step then formats the merged report, enforces thresholds, and writes `.last_run.json`. SimpleCov infers this external-finalization mode only for recognized multi-worker parallel runs with merging enabled and a custom coverage destination, and emits a configuration warning until users set `finalize_merge false` (or `true`) explicitly. See #1215.
6
+
7
+ ## Bugfixes
8
+ * The `parallel_tests` adapter now only activates and uses the native wait API when the native pid-file synchronization contract is present. Processes that inherit `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` without `PARALLEL_PID_FILE`, or lose `PARALLEL_PID_FILE` before SimpleCov's `at_exit` hook runs, now use the generic resultset polling path instead of calling `ParallelTests.wait_for_other_processes_to_finish` and failing when `parallel_tests` fetches the missing pid-file path. See #1210.
9
+ * The default `at_exit` formatter now writes reports only from the final parallel-test worker while still storing each worker's resultset for the final merge, so JSON/XML/HTML formatters no longer clobber canonical coverage files from non-final workers. See #1210.
10
+ * `SimpleCov.parallel_tests false` now disables the generic `TEST_ENV_NUMBER` adapter as well as the `parallel_tests` gem adapter, so projects that use those environment variables for a different coverage collation flow can opt out consistently. See #1208.
11
+ * Parallel result coordination now stores the final worker's own resultset before waiting for sibling resultsets, preventing an off-by-one timeout where the final worker reported `N-1` of `N` workers and skipped threshold checks immediately before producing a complete merged report. See #1208.
12
+ * Static branch coverage now matches Ruby's runtime branch tuple identities for `unless` and safe-navigation calls, and resultset merges now combine serialized branch tuples by source location instead of by their local sequential ids. This prevents equivalent branches from being duplicated when static and runtime branch extraction assign different ids. See #1206.
13
+
1
14
  1.0.0.rc3 (2026-06-18)
2
15
  ======================
3
16
 
data/README.md CHANGED
@@ -160,7 +160,8 @@ This is recommended whenever you merge frameworks that rely on each other, like
160
160
  > Calling `SimpleCov.start` directly from `.simplecov` is deprecated. Tracking still begins for backward
161
161
  > compatibility, but a one-time deprecation warning fires; a future release will require the explicit `SimpleCov.start`
162
162
  > from a test helper. Migrating prevents a long-standing bug where `.simplecov` auto-loaded in a Rakefile or Rails'
163
- > `Bundler.require` would leave an empty parent-process report that overwrites the test subprocess's good one. See #581.
163
+ > `Bundler.require` would leave an empty parent-process report that overwrites the test subprocess's good one. See
164
+ > [#581](https://github.com/simplecov-ruby/simplecov/issues/581).
164
165
 
165
166
  ### Changing the report location
166
167
 
@@ -217,8 +218,8 @@ Brand-new in the redesigned API (no legacy method to migrate from):
217
218
  |-------------------------------------|--------------------------------------------------------------------------------------------------------------------------|
218
219
  | `cover "lib/**/*.rb"` | Positive scope (allowlist). Multiple calls union; strings are globs. See above for the relationship with `track_files`. |
219
220
  | `no_default_skips` | Clear every previously-installed filter — defaults and anything earlier in the block — so subsequent `skip`s start clean.|
220
- | `formatter false` / `formatters []` | Opt out of formatting entirely. Workers in big parallel CI runs only need their `.resultset.json` for a final `SimpleCov.collate` step; skipping the formatter saves the per-job HTML / multi-formatter overhead. See #964. |
221
- | `parallel_tests true` / `false` | Force on / off the auto-require of the `parallel_tests` gem. Default (unset) auto-detects from `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` and silently skips if the gem isn't installed. Set explicitly when you use those env vars for unrelated subprocess coordination. See #1018. |
221
+ | `formatter false` / `formatters []` | Opt out of formatting entirely. Workers in big parallel CI runs only need their `.resultset.json` for a final `SimpleCov.collate` step; skipping the formatter saves the per-job HTML / multi-formatter overhead. See [#964](https://github.com/simplecov-ruby/simplecov/issues/964). |
222
+ | `parallel_tests true` / `false` | Force on / off the auto-require of the `parallel_tests` gem. Default (unset) auto-detects from `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` and silently skips if the gem isn't installed. Set explicitly when you use those env vars for unrelated subprocess coordination. See [#1018](https://github.com/simplecov-ruby/simplecov/issues/1018). |
222
223
 
223
224
  Example before/after:
224
225
 
@@ -748,6 +749,51 @@ whatever has arrived, skipping the minimum / maximum coverage checks against tha
748
749
  much heavier test files and routinely finishes a minute or more after the others, raise it with
749
750
  `SimpleCov.parallel_wait_timeout 180` so its coverage is included.
750
751
 
752
+ ### Merge finalization ownership
753
+
754
+ `SimpleCov.merging true` stores each process' resultset so it can be merged with other suites or workers. By default,
755
+ SimpleCov also owns **finalizing** that merge: waiting for sibling workers, building the merged result, formatting the
756
+ report, enforcing minimum / maximum coverage, and writing `.last_run.json`.
757
+
758
+ Some parallel runners intentionally write each worker's resultset to a separate coverage directory and then run an
759
+ explicit cleanup step with `SimpleCov.collate`. In that setup, workers should still store their resultsets, but the
760
+ cleanup task owns finalization:
761
+
762
+ ```ruby
763
+ # spec/spec_helper.rb
764
+ SimpleCov.start do
765
+ if ENV["TEST_ENV_NUMBER"]
766
+ merging true
767
+ coverage_dir "coverage/turbo_tests/#{ENV["TEST_ENV_NUMBER"]}"
768
+ command_name "rspec-#{ENV["TEST_ENV_NUMBER"]}"
769
+ finalize_merge false
770
+ end
771
+ end
772
+ ```
773
+
774
+ ```ruby
775
+ # Rakefile
776
+ task "coverage:collate" do
777
+ require "simplecov"
778
+
779
+ SimpleCov.collate Dir["coverage/turbo_tests/*/.resultset.json"] do
780
+ coverage(:line) { minimum 100 }
781
+ coverage(:branch) { minimum 100 }
782
+ end
783
+ end
784
+ ```
785
+
786
+ When `finalize_merge false`, the worker writes its `.resultset.json` and exits without waiting for siblings, formatting,
787
+ checking thresholds, or writing `.last_run.json`. The `SimpleCov.collate` process is the finalizer and performs those
788
+ steps for the merged result.
789
+
790
+ For compatibility, SimpleCov infers `finalize_merge false` and prints a configuration warning only when all of these are
791
+ true: a recognized parallel adapter is active, more than one worker is expected, merging is enabled, the coverage
792
+ destination was explicitly changed from the default, and the process has parallel-worker environment variables. Set
793
+ `SimpleCov.finalize_merge false` to keep external collation ownership without the warning, or
794
+ `SimpleCov.finalize_merge true` if the selected worker should own the built-in wait / merge / report flow even with a
795
+ custom coverage destination.
796
+
751
797
  ### Merging across execution environments
752
798
 
753
799
  If your tests run in parallel across multiple build machines, download each run's `.resultset.json` and merge them into
@@ -866,14 +912,14 @@ SimpleCov coordinates with parallel test runners through a small pluggable adapt
866
912
 
867
913
  - **`ParallelTestsAdapter`** — wraps the [grosser/parallel_tests](https://github.com/grosser/parallel_tests) gem and
868
914
  uses its `ParallelTests.first_process?` / `ParallelTests.wait_for_other_processes_to_finish` APIs for precise worker
869
- coordination.
915
+ coordination. Activates only when the native `parallel_tests` pid-file contract is present.
870
916
  - **`GenericAdapter`** — catch-all for any runner that follows the `TEST_ENV_NUMBER` / `PARALLEL_TEST_GROUPS` env-var
871
917
  convention but doesn't ship a Ruby API (parallel_rspec, knapsack-style splitters, custom CI sharding scripts).
872
918
  Activates when `TEST_ENV_NUMBER` is set and no more-specific adapter is.
873
919
 
874
920
  Adapters are tried in registration order; the first whose `active?` returns `true` is chosen. With both built-ins, this
875
921
  means parallel_tests users get the precise gem-based path and parallel_rspec (or any env-var-only runner) gets the
876
- polling-based fallback without any configuration change. See #1065.
922
+ polling-based fallback without any configuration change. See [#1065](https://github.com/simplecov-ruby/simplecov/issues/1065).
877
923
 
878
924
  #### Registering a custom adapter
879
925
 
@@ -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
  #
@@ -22,11 +24,30 @@ module SimpleCov
22
24
  # @return [Hash]
23
25
  #
24
26
  def combine(coverage_a, coverage_b)
25
- coverage_a.merge(coverage_b) do |_condition, branches_inside_a, branches_inside_b|
26
- branches_inside_a.merge(branches_inside_b) do |_branch, a_count, b_count|
27
- a_count + b_count
27
+ combined = [coverage_a, coverage_b].each_with_object({}) do |coverage, memo|
28
+ coverage.each do |condition, branches_inside|
29
+ condition_key = tuple_identity(condition)
30
+ condition_tuple, merged_branches = memo[condition_key] ||= [condition, {}]
31
+ merge_branches(merged_branches, branches_inside)
32
+ memo[condition_key] = [condition_tuple, merged_branches]
28
33
  end
29
34
  end
35
+
36
+ combined.values.to_h { |condition, branches| [condition, branches.values.to_h] }
37
+ end
38
+
39
+ def merge_branches(target, source)
40
+ source.each do |branch, count|
41
+ branch_key = tuple_identity(branch)
42
+ branch_tuple, existing_count = target[branch_key]
43
+ target[branch_key] = [branch_tuple || branch, existing_count ? existing_count + count : count]
44
+ end
45
+ end
46
+
47
+ def tuple_identity(tuple)
48
+ tuple = SourceFile::RubyDataParser.call(tuple)
49
+ type, _id, start_line, start_column, end_line, end_column = tuple
50
+ [type, start_line, start_column, end_line, end_column]
30
51
  end
31
52
  end
32
53
  end
@@ -54,6 +54,38 @@ module SimpleCov
54
54
  @use_merging
55
55
  end
56
56
 
57
+ #
58
+ # Get or set whether this process owns final merge processing:
59
+ # waiting for sibling workers, building the merged result, formatting,
60
+ # enforcing thresholds, and writing `.last_run.json`.
61
+ #
62
+ # Defaults to true, except for recognized multi-worker parallel runs
63
+ # that explicitly write to a custom coverage destination while merging
64
+ # is enabled. Those runs are likely using an external `SimpleCov.collate`
65
+ # step to finalize the merge.
66
+ #
67
+ def finalize_merge(value = :__no_arg__)
68
+ unless value == :__no_arg__
69
+ @finalize_merge = value
70
+ @finalize_merge_explicit = true
71
+ end
72
+
73
+ return @finalize_merge if defined?(@finalize_merge_explicit) && @finalize_merge_explicit
74
+
75
+ inferred = inferred_finalize_merge?
76
+ warn_about_inferred_finalize_merge unless inferred
77
+ inferred
78
+ end
79
+
80
+ def finalize_merge?
81
+ finalize_merge
82
+ end
83
+
84
+ # @api private
85
+ def merge_finalization_owner?
86
+ collating_result? || finalize_merge?
87
+ end
88
+
57
89
  # DEPRECATED: alias for `merging`. Same value, same behavior.
58
90
  def use_merging(use = nil)
59
91
  SimpleCov::Deprecation.warn("`SimpleCov.use_merging` is deprecated. " \
@@ -83,5 +115,50 @@ module SimpleCov
83
115
  @parallel_wait_timeout = seconds if seconds.is_a?(Integer)
84
116
  @parallel_wait_timeout ||= 60
85
117
  end
118
+
119
+ private
120
+
121
+ def inferred_finalize_merge?
122
+ return true unless merging
123
+
124
+ adapter = SimpleCov::ParallelAdapters.current
125
+ return true unless adapter
126
+ return true unless adapter.expected_worker_count > 1
127
+ return true unless parallel_worker_environment?
128
+ return true unless explicit_custom_coverage_destination?
129
+
130
+ false
131
+ end
132
+
133
+ def parallel_worker_environment?
134
+ ENV.key?("TEST_ENV_NUMBER") || ENV.key?("PARALLEL_TEST_GROUPS")
135
+ end
136
+
137
+ def explicit_custom_coverage_destination?
138
+ return false unless explicit_coverage_destination?
139
+
140
+ coverage_path != File.expand_path("coverage", root)
141
+ end
142
+
143
+ def explicit_coverage_destination?
144
+ (defined?(@coverage_path_explicit) && @coverage_path_explicit) ||
145
+ (defined?(@coverage_dir_explicit) && @coverage_dir_explicit)
146
+ end
147
+
148
+ def warn_about_inferred_finalize_merge
149
+ return if defined?(@finalize_merge_inference_warned) && @finalize_merge_inference_warned
150
+ return unless print_errors
151
+
152
+ @finalize_merge_inference_warned = true
153
+ warn SimpleCov::Color.colorize(inferred_finalize_merge_warning, :yellow)
154
+ end
155
+
156
+ def inferred_finalize_merge_warning
157
+ "SimpleCov inferred `finalize_merge false` because this parallel worker is merging " \
158
+ "into a custom coverage destination. Set `SimpleCov.finalize_merge false` to keep " \
159
+ "external collation ownership, or `SimpleCov.finalize_merge true` if this worker " \
160
+ "should wait, merge, format, enforce thresholds, and write `.last_run.json`. " \
161
+ "See https://github.com/simplecov-ruby/simplecov#merge-finalization-ownership."
162
+ end
86
163
  end
87
164
  end
@@ -31,6 +31,7 @@ module SimpleCov
31
31
  return @coverage_dir if defined?(@coverage_dir) && dir.nil?
32
32
 
33
33
  @coverage_path = nil unless @coverage_path_explicit # invalidate cache
34
+ @coverage_dir_explicit = true unless dir.nil?
34
35
  @coverage_dir = dir || "coverage"
35
36
  end
36
37
 
@@ -94,14 +95,18 @@ module SimpleCov
94
95
 
95
96
  #
96
97
  # Gets or sets the behavior to process coverage results.
97
- # By default, it calls SimpleCov.result.format!
98
+ # By default, it stores/merges the current result and formats only
99
+ # from the final reporting process.
98
100
  #
99
101
  def at_exit(&block)
100
102
  @at_exit = block if block
101
103
  return @at_exit if @at_exit
102
104
  return proc {} unless active_session?
103
105
 
104
- @at_exit = proc { SimpleCov.result.format! }
106
+ @at_exit = proc do
107
+ result = SimpleCov.result
108
+ result.format! if result && SimpleCov.merge_finalization_owner? && SimpleCov.final_result_process?
109
+ end
105
110
  end
106
111
 
107
112
  # Whether SimpleCov has anything to do at exit: the Coverage module
@@ -105,15 +105,16 @@ module SimpleCov
105
105
  Kernel.exit(exit_status)
106
106
  end
107
107
 
108
- # @api private — the first worker in a parallel run is the only
109
- # one that reports against thresholds, and only when its
108
+ # @api private — the process that owns final merge processing is the
109
+ # only one that reports against thresholds, and only when its
110
110
  # `wait_for_other_processes` confirmed every sibling reported.
111
111
  # When the wait times out, the merged total is partial and
112
112
  # comparing it against `minimum_coverage` / `maximum_coverage`
113
113
  # would surface a spurious "below minimum" violation about the
114
114
  # missing slice rather than a real shortfall.
115
115
  def ready_to_process_results?
116
- final_result_process? && result? && parallel_results_complete?
116
+ merge_finalization_owner? && final_result_process? && result? &&
117
+ (collating_result? || parallel_results_complete?)
117
118
  end
118
119
 
119
120
  def process_results_and_report_error
@@ -23,6 +23,8 @@ module SimpleCov
23
23
  class GenericAdapter < Base
24
24
  class << self
25
25
  def active?
26
+ return false if SimpleCov.parallel_tests == false
27
+
26
28
  ENV.key?("TEST_ENV_NUMBER")
27
29
  end
28
30
 
@@ -7,16 +7,20 @@ module SimpleCov
7
7
  # Adapter for [grosser/parallel_tests](https://github.com/grosser/parallel_tests).
8
8
  # This is the historical default — SimpleCov has special-cased
9
9
  # parallel_tests since 0.18 — and remains the most precise option for
10
- # projects on it. Detection is the standard pair: the `ParallelTests`
11
- # constant has been loaded AND `TEST_ENV_NUMBER` is set. The gem itself
12
- # is autoloaded lazily on first `active?` check so users who don't have
13
- # it installed see no warnings (see #1018).
10
+ # projects on it. Detection requires the full native coordination
11
+ # contract: the `ParallelTests` constant has been loaded,
12
+ # `TEST_ENV_NUMBER` is set, and `PARALLEL_PID_FILE` is set. The pid-file
13
+ # path is required because the native wait API reads it with `ENV.fetch`.
14
+ # When a runner only provides the env-var convention, GenericAdapter is
15
+ # the correct coordination path.
14
16
  class ParallelTestsAdapter < Base
15
17
  class << self
16
18
  def active?
19
+ return false if SimpleCov.parallel_tests == false
20
+
17
21
  ensure_loaded
18
22
  # !! to coerce `defined?` (returns nil or "constant") to a proper bool.
19
- !!(defined?(::ParallelTests) && ENV.key?("TEST_ENV_NUMBER"))
23
+ !!(defined?(::ParallelTests) && native_parallel_tests_environment?)
20
24
  end
21
25
 
22
26
  # Pick the *first* started process to do the final-result work,
@@ -34,6 +38,8 @@ module SimpleCov
34
38
  end
35
39
 
36
40
  def wait_for_siblings
41
+ return unless native_parallel_tests_environment?
42
+
37
43
  ::ParallelTests.wait_for_other_processes_to_finish
38
44
  end
39
45
 
@@ -71,6 +77,10 @@ module SimpleCov
71
77
  def env_suggests_parallel_tests?
72
78
  ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_TEST_GROUPS")
73
79
  end
80
+
81
+ def native_parallel_tests_environment?
82
+ ENV.key?("TEST_ENV_NUMBER") && ENV.key?("PARALLEL_PID_FILE")
83
+ end
74
84
  end
75
85
  end
76
86
  end
@@ -21,7 +21,10 @@ module SimpleCov
21
21
  # Use the ResultMerger to produce a single, merged result, ready to use.
22
22
  @result = ResultMerger.merge_and_store(*result_filenames, ignore_timeout: ignore_timeout)
23
23
 
24
+ @collating_result = true
24
25
  run_exit_tasks!
26
+ ensure
27
+ @collating_result = false
25
28
  end
26
29
 
27
30
  #
@@ -41,8 +44,10 @@ module SimpleCov
41
44
  # If we're using merging of results, store the current result
42
45
  # first (if there is one), then merge the results and return those
43
46
  if use_merging
44
- wait_for_other_processes
45
47
  SimpleCov::ResultMerger.store_result(@result) if result?
48
+ return @result unless finalize_merge?
49
+
50
+ wait_for_other_processes
46
51
  @result = SimpleCov::ResultMerger.merged_result
47
52
  end
48
53
 
@@ -54,6 +59,11 @@ module SimpleCov
54
59
  defined?(@result) && @result
55
60
  end
56
61
 
62
+ # @api private — true while `SimpleCov.collate` is running its finalizer.
63
+ def collating_result?
64
+ defined?(@collating_result) && @collating_result
65
+ end
66
+
57
67
  # Applies the configured filters to the given array of SimpleCov::SourceFile items
58
68
  def filtered(files)
59
69
  result = files.clone
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleCov
4
+ module StaticCoverageExtractor
5
+ # Visitor mixin that collects method tuples and tracks the lexical
6
+ # class / module nesting that names them, in the shape Ruby's
7
+ # `Coverage` reports methods. Mixed into `Visitor`, it shares that
8
+ # visitor's `@methods` / `@class_stack` state and keeps the
9
+ # method-collection concern separate from branch extraction.
10
+ module MethodCollector
11
+ # Track class/module nesting so method tuples carry the lexical
12
+ # class name. Module + Class are both treated as namespaces here
13
+ # since `Coverage` reports both as the constant.
14
+ def visit_class_node(node)
15
+ with_class(constant_name(node.constant_path)) { super }
16
+ end
17
+
18
+ def visit_module_node(node)
19
+ with_class(constant_name(node.constant_path)) { super }
20
+ end
21
+
22
+ # `def name(...)` and `def self.name(...)` both produce DefNode.
23
+ # The class context is the surrounding lexical class/module (or
24
+ # `Object` at the top level, matching `Coverage`'s convention).
25
+ def visit_def_node(node)
26
+ loc = node.location
27
+ class_name = @class_stack.last || "Object"
28
+ key = [class_name, node.name, loc.start_line, loc.start_column, loc.end_line, loc.end_column]
29
+ @methods[key] = 0
30
+ super
31
+ end
32
+
33
+ private
34
+
35
+ # Render a constant path (e.g., `Foo::Bar`) as its source-form
36
+ # string. Defensive nil / to_s fallbacks: ClassNode and ModuleNode
37
+ # always carry a constant_path in practice.
38
+ # simplecov:disable
39
+ def constant_name(node)
40
+ return "<anonymous>" if node.nil?
41
+ return node.slice if node.respond_to?(:slice)
42
+
43
+ node.to_s
44
+ end
45
+ # simplecov:enable
46
+
47
+ def with_class(name)
48
+ @class_stack.push(name)
49
+ yield
50
+ ensure
51
+ @class_stack.pop
52
+ end
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "method_collector"
4
+
3
5
  module SimpleCov
4
6
  module StaticCoverageExtractor
5
7
  # `Prism::IfNode#subsequent` was renamed from `consequent` in Prism
@@ -24,6 +26,10 @@ module SimpleCov
24
26
  # conventional shape. Only defined when Prism is loadable;
25
27
  # `StaticCoverageExtractor.available?` is the runtime gate.
26
28
  class Visitor < ::Prism::Visitor
29
+ # Method tuples and the class/module nesting that names them are
30
+ # collected by this mixin; this class focuses on branch extraction.
31
+ include MethodCollector
32
+
27
33
  attr_reader :branches, :methods
28
34
 
29
35
  def initialize
@@ -41,12 +47,17 @@ module SimpleCov
41
47
  # missing, Coverage synthesizes a `:else` arm attributed to the
42
48
  # whole condition's range — we do the same.
43
49
  def visit_if_node(node)
44
- emit_if_like(node)
50
+ emit_if_like(node, :if)
45
51
  super
46
52
  end
47
53
 
48
54
  def visit_unless_node(node)
49
- emit_if_like(node)
55
+ emit_if_like(node, :unless)
56
+ super
57
+ end
58
+
59
+ def visit_call_node(node)
60
+ emit_safe_navigation(node) if node.respond_to?(:safe_navigation?) && node.safe_navigation?
50
61
  super
51
62
  end
52
63
 
@@ -75,42 +86,28 @@ module SimpleCov
75
86
  super
76
87
  end
77
88
 
78
- # Track class/module nesting so method tuples carry the lexical
79
- # class name. Module + Class are both treated as namespaces here
80
- # since `Coverage` reports both as the constant.
81
- def visit_class_node(node)
82
- with_class(constant_name(node.constant_path)) { super }
83
- end
84
-
85
- def visit_module_node(node)
86
- with_class(constant_name(node.constant_path)) { super }
87
- end
88
-
89
- # `def name(...)` and `def self.name(...)` both produce DefNode.
90
- # The class context is the surrounding lexical class/module (or
91
- # `Object` at the top level, matching `Coverage`'s convention).
92
- def visit_def_node(node)
93
- loc = node.location
94
- class_name = @class_stack.last || "Object"
95
- key = [class_name, node.name, loc.start_line, loc.start_column, loc.end_line, loc.end_column]
96
- @methods[key] = 0
97
- super
98
- end
99
-
100
89
  private
101
90
 
102
91
  # IfNode and UnlessNode share a shape (predicate + then body +
103
92
  # optional else/elsif) but expose the trailing arm under different
104
93
  # accessors. `if_like_else_location` hides that split.
105
- def emit_if_like(node)
94
+ def emit_if_like(node, type)
106
95
  then_loc = arm_location(node.statements, node.location)
107
96
  else_loc = if_like_else_location(node)
108
- @branches[build_tuple(:if, node.location)] = {
97
+ @branches[build_tuple(type, node.location)] = {
109
98
  build_tuple(:then, then_loc) => 0,
110
99
  build_tuple(:else, else_loc) => 0
111
100
  }
112
101
  end
113
102
 
103
+ def emit_safe_navigation(node)
104
+ loc = node.location
105
+ @branches[build_tuple(:"&.", loc)] = {
106
+ build_tuple(:then, loc) => 0,
107
+ build_tuple(:else, loc) => 0
108
+ }
109
+ end
110
+
114
111
  # Resolve the source range Coverage attributes to a real-or-synthetic
115
112
  # `:else` arm of an if-like construct. IfNode uses
116
113
  # `subsequent` / `consequent` depending on Prism version (resolved
@@ -169,25 +166,6 @@ module SimpleCov
169
166
  @next_id += 1
170
167
  [type, id, location.start_line, location.start_column, location.end_line, location.end_column]
171
168
  end
172
-
173
- # Render a constant path (e.g., `Foo::Bar`) as its source-form
174
- # string. Defensive nil / to_s fallbacks: ClassNode and ModuleNode
175
- # always carry a constant_path in practice.
176
- # simplecov:disable
177
- def constant_name(node)
178
- return "<anonymous>" if node.nil?
179
- return node.slice if node.respond_to?(:slice)
180
-
181
- node.to_s
182
- end
183
- # simplecov:enable
184
-
185
- def with_class(name)
186
- @class_stack.push(name)
187
- yield
188
- ensure
189
- @class_stack.pop
190
- end
191
169
  end
192
170
  end
193
171
  end
@@ -79,13 +79,14 @@ module SimpleCov
79
79
  # methods: Set[[name, start_line], ...] # e.g., [[:foo, 7], [:bar, 13]]
80
80
  # }
81
81
  #
82
- # Branch matching is start_line-only because Coverage's condition type
83
- # vocabulary (`:if`, `:unless`, `:case`, `:while`, `:until`) does not
84
- # always match Prism's emitted type (the existing visitor reports
85
- # `:if` for `unless` and ternary). Coincidental line-sharing between
86
- # a real branch and an eval-generated one will keep both, which is
87
- # an acceptable false-negative for an opt-in filter. Method matching
88
- # uses (name, start_line) since a method name is unique at any line.
82
+ # Branch matching is start_line-only rather than by the full tuple.
83
+ # Static extraction and Coverage can still disagree on a branch's exact
84
+ # column positions (and, for some constructs, its type), so matching on
85
+ # start_line alone is the conservative choice that tolerates those
86
+ # differences. Coincidental line-sharing between a real branch and an
87
+ # eval-generated one will keep both, which is an acceptable
88
+ # false-negative for an opt-in filter. Method matching uses
89
+ # (name, start_line) since a method name is unique at any line.
89
90
  #
90
91
  # Returns nil when Prism is unavailable or parsing fails, signaling
91
92
  # callers to keep every Coverage entry (no false drops).
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCov
4
- VERSION = "1.0.0.rc3"
4
+ VERSION = "1.0.0.rc4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplecov
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc3
4
+ version: 1.0.0.rc4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Erik Berlin
@@ -138,6 +138,7 @@ files:
138
138
  - lib/simplecov/source_file/source_loader.rb
139
139
  - lib/simplecov/source_file/statistics.rb
140
140
  - lib/simplecov/static_coverage_extractor.rb
141
+ - lib/simplecov/static_coverage_extractor/method_collector.rb
141
142
  - lib/simplecov/static_coverage_extractor/visitor.rb
142
143
  - lib/simplecov/useless_results_remover.rb
143
144
  - lib/simplecov/version.rb
@@ -150,9 +151,9 @@ licenses:
150
151
  metadata:
151
152
  bug_tracker_uri: https://github.com/simplecov-ruby/simplecov/issues
152
153
  changelog_uri: https://github.com/simplecov-ruby/simplecov/blob/main/CHANGELOG.md
153
- documentation_uri: https://www.rubydoc.info/gems/simplecov/1.0.0.rc3
154
+ documentation_uri: https://www.rubydoc.info/gems/simplecov/1.0.0.rc4
154
155
  mailing_list_uri: https://groups.google.com/forum/#!forum/simplecov
155
- source_code_uri: https://github.com/simplecov-ruby/simplecov/tree/v1.0.0.rc3
156
+ source_code_uri: https://github.com/simplecov-ruby/simplecov/tree/v1.0.0.rc4
156
157
  rubygems_mfa_required: 'true'
157
158
  rdoc_options: []
158
159
  require_paths:
@@ -168,7 +169,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
168
169
  - !ruby/object:Gem::Version
169
170
  version: '0'
170
171
  requirements: []
171
- rubygems_version: 4.0.14
172
+ rubygems_version: 4.0.15
172
173
  specification_version: 4
173
174
  summary: Code coverage for Ruby
174
175
  test_files: []