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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 405b1cf99501c2de2098e7f7349191811bc882a303cad1081923d80ca93b9944
4
- data.tar.gz: 9a6fd07fd98b90b5e6f124191888eff19ed380eb3c3827594b78ca631e29c77f
3
+ metadata.gz: 6a5de925a1a0fb2a5857e4c6b8e021abeac3fe0e7f792c536497db7468382375
4
+ data.tar.gz: 32c20a277a217ffa24a011ae7fa178ea81c5db9d99a74076686d56504d243043
5
5
  SHA512:
6
- metadata.gz: 34c8cb1ead5f2dd51ab8bce3b2fbd0f3cc3030cdb6d13b77164c8b19ef9c26fe35275da0ec78ebb9a94ce57620639879ded4f2d95650dd1362512d73261f8394
7
- data.tar.gz: 9bdea0f802338bed6fa80c5593e1577758e7e4aeb06a03e28779193ae71cef90360c602ec6ed06f17e54405bead119c2901fc9edd618db96d89a3f799ce89833
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
- base_spec_path = expected_spec_path
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
- return false if @skip_method_describe_paths.empty?
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
- @skip_method_describe_paths.any? do |pattern|
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, base_spec_path)
50
- return [] unless base_spec_path
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
- # Always include the base spec file if it exists (maintains original behavior)
55
- valid_files << base_spec_path if File.exist?(base_spec_path)
55
+ base_spec_paths.each do |base_spec_path|
56
+ next unless base_spec_path
56
57
 
57
- # Also check for wildcard spec files (e.g., user_updates_spec.rb)
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
- wildcard_files.each do |file|
64
- valid_files << file if File.exist?(file) && spec_describes_class?(file, class_name)
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 = @describe_aliases[describe_key]
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
- base_spec_path = spec_file_path
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
- return false if @skip_method_describe_paths.empty?
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
- @skip_method_describe_paths.any? do |pattern|
373
+ skip_paths.any? do |pattern|
381
374
  File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
382
375
  end
383
376
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module RSpecParity
5
- VERSION = "1.3.5"
5
+ VERSION = "1.4.0"
6
6
  end
7
7
  end
@@ -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.3.5
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