rubocop-rspec_parity 1.3.5 → 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 +5 -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 +5 -27
- 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,10 @@
|
|
|
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
|
+
|
|
3
8
|
## [1.3.5] - 2026-03-02
|
|
4
9
|
|
|
5
10
|
Fixed: `PublicMethodHasSpec` now correctly recognizes `private` declarations inside `included do`, `class_eval do`, and `module_eval do` blocks as visibility scopes
|
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)
|
|
@@ -207,8 +202,7 @@ module RuboCop
|
|
|
207
202
|
class_name = extract_class_name(node)
|
|
208
203
|
return unless class_name
|
|
209
204
|
|
|
210
|
-
|
|
211
|
-
spec_paths = find_valid_spec_files(class_name, base_spec_path)
|
|
205
|
+
spec_paths = find_valid_spec_files(class_name, expected_spec_paths)
|
|
212
206
|
return if spec_paths.empty?
|
|
213
207
|
|
|
214
208
|
method_name = node.method_name.to_s
|
|
@@ -272,30 +266,14 @@ module RuboCop
|
|
|
272
266
|
]
|
|
273
267
|
end
|
|
274
268
|
|
|
275
|
-
def expected_spec_path
|
|
276
|
-
processed_source.file_path&.sub("/app/", "/spec/")&.sub(/\.rb$/, "_spec.rb")
|
|
277
|
-
end
|
|
278
|
-
|
|
279
|
-
def relative_spec_path(spec_path)
|
|
280
|
-
root = find_project_root
|
|
281
|
-
root ? spec_path.sub("#{root}/", "") : spec_path
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def find_project_root
|
|
285
|
-
path = processed_source.file_path
|
|
286
|
-
return nil if path.nil?
|
|
287
|
-
|
|
288
|
-
app_index = path.split("/").index("app")
|
|
289
|
-
app_index ? path.split("/")[0...app_index].join("/") : nil
|
|
290
|
-
end
|
|
291
|
-
|
|
292
269
|
def matches_skip_path?
|
|
293
|
-
|
|
270
|
+
skip_paths = shared_skip_method_describe_paths
|
|
271
|
+
return false if skip_paths.empty?
|
|
294
272
|
|
|
295
273
|
file_path = processed_source.file_path
|
|
296
274
|
return false unless file_path
|
|
297
275
|
|
|
298
|
-
|
|
276
|
+
skip_paths.any? do |pattern|
|
|
299
277
|
# Match against both absolute path and relative path
|
|
300
278
|
File.fnmatch?(pattern, file_path, File::FNM_PATHNAME | File::FNM_EXTGLOB) ||
|
|
301
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
|