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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2bda6248c84f44ce1e039baac11b9e6b3075c3feb13bb4e06a9e804ff26436a6
4
- data.tar.gz: ebc0e696c427bd033637609d7f1ad4aaa66c603a9ca1ed82adb09d2857c99161
3
+ metadata.gz: 888fb83446806d8a28fecfc45aaeef7c72183e95214cc9e12b63d3f98e0c746a
4
+ data.tar.gz: 56d460393929a8ca3e990cef75441f49fa0be2cf0c814e151ab911b0392e2162
5
5
  SHA512:
6
- metadata.gz: 102aceedd06b6db82be97c0a45d95a57e22cb1b1ab7427875c6137f98dcfcc87b862cd2cc26dfc14d8f5604a24702166db12b6f94c1835f7e0395fd98082d556
7
- data.tar.gz: ca83d65bb73322941c49160de66943b2e1c6c93f35db372ef8cb559e5ea2cf3bdf1d3cc31151e654c8d19b620ceb1f4ee2f6dbf5ef8a8213277bd243c7104111
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, and each one covers exactly one branch. A mistyped label is reported with a did-you-mean suggestion.
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, just check for examples
223
- return if spec_paths.any? { |spec_path| spec_has_examples?(spec_path, class_name) }
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module RSpecParity
5
- VERSION = "2.0.0"
5
+ VERSION = "2.0.2"
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: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Povilas Jurcys