rubocop-rspec_parity 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2fc4286fcc69b46b490ae21db9ce9681ca7dd25577521e329d0bc3d89b33940a
4
- data.tar.gz: da7872a16a143f8d44a178c7b1912996c8d5213fb1538a8f4231e9ceedf361dc
3
+ metadata.gz: f70e06a85873dc993a895443df9bab8892de60d0cc75573ffdc6487245c09dcf
4
+ data.tar.gz: 7d8baed7d58b13d98fb7b7e14084c3c81dcb1905df3bacfc52772dcb65ba7dbd
5
5
  SHA512:
6
- metadata.gz: 3d9f782158d6c053256401c6b687eb8b22dd1780f444b42783609b428ea0f82898a33e5c502818df86be8e56847a7be8012a99ead9c3aaffe91e5420d7be5a66
7
- data.tar.gz: 44b7a03a46444bcacbec096e659fd435dc288278ba1f14d72d87311d1bca1b4feb32643c68c6fcb3ff2fbb5be1118adddddb8e4ec164b4d710cc00efbb287bc5
6
+ metadata.gz: 66ae9ef49f40f4323af10a733c9c1eb6bbc7a49883b3f00619fda6610c22b035ca53aa9e6b259259df8c7a5cb49aded12d56e8b96082074036f4fd9dc960baf3
7
+ data.tar.gz: 18635158b5155856049fdbfd33aaeb732b013d729467f1aefd8751873f997411b78a4d1489fa692ef64b0030691a6141b8ab6c6ca28e95398ea5d87201d0b3e7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.7.0] - 2026-06-11
4
+
5
+ Fixed: `SufficientContexts` no longer over-counts chains of guard clauses (`return`/`raise`/`next ... if/unless`). A sequence of guards shares a single "all guards pass" fall-through, so that happy path is counted once for the whole method instead of once per guard. Each `&&`/`||` inside a guard condition is still counted as a distinct way for the guard to fire (one scenario per operand).
6
+
3
7
  ## [1.6.0] - 2026-06-11
4
8
 
5
9
  Fixed: `SufficientContexts` now counts each `it`/`example` within a single context as a separate scenario, so specs that cover branches with multiple examples (instead of separate contexts) no longer trigger false violations
@@ -8,7 +8,9 @@ module RuboCop
8
8
  # private/protected helpers that are called from exactly one place in
9
9
  # the same container.
10
10
  class PrivateMethodCallGraph # rubocop:disable Metrics/ClassLength
11
- Result = Struct.new(:branches, :traced_methods)
11
+ # `branch_tally` is whatever the branch_counter returns (a BranchTally),
12
+ # or nil when nothing was inlined. It must respond to `+` and `total`.
13
+ Result = Struct.new(:branch_tally, :traced_methods)
12
14
 
13
15
  DYNAMIC_DISPATCH_SENDS = %i[send public_send __send__].freeze
14
16
  EVAL_METHODS = %i[class_eval instance_eval module_eval].freeze
@@ -28,13 +30,13 @@ module RuboCop
28
30
  end
29
31
 
30
32
  def inlinable_from(method_node, branch_counter)
31
- return Result.new(0, []) unless @container
32
- return Result.new(0, []) if dynamic_dispatch?
33
+ return Result.new(nil, []) unless @container
34
+ return Result.new(nil, []) if dynamic_dispatch?
33
35
 
34
36
  build! unless @built
35
37
 
36
38
  key = key_for(method_node)
37
- return Result.new(0, []) unless @methods.key?(key)
39
+ return Result.new(nil, []) unless @methods.key?(key)
38
40
 
39
41
  traverse(key, branch_counter)
40
42
  end
@@ -42,7 +44,7 @@ module RuboCop
42
44
  private
43
45
 
44
46
  def traverse(start_key, branch_counter)
45
- state = { visited: Set.new([start_key]), total: 0, traced: [] }
47
+ state = { visited: Set.new([start_key]), tally: nil, traced: [] }
46
48
  stack = callees_of(start_key)
47
49
  until stack.empty?
48
50
  key = stack.shift
@@ -51,7 +53,7 @@ module RuboCop
51
53
  visit_callee(key, branch_counter, state)
52
54
  stack.concat(callees_of(key))
53
55
  end
54
- Result.new(state[:total], sorted_names(state[:traced]))
56
+ Result.new(state[:tally], sorted_names(state[:traced]))
55
57
  end
56
58
 
57
59
  def callees_of(key)
@@ -60,10 +62,10 @@ module RuboCop
60
62
 
61
63
  def visit_callee(key, branch_counter, state)
62
64
  state[:visited] << key
63
- count = branch_counter.call(@methods[key][:node])
64
- return unless count.positive?
65
+ tally = branch_counter.call(@methods[key][:node])
66
+ return unless tally.total.positive?
65
67
 
66
- state[:total] += count
68
+ state[:tally] = state[:tally] ? state[:tally] + tally : tally
67
69
  state[:traced] << key
68
70
  end
69
71
 
@@ -63,6 +63,24 @@ module RuboCop
63
63
  # Tallies extracted from a spec's text for a single method describe block.
64
64
  ParsedSpec = Struct.new(:context_count, :example_count, :has_examples, :has_direct_examples)
65
65
 
66
+ # Branch tally split into guard-clause "fire" branches vs. every other
67
+ # branch. A sequence of guard clauses (`return/raise/next ... if/unless`)
68
+ # shares a single "all guards pass" fall-through, so that happy path is
69
+ # counted once for the whole method (see #branches_from) rather than once
70
+ # per guard — which previously inflated guard-heavy methods.
71
+ BranchTally = Struct.new(:guard, :regular) do
72
+ def +(other)
73
+ BranchTally.new(guard + other.guard, regular + other.regular)
74
+ end
75
+
76
+ def total
77
+ guard + regular
78
+ end
79
+ end
80
+
81
+ # Node types whose presence as a one-armed `if` body makes it a guard clause.
82
+ GUARD_TERMINATOR_TYPES = %i[return break next redo retry].freeze
83
+
66
84
  def initialize(config = nil, options = nil)
67
85
  super
68
86
  @ignore_memoization = cop_config.fetch("IgnoreMemoization", true)
@@ -84,13 +102,14 @@ module RuboCop
84
102
  return unless in_covered_directory?
85
103
  return if excluded_method?(method_name(node))
86
104
 
87
- branches = count_branches(node)
105
+ tally = branch_tally(node)
88
106
  traced_methods = []
89
107
  if @trace_single_use_private
90
108
  extra = inlined_branches(node)
91
- branches += extra.branches
109
+ tally += extra.branch_tally if extra.branch_tally
92
110
  traced_methods = extra.traced_methods
93
111
  end
112
+ branches = branches_from(tally)
94
113
  return if branches < 2 # Only check methods with branches
95
114
 
96
115
  class_name = extract_class_name(node)
@@ -134,10 +153,10 @@ module RuboCop
134
153
 
135
154
  def inlined_branches(node)
136
155
  container = find_class_or_module(node)
137
- return PrivateMethodCallGraph::Result.new(0, []) unless container
156
+ return PrivateMethodCallGraph::Result.new(nil, []) unless container
138
157
 
139
158
  graph = (@call_graphs[container] ||= PrivateMethodCallGraph.new(container))
140
- graph.inlinable_from(node, method(:count_branches))
159
+ graph.inlinable_from(node, method(:branch_tally))
141
160
  end
142
161
 
143
162
  def method_name(node)
@@ -163,17 +182,75 @@ module RuboCop
163
182
  processed_source.path
164
183
  end
165
184
 
166
- def count_branches(node)
167
- branches = 0
185
+ # Total scenario count from a tally. Guard clauses share one "all guards
186
+ # pass" fall-through; count it once, but only when guards are the sole
187
+ # branching — otherwise the happy path is already represented by the
188
+ # other branches and adding it would over-count.
189
+ def branches_from(tally)
190
+ total = tally.total
191
+ total += 1 if tally.guard.positive? && tally.regular.zero?
192
+ total
193
+ end
194
+
195
+ def branch_tally(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
196
+ guard = 0
197
+ regular = 0
168
198
  elsif_nodes = collect_elsif_nodes(node)
199
+ guard_operators = collect_guard_condition_operators(node)
169
200
 
170
201
  node.each_descendant do |descendant|
171
202
  next if elsif_nodes.include?(descendant)
172
203
  next if should_skip_node?(descendant)
173
204
 
174
- branches += branch_count_for_node(descendant)
205
+ case descendant.type
206
+ when :if
207
+ guard_clause?(descendant) ? guard += 1 : regular += count_if_branches(descendant)
208
+ when :case then regular += count_case_branches(descendant)
209
+ when :and, :or then guard_operators.include?(descendant) ? guard += 1 : regular += 1
210
+ when :or_asgn, :and_asgn then regular += 2 # ||= and &&= create 2 branches (set vs already set)
211
+ when :send then regular += send_node_branch_count(descendant)
212
+ end
175
213
  end
176
- branches
214
+
215
+ BranchTally.new(guard, regular)
216
+ end
217
+
218
+ # A guard clause is a one-armed `if`/`unless` whose single body exits the
219
+ # method early (`return`/`raise`/`next`/`break`/...). Its non-exit path is
220
+ # the shared fall-through, so it is not a branch of its own.
221
+ def guard_clause?(node)
222
+ return false unless node.if_type?
223
+
224
+ present = [node.if_branch, node.else_branch].compact
225
+ return false unless present.size == 1
226
+
227
+ guard_terminator?(present.first)
228
+ end
229
+
230
+ def guard_terminator?(node)
231
+ return false unless node
232
+ return true if GUARD_TERMINATOR_TYPES.include?(node.type)
233
+ return true if node.send_type? && (node.method?(:raise) || node.method?(:throw))
234
+ return guard_terminator?(node.children.last) if node.begin_type?
235
+
236
+ false
237
+ end
238
+
239
+ # The `&&`/`||` nodes that live inside a guard clause's condition. Each
240
+ # such operator is a distinct way for the guard to fire (one scenario per
241
+ # operand), so it counts as a guard branch rather than an "other" branch.
242
+ def collect_guard_condition_operators(node)
243
+ operators = Set.new
244
+ node.each_descendant(:if) do |if_node|
245
+ next unless guard_clause?(if_node)
246
+
247
+ condition = if_node.condition
248
+ next unless condition
249
+
250
+ operators.add(condition) if condition.and_type? || condition.or_type?
251
+ condition.each_descendant(:and, :or) { |op| operators.add(op) }
252
+ end
253
+ operators
177
254
  end
178
255
 
179
256
  def collect_elsif_nodes(node)
@@ -188,17 +265,6 @@ module RuboCop
188
265
  @ignore_memoization && memoization_pattern?(node)
189
266
  end
190
267
 
191
- def branch_count_for_node(node)
192
- case node.type
193
- when :if then count_if_branches(node)
194
- when :case then count_case_branches(node)
195
- when :and, :or then 1
196
- when :or_asgn, :and_asgn then 2 # ||= and &&= create 2 branches (set vs already set)
197
- when :send then send_node_branch_count(node)
198
- else 0
199
- end
200
- end
201
-
202
268
  def send_node_branch_count(node)
203
269
  node.method?(:&) || node.method?(:|) ? 1 : 0
204
270
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module RSpecParity
5
- VERSION = "1.6.0"
5
+ VERSION = "1.7.0"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-rspec_parity
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Povilas Jurcys