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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f70e06a85873dc993a895443df9bab8892de60d0cc75573ffdc6487245c09dcf
|
|
4
|
+
data.tar.gz: 7d8baed7d58b13d98fb7b7e14084c3c81dcb1905df3bacfc52772dcb65ba7dbd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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(
|
|
32
|
-
return Result.new(
|
|
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(
|
|
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]),
|
|
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[:
|
|
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
|
-
|
|
64
|
-
return unless
|
|
65
|
+
tally = branch_counter.call(@methods[key][:node])
|
|
66
|
+
return unless tally.total.positive?
|
|
65
67
|
|
|
66
|
-
state[:
|
|
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
|
-
|
|
105
|
+
tally = branch_tally(node)
|
|
88
106
|
traced_methods = []
|
|
89
107
|
if @trace_single_use_private
|
|
90
108
|
extra = inlined_branches(node)
|
|
91
|
-
|
|
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(
|
|
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(:
|
|
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
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|