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 +4 -4
- data/CHANGELOG.md +4 -0
- data/lib/rubocop/cop/rspec_parity/department_config.rb +47 -1
- data/lib/rubocop/cop/rspec_parity/public_method_has_spec.rb +41 -19
- data/lib/rubocop/cop/rspec_parity/sufficient_contexts.rb +12 -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: a1801f19d786c13a744e801e1ddcfaab61d6c690a4a98868600b18181fbaedab
|
|
4
|
+
data.tar.gz: 465d0ff6d064d1f8c65b6c8e8af60336ad78d75d21204228ebe4c4049c3e6bf2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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?
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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
|
|
250
|
-
describes = [
|
|
251
|
-
|
|
252
|
-
describes << "describe '#{
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|