rubocop-rspec_parity 1.4.4 → 1.4.5

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: 6f0344e2a2bf2511a1bc8739c4ccf449a59526efde141ea4d9209c07b1330be1
4
- data.tar.gz: a5432ea9890d24e15815ddc8cdfcae8762cdd65b0de0ba9a77ab876d5cc20e53
3
+ metadata.gz: a1801f19d786c13a744e801e1ddcfaab61d6c690a4a98868600b18181fbaedab
4
+ data.tar.gz: 465d0ff6d064d1f8c65b6c8e8af60336ad78d75d21204228ebe4c4049c3e6bf2
5
5
  SHA512:
6
- metadata.gz: 85859f89d3a1703a93be6165112cf703a3fbbaf76b32ac0a5b813a6cc585f7351786e58e476a6cb06a4ac70f4576107ebc1914b09dddece59c418c862b2fbc07
7
- data.tar.gz: 62ed3e43cd3a38c9e71beb8e4f69b4800136102d878820eb2f5e679efa9a1dd01e2f11ba1a1f49d7a427d9f0c707fabb887249aa25e72221f37d2cfd98e8e46b
6
+ metadata.gz: 748e44075554a6f07b71992264df4a36a00c50fb5c4e53775d8668e911d3e5d8243de1b81d539fcf29896c8e24cd09b5dce3508b3736c6ac1878e4e404adf11c
7
+ data.tar.gz: 29f5dff55dc313461b945e26c73c6f4a81d72a41ff3de9721a830440536f770b6f3ed15ff0f65029126db8de933c33650244eb2cbba0bc7d5b83e96ee66755a2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.4.5] - 2026-03-27
4
+
5
+ Added: Allow both `#method` and `.method` notation in specs for modules using `extend self`, `module_function`, or `module_function :method_name`
6
+
3
7
  ## [1.4.4] - 2026-03-18
4
8
 
5
9
  Removed: Hardcoded directory lists in `PublicMethodHasSpec` and `SufficientContexts` cops; file selection now relies entirely on department-level `Include`/`Exclude` config
@@ -6,7 +6,7 @@ module RuboCop
6
6
  # Shared module for reading department-level configuration.
7
7
  # Provides config resolution (cop-level > department-level > default),
8
8
  # spec file path mappings, and shared describe aliases / skip paths.
9
- module DepartmentConfig
9
+ module DepartmentConfig # rubocop:disable Metrics/ModuleLength
10
10
  SHARED_CONFIG_DEFAULTS = {
11
11
  "SpecFilePathMappings" => { "app/" => ["spec/"] },
12
12
  "DescribeAliases" => {},
@@ -98,6 +98,52 @@ module RuboCop
98
98
  end
99
99
  nil
100
100
  end
101
+
102
+ # Detects if a method is inside a module that makes instance methods
103
+ # callable as module-level methods (e.g., extend self, module_function).
104
+ def module_with_dual_access?(node)
105
+ enclosing = find_class_or_module(node)
106
+ return false unless enclosing&.module_type?
107
+
108
+ extend_self?(enclosing) || module_function_applies?(enclosing, node)
109
+ end
110
+
111
+ def extend_self?(module_node)
112
+ return false unless module_node.body
113
+
114
+ module_body_children(module_node).any? do |child|
115
+ child.send_type? && child.method_name == :extend && child.arguments.size == 1 &&
116
+ child.first_argument.self_type?
117
+ end
118
+ end
119
+
120
+ def module_function_applies?(module_node, target_node)
121
+ return false unless module_node.body
122
+
123
+ children = module_body_children(module_node)
124
+
125
+ targeted_module_function?(children, target_node) || section_module_function?(children, target_node)
126
+ end
127
+
128
+ def targeted_module_function?(children, target_node)
129
+ children.any? do |child|
130
+ child.send_type? && child.method_name == :module_function &&
131
+ child.arguments.any? { |arg| arg.sym_type? && arg.value == target_node.method_name }
132
+ end
133
+ end
134
+
135
+ def section_module_function?(children, target_node)
136
+ section_active = false
137
+ children.each do |child|
138
+ section_active = true if child.send_type? && child.method_name == :module_function && child.arguments.empty?
139
+ return true if section_active && child == target_node
140
+ end
141
+ false
142
+ end
143
+
144
+ def module_body_children(module_node)
145
+ module_node.body.begin_type? ? module_node.body.children : [module_node.body]
146
+ end
101
147
  end
102
148
  end
103
149
  end
@@ -28,7 +28,9 @@ module RuboCop
28
28
  return unless checkable_method?(node) && public_method?(node)
29
29
  return if inside_inner_class?(node)
30
30
 
31
- check_method_has_spec(node, instance_method: !inside_eigenclass?(node) && !inside_class_methods_block?(node))
31
+ instance_method = !inside_eigenclass?(node) && !inside_class_methods_block?(node)
32
+ flexible_prefix = instance_method && module_with_dual_access?(node)
33
+ check_method_has_spec(node, instance_method: instance_method, flexible_prefix: flexible_prefix)
32
34
  end
33
35
 
34
36
  def on_defs(node)
@@ -197,7 +199,7 @@ module RuboCop
197
199
  end
198
200
 
199
201
  # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
200
- def check_method_has_spec(node, instance_method:)
202
+ def check_method_has_spec(node, instance_method:, flexible_prefix: false)
201
203
  class_name = extract_class_name(node)
202
204
  return unless class_name
203
205
 
@@ -208,7 +210,10 @@ module RuboCop
208
210
  if spec_paths.empty?
209
211
  # No spec file exists — public method is untested
210
212
  report_path = all_expected.first
211
- add_method_offense(node, method_name, report_path, instance_method: instance_method) if report_path
213
+ if report_path
214
+ add_method_offense(node, method_name, report_path, instance_method: instance_method,
215
+ flexible_prefix: flexible_prefix)
216
+ end
212
217
  return
213
218
  end
214
219
 
@@ -216,44 +221,61 @@ module RuboCop
216
221
  if matches_skip_path? && count_public_methods(node) == 1
217
222
  # For single-method classes in configured paths, just check for examples
218
223
  return if spec_paths.any? { |spec_path| spec_has_examples?(spec_path, class_name) }
219
- elsif spec_paths.any? { |spec_path| spec_covers_method?(spec_path, method_name, instance_method) }
224
+ elsif spec_paths.any? do |sp|
225
+ spec_covers_method?(sp, method_name, instance_method, flexible_prefix: flexible_prefix)
226
+ end
220
227
  # Normal validation: check for method describe
221
228
  return
222
229
  end
223
230
 
224
- add_method_offense(node, method_name, spec_paths.first, instance_method: instance_method)
231
+ add_method_offense(node, method_name, spec_paths.first, instance_method: instance_method,
232
+ flexible_prefix: flexible_prefix)
225
233
  end
226
234
  # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
227
235
 
228
- def spec_covers_method?(spec_path, method_name, instance_method)
236
+ def spec_covers_method?(spec_path, method_name, instance_method, flexible_prefix: false)
229
237
  return true if method_tested_in_spec?(spec_path, method_name, instance_method)
238
+ return true if flexible_prefix && method_tested_in_spec?(spec_path, method_name, !instance_method)
230
239
 
231
- prefix = instance_method ? "#" : "."
232
- describe_key = "#{prefix}#{method_name}"
233
- describe_aliases_for(describe_key).any? do |alias_desc|
234
- alias_name = alias_desc.sub(/^[#.]/, "")
235
- alias_instance = !alias_desc.start_with?(".")
236
- method_tested_in_spec?(spec_path, alias_name, alias_instance)
240
+ spec_covers_via_aliases?(spec_path, method_name, instance_method, flexible_prefix)
241
+ end
242
+
243
+ def spec_covers_via_aliases?(spec_path, method_name, instance_method, flexible_prefix)
244
+ prefixes_for(instance_method, flexible_prefix).any? do |prefix|
245
+ describe_aliases_for("#{prefix}#{method_name}").any? do |alias_desc|
246
+ alias_name = alias_desc.sub(/^[#.]/, "")
247
+ method_tested_in_spec?(spec_path, alias_name, !alias_desc.start_with?("."))
248
+ end
237
249
  end
238
250
  end
239
251
 
240
- def add_method_offense(node, method_name, spec_path, instance_method:)
241
- prefix = instance_method ? "#" : "."
242
- expected = expected_describes(prefix, method_name)
252
+ def add_method_offense(node, method_name, spec_path, instance_method:, flexible_prefix: false)
253
+ expected = expected_describes_for(method_name, instance_method, flexible_prefix)
243
254
  add_offense(
244
255
  node.loc.keyword.join(node.loc.name),
245
256
  message: format(MSG, method_name: method_name, expected: expected, spec_path: relative_spec_path(spec_path))
246
257
  )
247
258
  end
248
259
 
249
- def expected_describes(prefix, method_name)
250
- describes = ["describe '#{prefix}#{method_name}'"]
251
- describe_aliases_for("#{prefix}#{method_name}").each do |alias_desc|
252
- describes << "describe '#{alias_desc}'"
260
+ def expected_describes_for(method_name, instance_method, flexible_prefix)
261
+ describes = []
262
+ prefixes_for(instance_method, flexible_prefix).each do |prefix|
263
+ describes << "describe '#{prefix}#{method_name}'"
264
+ describe_aliases_for("#{prefix}#{method_name}").each do |alias_desc|
265
+ describes << "describe '#{alias_desc}'"
266
+ end
253
267
  end
254
268
  describes.join(" or ")
255
269
  end
256
270
 
271
+ def prefixes_for(instance_method, flexible_prefix)
272
+ if flexible_prefix
273
+ ["#", "."]
274
+ else
275
+ [instance_method ? "#" : "."]
276
+ end
277
+ end
278
+
257
279
  def method_tested_in_spec?(spec_path, method_name, instance_method)
258
280
  spec_content = File.read(spec_path)
259
281
  prefix = instance_method ? "#" : "."
@@ -84,13 +84,17 @@ module RuboCop
84
84
  spec_files = find_valid_spec_files(class_name, expected_spec_paths)
85
85
  return if spec_files.empty?
86
86
 
87
+ dual_access = node.def_type? && module_with_dual_access?(node)
88
+
87
89
  # Aggregate contexts from all valid spec files
88
90
  contexts = if matches_skip_path? && count_public_methods(node) == 1
89
91
  # For single-method classes, count top-level contexts instead
90
92
  spec_files.sum { |spec_file| count_top_level_contexts(File.read(spec_file), class_name) }
91
93
  else
92
94
  # Normal path: look for method describes
93
- spec_files.sum { |spec_file| count_method_contexts(File.read(spec_file), method_name(node)) }
95
+ spec_files.sum do |spec_file|
96
+ count_method_contexts(File.read(spec_file), method_name(node), dual_access: dual_access)
97
+ end
94
98
  end
95
99
 
96
100
  return if contexts.zero? # Method has no specs at all - PublicMethodHasSpec handles this
@@ -189,11 +193,14 @@ module RuboCop
189
193
  when_count + (has_else ? 1 : 0)
190
194
  end
191
195
 
192
- def count_method_contexts(spec_content, mname)
196
+ def count_method_contexts(spec_content, mname, dual_access: false)
193
197
  count = count_contexts_for_method(spec_content, mname)
194
- describe_aliases_for("##{mname}").each do |alias_desc|
195
- alias_name = alias_desc.sub(/^[#.]/, "")
196
- count += count_contexts_for_method(spec_content, alias_name) if alias_name != mname
198
+ prefixes = dual_access ? ["#", "."] : ["#"]
199
+ prefixes.each do |prefix|
200
+ describe_aliases_for("#{prefix}#{mname}").each do |alias_desc|
201
+ alias_name = alias_desc.sub(/^[#.]/, "")
202
+ count += count_contexts_for_method(spec_content, alias_name) if alias_name != mname
203
+ end
197
204
  end
198
205
  count
199
206
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module RSpecParity
5
- VERSION = "1.4.4"
5
+ VERSION = "1.4.5"
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.4.4
4
+ version: 1.4.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Povilas Jurcys