rubocop-rspec_parity 2.0.0 → 2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +18 -1
- data/lib/rubocop/cop/rspec_parity/public_method_has_spec.rb +61 -5
- data/lib/rubocop/rspec_parity/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 888fb83446806d8a28fecfc45aaeef7c72183e95214cc9e12b63d3f98e0c746a
|
|
4
|
+
data.tar.gz: 56d460393929a8ca3e990cef75441f49fa0be2cf0c814e151ab911b0392e2162
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4dbc71657523c23118ff2bd823991ceea9aa635b02f35e3da8324e3e0dafc851c263af485991e9897ebf4efbaffc15518099a1c6a2ed69c84b290968229e3d4a
|
|
7
|
+
data.tar.gz: ce9de4bb85bf234ea735755b65b28ba6021eb5fd99a054a483c18e4c82d67d37ab51454b77e7750a5aa15ba163eb62e243ac3478f7766ea0676ba7744e1b365d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.0.2] - 2026-06-18
|
|
4
|
+
|
|
5
|
+
Fixed: `PublicMethodHasSpec` now treats a public class method and a public instance method that share a name (the service-object `def self.call` delegating to `def call`) as one logical method — a single `describe '.call'` or `describe '#call'` satisfies both instead of each requiring its own.
|
|
6
|
+
Fixed: `PublicMethodHasSpec` offense message no longer repeats a describe alias (e.g. `describe '#call' or describe '.call' or describe '.call'`) when a configured alias collides with the flexible-prefix expansion.
|
|
7
|
+
|
|
8
|
+
## [2.0.1] - 2026-06-18
|
|
9
|
+
|
|
10
|
+
Fixed: `PublicMethodHasSpec` relaxed validation for single-public-method classes (e.g. service objects) no longer passes when the spec's only method-style `describe`/`context` covers a different method — if a method describe is present it must describe the actual public method.
|
|
11
|
+
|
|
3
12
|
## [2.0.0] - 2026-06-18
|
|
4
13
|
|
|
5
14
|
Added: `SufficientContexts` now pinpoints which branch is untested — its message names one uncovered branch and the `# rspec_parity:covers <branch>` annotation to add, and the bundled `rspec-parity-cover` executable lists all of a method's gaps as paste-ready context stubs. Annotations are opt-in (new `CoversAnnotations` config key, default `true`).
|
data/README.md
CHANGED
|
@@ -249,7 +249,24 @@ context 'when staff' do # rspec_parity:covers user.staff?
|
|
|
249
249
|
end
|
|
250
250
|
```
|
|
251
251
|
|
|
252
|
-
Re-run and the message advances to the next uncovered branch. Annotations are opt-in and only ever raise coverage — they never create a new violation
|
|
252
|
+
Re-run and the message advances to the next uncovered branch. Annotations are opt-in and only ever raise coverage — they never create a new violation. A mistyped label is reported with a did-you-mean suggestion.
|
|
253
|
+
|
|
254
|
+
Each annotation covers exactly one branch, and the normal expectation is one annotation per context — one context, one scenario, one branch. In rare cases a single context genuinely exercises several branches at once; you can then list more than one branch on it, either as separate comments or with a `;`-separated list:
|
|
255
|
+
|
|
256
|
+
```ruby
|
|
257
|
+
context 'when fully privileged' do
|
|
258
|
+
# rspec_parity:covers user.admin?
|
|
259
|
+
# rspec_parity:covers user.staff?
|
|
260
|
+
it { is_expected.to be_allowed }
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# or, equivalently, on one line:
|
|
264
|
+
context 'when fully privileged' do # rspec_parity:covers user.admin?; user.staff?
|
|
265
|
+
it { is_expected.to be_allowed }
|
|
266
|
+
end
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Reach for this only when the branches really are covered together — multiple annotations on a context that only tests one path inflate coverage and defeat the point of the check.
|
|
253
270
|
|
|
254
271
|
Long conditions can push the comment past `Layout/LineLength`; exempt these comments rather than editing the label:
|
|
255
272
|
|
|
@@ -29,7 +29,7 @@ module RuboCop
|
|
|
29
29
|
return if inside_inner_class?(node)
|
|
30
30
|
|
|
31
31
|
instance_method = !inside_eigenclass?(node) && !inside_class_methods_block?(node)
|
|
32
|
-
flexible_prefix = instance_method && module_with_dual_access?(node)
|
|
32
|
+
flexible_prefix = (instance_method && module_with_dual_access?(node)) || dual_scope_method?(node)
|
|
33
33
|
check_method_has_spec(node, instance_method: instance_method, flexible_prefix: flexible_prefix)
|
|
34
34
|
end
|
|
35
35
|
|
|
@@ -38,7 +38,7 @@ module RuboCop
|
|
|
38
38
|
return if EXCLUDED_HOOK_METHODS.include?(node.method_name.to_s)
|
|
39
39
|
return if inside_inner_class?(node)
|
|
40
40
|
|
|
41
|
-
check_method_has_spec(node, instance_method: false)
|
|
41
|
+
check_method_has_spec(node, instance_method: false, flexible_prefix: dual_scope_method?(node))
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
private
|
|
@@ -151,6 +151,42 @@ module RuboCop
|
|
|
151
151
|
node.each_ancestor.find { |n| n.class_type? || n.module_type? }
|
|
152
152
|
end
|
|
153
153
|
|
|
154
|
+
# A class method and an instance method that share a name (the common
|
|
155
|
+
# service-object `def self.call` -> `new(...).call` plus the `def call`
|
|
156
|
+
# it delegates to) describe one logical operation. A single
|
|
157
|
+
# `describe '.call'` or `describe '#call'` should satisfy both, so we
|
|
158
|
+
# check the method with a flexible `#`/`.` prefix.
|
|
159
|
+
def dual_scope_method?(node)
|
|
160
|
+
class_node = find_class_or_module(node)
|
|
161
|
+
return false unless class_node&.body
|
|
162
|
+
|
|
163
|
+
instance_names, class_names = scope_method_names(class_node)
|
|
164
|
+
name = node.method_name
|
|
165
|
+
instance_names.include?(name) && class_names.include?(name)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def scope_method_names(class_node)
|
|
169
|
+
instance = []
|
|
170
|
+
klass = []
|
|
171
|
+
scope_children(class_node).each { |child| classify_scope_method(child, instance, klass) }
|
|
172
|
+
[instance, klass]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def classify_scope_method(child, instance, klass)
|
|
176
|
+
case child&.type
|
|
177
|
+
when :def then instance << child.method_name
|
|
178
|
+
when :defs then klass << child.method_name if child.children.first&.self_type?
|
|
179
|
+
when :sclass then collect_eigenclass_method_names(child, klass)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def collect_eigenclass_method_names(node, klass)
|
|
184
|
+
return unless node.children.first&.self_type? && node.body
|
|
185
|
+
|
|
186
|
+
body_children = node.body.begin_type? ? node.body.children : [node.body]
|
|
187
|
+
body_children.each { |child| klass << child.method_name if child.def_type? }
|
|
188
|
+
end
|
|
189
|
+
|
|
154
190
|
def targeted_visibility(scope, method_name)
|
|
155
191
|
return nil unless scope.body
|
|
156
192
|
|
|
@@ -219,8 +255,9 @@ module RuboCop
|
|
|
219
255
|
|
|
220
256
|
# Check if relaxed validation applies
|
|
221
257
|
if matches_skip_path? && count_public_methods(node) == 1
|
|
222
|
-
# For single-method classes in configured paths,
|
|
223
|
-
|
|
258
|
+
# For single-method classes in configured paths, the method describe is optional —
|
|
259
|
+
# but if one is present, it must describe the actual public method, not a private/random one.
|
|
260
|
+
return if relaxed_spec_valid?(spec_paths, class_name, method_name, instance_method, flexible_prefix)
|
|
224
261
|
elsif spec_paths.any? do |sp|
|
|
225
262
|
spec_covers_method?(sp, method_name, instance_method, flexible_prefix: flexible_prefix)
|
|
226
263
|
end
|
|
@@ -233,6 +270,25 @@ module RuboCop
|
|
|
233
270
|
end
|
|
234
271
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
235
272
|
|
|
273
|
+
# Relaxed validation for single-public-method classes in skip paths.
|
|
274
|
+
# Passes when the spec describes the class with examples AND either has no
|
|
275
|
+
# method-style describe/context block at all, or has one that covers the public method.
|
|
276
|
+
def relaxed_spec_valid?(spec_paths, class_name, method_name, instance_method, flexible_prefix)
|
|
277
|
+
return false unless spec_paths.any? { |sp| spec_has_examples?(sp, class_name) }
|
|
278
|
+
return true unless spec_paths.any? { |sp| spec_has_method_describe?(sp) }
|
|
279
|
+
|
|
280
|
+
spec_paths.any? do |sp|
|
|
281
|
+
spec_covers_method?(sp, method_name, instance_method, flexible_prefix: flexible_prefix)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Detects a method-style block, e.g. describe '#foo' / context '.bar'
|
|
286
|
+
# (the char right after the quote is a `#` or `.` prefix), as opposed to a
|
|
287
|
+
# plain descriptive string or a class constant.
|
|
288
|
+
def spec_has_method_describe?(spec_path)
|
|
289
|
+
File.read(spec_path).match?(/(?:describe|context)\s+['"][#.][^'"]+['"]/)
|
|
290
|
+
end
|
|
291
|
+
|
|
236
292
|
def spec_covers_method?(spec_path, method_name, instance_method, flexible_prefix: false)
|
|
237
293
|
return true if method_tested_in_spec?(spec_path, method_name, instance_method)
|
|
238
294
|
return true if flexible_prefix && method_tested_in_spec?(spec_path, method_name, !instance_method)
|
|
@@ -265,7 +321,7 @@ module RuboCop
|
|
|
265
321
|
describes << "describe '#{alias_desc}'"
|
|
266
322
|
end
|
|
267
323
|
end
|
|
268
|
-
describes.join(" or ")
|
|
324
|
+
describes.uniq.join(" or ")
|
|
269
325
|
end
|
|
270
326
|
|
|
271
327
|
def prefixes_for(instance_method, flexible_prefix)
|