rubocop-rspec_parity 1.3.4 → 1.4.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 +4 -4
- data/CHANGELOG.md +9 -0
- data/config/default.yml +8 -4
- data/lib/rubocop/cop/rspec_parity/department_config.rb +95 -0
- data/lib/rubocop/cop/rspec_parity/file_has_spec.rb +42 -0
- data/lib/rubocop/cop/rspec_parity/public_method_has_spec.rb +15 -28
- data/lib/rubocop/cop/rspec_parity/spec_file_finder.rb +14 -12
- data/lib/rubocop/cop/rspec_parity/sufficient_contexts.rb +5 -12
- data/lib/rubocop/rspec_parity/version.rb +1 -1
- data/lib/rubocop_rspec_parity.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6a5de925a1a0fb2a5857e4c6b8e021abeac3fe0e7f792c536497db7468382375
|
|
4
|
+
data.tar.gz: 32c20a277a217ffa24a011ae7fa178ea81c5db9d99a74076686d56504d243043
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 177de764d463e227cf080453bec0d526a339f8c86dc10cb75a86a6d03b7bc49cfb5a69837949285b9e5e9d016e355b1eb0b4b72e5e8beb80b3fef07bd98fd5dd
|
|
7
|
+
data.tar.gz: d3dd4830f5c4640f2af0fa407552b0c81bd0fac8d5a933498eebd4b6c4536138cf06f92a903024de08977dadb55ebcd4b530e4e7bdaa0108bb6cfa380e12fc05
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.4.0] - 2026-03-03
|
|
4
|
+
|
|
5
|
+
Added: `FileHasSpec` cop that checks each app file has a corresponding spec file
|
|
6
|
+
Added: Department-level shared configuration (`SpecFilePathMappings`, `DescribeAliases`, `SkipMethodDescribeFor`) for all RSpecParity cops
|
|
7
|
+
|
|
8
|
+
## [1.3.5] - 2026-03-02
|
|
9
|
+
|
|
10
|
+
Fixed: `PublicMethodHasSpec` now correctly recognizes `private` declarations inside `included do`, `class_eval do`, and `module_eval do` blocks as visibility scopes
|
|
11
|
+
|
|
3
12
|
## [1.3.4] - 2026-02-23
|
|
4
13
|
|
|
5
14
|
Added: `PublicMethodHasSpec` now treats methods inside `class_methods do` blocks (ActiveSupport::Concern) as class methods, expecting `.method_name` in specs
|
data/config/default.yml
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Default configuration for rubocop-rspec_parity
|
|
2
2
|
|
|
3
|
+
# Department-level shared configuration for all RSpecParity cops.
|
|
4
|
+
# Cop-level settings override these when explicitly set per-cop.
|
|
5
|
+
RSpecParity:
|
|
6
|
+
SpecFilePathMappings:
|
|
7
|
+
'app/': ['spec/']
|
|
8
|
+
DescribeAliases: {}
|
|
9
|
+
SkipMethodDescribeFor: []
|
|
10
|
+
|
|
3
11
|
RSpecParity/FileHasSpec:
|
|
4
12
|
Description: 'Checks that each Ruby file in the app directory has a corresponding spec file.'
|
|
5
13
|
Enabled: true
|
|
@@ -13,8 +21,6 @@ RSpecParity/FileHasSpec:
|
|
|
13
21
|
RSpecParity/PublicMethodHasSpec:
|
|
14
22
|
Description: 'Checks that each public method has a corresponding spec test.'
|
|
15
23
|
Enabled: true
|
|
16
|
-
SkipMethodDescribeFor: []
|
|
17
|
-
DescribeAliases: {}
|
|
18
24
|
Include:
|
|
19
25
|
- 'app/**/*.rb'
|
|
20
26
|
Exclude:
|
|
@@ -26,8 +32,6 @@ RSpecParity/SufficientContexts:
|
|
|
26
32
|
Description: 'Ensures specs have at least as many contexts as the method has branches.'
|
|
27
33
|
Enabled: true
|
|
28
34
|
IgnoreMemoization: true
|
|
29
|
-
SkipMethodDescribeFor: []
|
|
30
|
-
DescribeAliases: {}
|
|
31
35
|
Include:
|
|
32
36
|
- 'app/**/*.rb'
|
|
33
37
|
Exclude:
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module RSpecParity
|
|
6
|
+
# Shared module for reading department-level configuration.
|
|
7
|
+
# Provides config resolution (cop-level > department-level > default),
|
|
8
|
+
# spec file path mappings, and shared describe aliases / skip paths.
|
|
9
|
+
module DepartmentConfig
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def department_config
|
|
13
|
+
@department_config ||= config["RSpecParity"] || {}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def spec_file_path_mappings
|
|
17
|
+
resolve_config("SpecFilePathMappings", { "app/" => ["spec/"] })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def shared_describe_aliases
|
|
21
|
+
resolve_config("DescribeAliases", {})
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def shared_skip_method_describe_paths
|
|
25
|
+
resolve_config("SkipMethodDescribeFor", [])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def expected_spec_paths(source_path = nil) # rubocop:disable Metrics/MethodLength
|
|
29
|
+
source_path ||= processed_source.file_path
|
|
30
|
+
return [] unless source_path
|
|
31
|
+
|
|
32
|
+
paths = []
|
|
33
|
+
spec_file_path_mappings.each do |source_dir, spec_dirs|
|
|
34
|
+
next unless path_matches_mapping?(source_path, source_dir)
|
|
35
|
+
|
|
36
|
+
Array(spec_dirs).each do |spec_dir|
|
|
37
|
+
spec_path = substitute_path(source_path, source_dir, spec_dir)
|
|
38
|
+
paths << spec_path if spec_path
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
paths.uniq
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def matches_any_mapping?(source_path)
|
|
45
|
+
spec_file_path_mappings.any? do |source_dir, _|
|
|
46
|
+
path_matches_mapping?(source_path, source_dir)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def relative_spec_path(spec_path)
|
|
51
|
+
source_path = processed_source.file_path
|
|
52
|
+
return spec_path unless source_path
|
|
53
|
+
|
|
54
|
+
root = find_project_root(source_path)
|
|
55
|
+
root ? spec_path.sub("#{root}/", "") : spec_path
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def resolve_config(key, default)
|
|
59
|
+
if cop_config.key?(key)
|
|
60
|
+
cop_config[key]
|
|
61
|
+
elsif department_config.key?(key)
|
|
62
|
+
department_config[key]
|
|
63
|
+
else
|
|
64
|
+
default
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def path_matches_mapping?(source_path, source_dir)
|
|
69
|
+
source_path.include?("/#{source_dir}") || source_path.start_with?(source_dir)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def substitute_path(source_path, source_dir, spec_dir)
|
|
73
|
+
result = if source_path.include?("/#{source_dir}")
|
|
74
|
+
source_path.sub("/#{source_dir}", "/#{spec_dir}")
|
|
75
|
+
elsif source_path.start_with?(source_dir)
|
|
76
|
+
source_path.sub(source_dir, spec_dir)
|
|
77
|
+
end
|
|
78
|
+
result&.sub(/\.rb$/, "_spec.rb")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def find_project_root(source_path)
|
|
82
|
+
spec_file_path_mappings.each_key do |source_dir|
|
|
83
|
+
first_dir = source_dir.split("/").first
|
|
84
|
+
parts = source_path.split("/")
|
|
85
|
+
dir_index = parts.index(first_dir)
|
|
86
|
+
next unless dir_index&.positive?
|
|
87
|
+
|
|
88
|
+
return parts[0...dir_index].join("/")
|
|
89
|
+
end
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module RSpecParity
|
|
6
|
+
# Checks that each Ruby file in the app directory has a corresponding spec file.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad - app/models/user.rb exists but spec/models/user_spec.rb does not
|
|
10
|
+
#
|
|
11
|
+
# # good - app/models/user.rb has a matching spec/models/user_spec.rb
|
|
12
|
+
#
|
|
13
|
+
class FileHasSpec < Base
|
|
14
|
+
include DepartmentConfig
|
|
15
|
+
|
|
16
|
+
MSG = "Missing spec file. Expected %<spec_path>s to exist"
|
|
17
|
+
|
|
18
|
+
def on_new_investigation
|
|
19
|
+
return unless should_check_file?
|
|
20
|
+
|
|
21
|
+
spec_paths = expected_spec_paths
|
|
22
|
+
return if spec_paths.empty?
|
|
23
|
+
return if spec_paths.any? { |path| File.exist?(path) }
|
|
24
|
+
|
|
25
|
+
add_offense(
|
|
26
|
+
processed_source.ast || processed_source.tokens.first,
|
|
27
|
+
message: format(MSG, spec_path: relative_spec_path(spec_paths.first))
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def should_check_file?
|
|
34
|
+
path = processed_source.file_path
|
|
35
|
+
return false unless path && !path.end_with?("_spec.rb")
|
|
36
|
+
|
|
37
|
+
matches_any_mapping?(path)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -13,6 +13,7 @@ module RuboCop
|
|
|
13
13
|
# # good - public method `perform` has describe '#perform' in spec
|
|
14
14
|
#
|
|
15
15
|
class PublicMethodHasSpec < Base # rubocop:disable Metrics/ClassLength
|
|
16
|
+
include DepartmentConfig
|
|
16
17
|
include SpecFileFinder
|
|
17
18
|
|
|
18
19
|
MSG = "Missing spec for public method `%<method_name>s`. " \
|
|
@@ -24,12 +25,6 @@ module RuboCop
|
|
|
24
25
|
EXCLUDED_PATTERNS = [/^before_/, /^after_/, /^around_/, /^validate_/, /^autosave_/].freeze
|
|
25
26
|
VISIBILITY_METHODS = { private: :private, protected: :protected, public: :public }.freeze
|
|
26
27
|
|
|
27
|
-
def initialize(config = nil, options = nil)
|
|
28
|
-
super
|
|
29
|
-
@skip_method_describe_paths = cop_config.fetch("SkipMethodDescribeFor", [])
|
|
30
|
-
@describe_aliases = cop_config.fetch("DescribeAliases", {})
|
|
31
|
-
end
|
|
32
|
-
|
|
33
28
|
def on_def(node)
|
|
34
29
|
return unless checkable_method?(node) && public_method?(node)
|
|
35
30
|
return if inside_inner_class?(node)
|
|
@@ -134,7 +129,8 @@ module RuboCop
|
|
|
134
129
|
|
|
135
130
|
def find_enclosing_scope(node)
|
|
136
131
|
node.each_ancestor.find do |n|
|
|
137
|
-
n.class_type? || n.module_type? || n.sclass_type? || class_methods_block?(n)
|
|
132
|
+
n.class_type? || n.module_type? || n.sclass_type? || class_methods_block?(n) || included_block?(n) ||
|
|
133
|
+
eval_block?(n)
|
|
138
134
|
end
|
|
139
135
|
end
|
|
140
136
|
|
|
@@ -142,6 +138,14 @@ module RuboCop
|
|
|
142
138
|
node.block_type? && node.send_node.method_name == :class_methods
|
|
143
139
|
end
|
|
144
140
|
|
|
141
|
+
def included_block?(node)
|
|
142
|
+
node.block_type? && node.send_node.method_name == :included
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def eval_block?(node)
|
|
146
|
+
node.block_type? && %i[class_eval module_eval].include?(node.send_node.method_name)
|
|
147
|
+
end
|
|
148
|
+
|
|
145
149
|
def find_class_or_module(node)
|
|
146
150
|
node.each_ancestor.find { |n| n.class_type? || n.module_type? }
|
|
147
151
|
end
|
|
@@ -198,8 +202,7 @@ module RuboCop
|
|
|
198
202
|
class_name = extract_class_name(node)
|
|
199
203
|
return unless class_name
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
spec_paths = find_valid_spec_files(class_name, base_spec_path)
|
|
205
|
+
spec_paths = find_valid_spec_files(class_name, expected_spec_paths)
|
|
203
206
|
return if spec_paths.empty?
|
|
204
207
|
|
|
205
208
|
method_name = node.method_name.to_s
|
|
@@ -263,30 +266,14 @@ module RuboCop
|
|
|
263
266
|
]
|
|
264
267
|
end
|
|
265
268
|
|
|
266
|
-
def expected_spec_path
|
|
267
|
-
processed_source.file_path&.sub("/app/", "/spec/")&.sub(/\.rb$/, "_spec.rb")
|
|
268
|
-
end
|
|
269
|
-
|
|
270
|
-
def relative_spec_path(spec_path)
|
|
271
|
-
root = find_project_root
|
|
272
|
-
root ? spec_path.sub("#{root}/", "") : spec_path
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def find_project_root
|
|
276
|
-
path = processed_source.file_path
|
|
277
|
-
return nil if path.nil?
|
|
278
|
-
|
|
279
|
-
app_index = path.split("/").index("app")
|
|
280
|
-
app_index ? path.split("/")[0...app_index].join("/") : nil
|
|
281
|
-
end
|
|
282
|
-
|
|
283
269
|
def matches_skip_path?
|
|
284
|
-
|
|
270
|
+
skip_paths = shared_skip_method_describe_paths
|
|
271
|
+
return false if skip_paths.empty?
|
|
285
272
|
|
|
286
273
|
file_path = processed_source.file_path
|
|
287
274
|
return false unless file_path
|
|
288
275
|
|
|
289
|
-
|
|
276
|
+
skip_paths.any? do |pattern|
|
|
290
277
|
# Match against both absolute path and relative path
|
|
291
278
|
File.fnmatch?(pattern, file_path, File::FNM_PATHNAME | File::FNM_EXTGLOB) ||
|
|
292
279
|
File.fnmatch?(pattern, extract_relative_path(file_path), File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
@@ -46,22 +46,24 @@ module RuboCop
|
|
|
46
46
|
filename.split("_").map(&:capitalize).join
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def find_valid_spec_files(class_name,
|
|
50
|
-
|
|
49
|
+
def find_valid_spec_files(class_name, base_spec_paths) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
50
|
+
base_spec_paths = Array(base_spec_paths)
|
|
51
|
+
return [] if base_spec_paths.empty?
|
|
51
52
|
|
|
52
53
|
valid_files = []
|
|
53
54
|
|
|
54
|
-
|
|
55
|
-
|
|
55
|
+
base_spec_paths.each do |base_spec_path|
|
|
56
|
+
next unless base_spec_path
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
# But only include them if they describe the correct class
|
|
59
|
-
spec_dir = File.dirname(base_spec_path)
|
|
60
|
-
base_name = File.basename(base_spec_path, "_spec.rb")
|
|
61
|
-
wildcard_files = Dir.glob(File.join(spec_dir, "#{base_name}_*_spec.rb"))
|
|
58
|
+
valid_files << base_spec_path if File.exist?(base_spec_path)
|
|
62
59
|
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
spec_dir = File.dirname(base_spec_path)
|
|
61
|
+
base_name = File.basename(base_spec_path, "_spec.rb")
|
|
62
|
+
wildcard_files = Dir.glob(File.join(spec_dir, "#{base_name}_*_spec.rb"))
|
|
63
|
+
|
|
64
|
+
wildcard_files.each do |file|
|
|
65
|
+
valid_files << file if File.exist?(file) && spec_describes_class?(file, class_name)
|
|
66
|
+
end
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
valid_files.uniq
|
|
@@ -82,7 +84,7 @@ module RuboCop
|
|
|
82
84
|
end
|
|
83
85
|
|
|
84
86
|
def describe_aliases_for(describe_key)
|
|
85
|
-
value =
|
|
87
|
+
value = shared_describe_aliases[describe_key]
|
|
86
88
|
return [] unless value
|
|
87
89
|
|
|
88
90
|
Array(value).map(&:to_s)
|
|
@@ -38,6 +38,7 @@ module RuboCop
|
|
|
38
38
|
# context 'when creating a regular user' do
|
|
39
39
|
# end
|
|
40
40
|
class SufficientContexts < Base # rubocop:disable Metrics/ClassLength
|
|
41
|
+
include DepartmentConfig
|
|
41
42
|
include SpecFileFinder
|
|
42
43
|
|
|
43
44
|
MSG = "Method `%<method_name>s` has %<branches>d %<branch_word>s but only %<contexts>d %<context_word>s " \
|
|
@@ -65,8 +66,6 @@ module RuboCop
|
|
|
65
66
|
def initialize(config = nil, options = nil)
|
|
66
67
|
super
|
|
67
68
|
@ignore_memoization = cop_config.fetch("IgnoreMemoization", true)
|
|
68
|
-
@skip_method_describe_paths = cop_config.fetch("SkipMethodDescribeFor", [])
|
|
69
|
-
@describe_aliases = cop_config.fetch("DescribeAliases", {})
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
def on_def(node)
|
|
@@ -89,8 +88,7 @@ module RuboCop
|
|
|
89
88
|
class_name = extract_class_name(node)
|
|
90
89
|
return unless class_name
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
spec_files = find_valid_spec_files(class_name, base_spec_path)
|
|
91
|
+
spec_files = find_valid_spec_files(class_name, expected_spec_paths)
|
|
94
92
|
return if spec_files.empty?
|
|
95
93
|
|
|
96
94
|
# Aggregate contexts from all valid spec files
|
|
@@ -143,12 +141,6 @@ module RuboCop
|
|
|
143
141
|
processed_source.path
|
|
144
142
|
end
|
|
145
143
|
|
|
146
|
-
def spec_file_path
|
|
147
|
-
path = processed_source.path
|
|
148
|
-
# Handle both absolute and relative paths
|
|
149
|
-
path.sub(%r{/app/}, "/spec/").sub(%r{^app/}, "spec/").sub(/\.rb$/, "_spec.rb")
|
|
150
|
-
end
|
|
151
|
-
|
|
152
144
|
def count_branches(node)
|
|
153
145
|
branches = 0
|
|
154
146
|
elsif_nodes = collect_elsif_nodes(node)
|
|
@@ -372,12 +364,13 @@ module RuboCop
|
|
|
372
364
|
end
|
|
373
365
|
|
|
374
366
|
def matches_skip_path?
|
|
375
|
-
|
|
367
|
+
skip_paths = shared_skip_method_describe_paths
|
|
368
|
+
return false if skip_paths.empty?
|
|
376
369
|
|
|
377
370
|
path = processed_source.path
|
|
378
371
|
return false unless path
|
|
379
372
|
|
|
380
|
-
|
|
373
|
+
skip_paths.any? do |pattern|
|
|
381
374
|
File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
|
|
382
375
|
end
|
|
383
376
|
end
|
data/lib/rubocop_rspec_parity.rb
CHANGED
|
@@ -5,5 +5,7 @@ require "rubocop"
|
|
|
5
5
|
require_relative "rubocop/rspec_parity"
|
|
6
6
|
require_relative "rubocop/rspec_parity/version"
|
|
7
7
|
require_relative "rubocop/rspec_parity/plugin"
|
|
8
|
+
require_relative "rubocop/cop/rspec_parity/department_config"
|
|
9
|
+
require_relative "rubocop/cop/rspec_parity/file_has_spec"
|
|
8
10
|
require_relative "rubocop/cop/rspec_parity/public_method_has_spec"
|
|
9
11
|
require_relative "rubocop/cop/rspec_parity/sufficient_contexts"
|
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
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Povilas Jurcys
|
|
@@ -54,6 +54,8 @@ files:
|
|
|
54
54
|
- Rakefile
|
|
55
55
|
- config/default.yml
|
|
56
56
|
- lib/rubocop-rspec_parity.rb
|
|
57
|
+
- lib/rubocop/cop/rspec_parity/department_config.rb
|
|
58
|
+
- lib/rubocop/cop/rspec_parity/file_has_spec.rb
|
|
57
59
|
- lib/rubocop/cop/rspec_parity/public_method_has_spec.rb
|
|
58
60
|
- lib/rubocop/cop/rspec_parity/spec_file_finder.rb
|
|
59
61
|
- lib/rubocop/cop/rspec_parity/sufficient_contexts.rb
|