rubocop-rspec_parity 1.0.0 → 1.1.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: 2e75d169afdb6caa654d618ae15b194b3329b53c1151ce2cc492ed521a513327
4
- data.tar.gz: 7ae507090fa12baa552224b911b5f50fb98096f87181880c98395b93368009cf
3
+ metadata.gz: f9e0dc1e3be84865d59b94dfae0d55c808bb307c2bfefd5f85bc66921048afbe
4
+ data.tar.gz: e6abf7807ddc099af831d46b90e5c18913c76f0be754553dbffef0c83f723eba
5
5
  SHA512:
6
- metadata.gz: 4c7e2fda565eafad49f2d7017edc91df2f12c200c821f59313afdc4c0e8221c7136e9197010a21ff6b2819c91b7a5cd72c7f71936aea2321ad389638bd830f70
7
- data.tar.gz: b67f7496c9f0c483c82af7691579b3c9bb21efed23a4d7d50d70dac5fc5e3b2b7b531ecab0aedc660579fbbed659b1c38db0bf563c2f09ebf2f05be1d68665ae
6
+ metadata.gz: c434bf6e258c007681582e7666184bd5233ba260ab60f981e46140f7a420a28e6079ecc506810728f552e2aaacc3aa71f181df6d684e6fd8f8f28d4279ee2b8f
7
+ data.tar.gz: 4671da5c79fe882e1cc8808764430d819b30646594d39aadc774559eb1055c01903b869577bf38139f8a32acc9f6a10f72afcde256cc49bb7d88324785ece872
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.1.0] - 2026-02-06
4
+
5
+ Added: Support for checking wildcard spec files (`<model>_*_spec.rb`) in addition to base spec file, with automatic class validation
6
+ Added: `SkipMethodDescribeFor` configuration for `PublicMethodHasSpec` and `SufficientContexts` cops to allow single-method classes (like service objects) to skip method describe blocks in specs
7
+ Added: `DescribeAliases` configuration for mapping describe strings to alias describe patterns
8
+
3
9
  ## [1.0.0] - 2026-01-27
4
10
 
5
11
  Added: `IgnoreMemoization` configuration option for `SufficientContexts` cop to ignore memoization patterns like `@var ||=` and `return @var if defined?(@var)`
data/README.md CHANGED
@@ -54,6 +54,126 @@ RSpec.describe UserCreator do
54
54
  end
55
55
  ```
56
56
 
57
+ #### SkipMethodDescribeFor Configuration
58
+
59
+ For service objects and similar patterns with a single public method, you can skip the method describe block:
60
+
61
+ ```yaml
62
+ RSpecParity/PublicMethodHasSpec:
63
+ SkipMethodDescribeFor:
64
+ - 'app/services/**/*'
65
+ - 'app/operations/**/*'
66
+
67
+ RSpecParity/SufficientContexts:
68
+ SkipMethodDescribeFor:
69
+ - 'app/services/**/*'
70
+ - 'app/operations/**/*'
71
+ ```
72
+
73
+ With this configuration:
74
+
75
+ ```ruby
76
+ # app/services/user_creator.rb
77
+ class UserCreator
78
+ def call(params)
79
+ User.create(params)
80
+ end
81
+
82
+ private
83
+
84
+ def validate(params)
85
+ # Private helper - doesn't count
86
+ end
87
+ end
88
+
89
+ # spec/services/user_creator_spec.rb - Valid!
90
+ RSpec.describe UserCreator do
91
+ it 'creates a user' do
92
+ # test implementation
93
+ end
94
+
95
+ context 'when invalid params' do
96
+ it 'raises error' do
97
+ end
98
+ end
99
+ end
100
+ ```
101
+
102
+ **Requirements:**
103
+ - Class must have exactly ONE public method
104
+ - Spec must contain at least one example (`it`, `example`, or `specify`)
105
+ - Traditional method describes still work if you prefer them
106
+
107
+ **How it works:**
108
+ - `PublicMethodHasSpec`: Instead of requiring `describe '#call'`, it just checks that examples exist
109
+ - `SufficientContexts`: Counts top-level contexts/examples instead of looking inside method describes
110
+
111
+ #### DescribeAliases Configuration
112
+
113
+ When an instance method is tested via a class method (e.g., service objects where `def call` is tested via `describe '.call'`, or jobs where `def perform` is tested via `describe '.perform_later'`), you can configure describe aliases:
114
+
115
+ ```yaml
116
+ RSpecParity/PublicMethodHasSpec:
117
+ DescribeAliases:
118
+ '#call': '.call'
119
+ '#perform':
120
+ - '.perform_later'
121
+ - '.perform_now'
122
+
123
+ RSpecParity/SufficientContexts:
124
+ DescribeAliases:
125
+ '#call': '.call'
126
+ '#perform':
127
+ - '.perform_later'
128
+ - '.perform_now'
129
+ ```
130
+
131
+ With this configuration:
132
+
133
+ ```ruby
134
+ # app/services/user_creator.rb
135
+ class UserCreator
136
+ def call(params)
137
+ User.create(params)
138
+ end
139
+ end
140
+
141
+ # spec/services/user_creator_spec.rb - Valid!
142
+ # The alias '#call' => '.call' allows .call to satisfy #call
143
+ RSpec.describe UserCreator do
144
+ describe '.call' do
145
+ it 'creates a user' do
146
+ # test implementation
147
+ end
148
+ end
149
+ end
150
+ ```
151
+
152
+ ```ruby
153
+ # app/jobs/user_job.rb
154
+ class UserJob
155
+ def perform(params)
156
+ process(params)
157
+ end
158
+ end
159
+
160
+ # spec/jobs/user_job_spec.rb - Valid!
161
+ # The alias '#perform' => ['.perform_later', '.perform_now'] allows either
162
+ RSpec.describe UserJob do
163
+ describe '.perform_later' do
164
+ it 'processes params' do
165
+ # test implementation
166
+ end
167
+ end
168
+ end
169
+ ```
170
+
171
+ **Notes:**
172
+ - Keys use `#` for instance method describes, `.` for class method describes
173
+ - Values can be a single string or an array of strings
174
+ - Aliases are unidirectional: `'#call': '.call'` allows `.call` to satisfy `#call`, but not vice versa
175
+ - `SufficientContexts` aggregates contexts from both the original and alias describe blocks
176
+
57
177
  ### RSpecParity/SufficientContexts
58
178
 
59
179
  Ensures specs have at least as many contexts as the method has branches.
@@ -169,12 +289,16 @@ RSpecParity/FileHasSpec:
169
289
 
170
290
  RSpecParity/PublicMethodHasSpec:
171
291
  Enabled: true
292
+ SkipMethodDescribeFor: [] # Paths where single-method classes don't need method describe
293
+ DescribeAliases: {} # Map describe strings to aliases, e.g. '#call': '.call'
172
294
  Include:
173
295
  - 'app/**/*.rb'
174
296
 
175
297
  RSpecParity/SufficientContexts:
176
298
  Enabled: true
177
299
  IgnoreMemoization: true # Set to false to count memoization patterns as branches
300
+ SkipMethodDescribeFor: [] # Paths where single-method classes use top-level contexts
301
+ DescribeAliases: {} # Map describe strings to aliases, e.g. '#call': '.call'
178
302
  Include:
179
303
  - 'app/**/*.rb'
180
304
  Exclude:
data/config/default.yml CHANGED
@@ -13,6 +13,8 @@ RSpecParity/FileHasSpec:
13
13
  RSpecParity/PublicMethodHasSpec:
14
14
  Description: 'Checks that each public method has a corresponding spec test.'
15
15
  Enabled: true
16
+ SkipMethodDescribeFor: []
17
+ DescribeAliases: {}
16
18
  Include:
17
19
  - 'app/**/*.rb'
18
20
  Exclude:
@@ -24,6 +26,8 @@ RSpecParity/SufficientContexts:
24
26
  Description: 'Ensures specs have at least as many contexts as the method has branches.'
25
27
  Enabled: true
26
28
  IgnoreMemoization: true
29
+ SkipMethodDescribeFor: []
30
+ DescribeAliases: {}
27
31
  Include:
28
32
  - 'app/**/*.rb'
29
33
  Exclude:
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "spec_file_finder"
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module RSpecParity
@@ -10,7 +12,9 @@ module RuboCop
10
12
  #
11
13
  # # good - public method `perform` has describe '#perform' in spec
12
14
  #
13
- class PublicMethodHasSpec < Base
15
+ class PublicMethodHasSpec < Base # rubocop:disable Metrics/ClassLength
16
+ include SpecFileFinder
17
+
14
18
  MSG = "Missing spec for public method `%<method_name>s`. " \
15
19
  "Expected describe '#%<method_name>s' or describe '.%<method_name>s' in %<spec_path>s"
16
20
 
@@ -19,6 +23,12 @@ module RuboCop
19
23
  EXCLUDED_PATTERNS = [/^before_/, /^after_/, /^around_/, /^validate_/, /^autosave_/].freeze
20
24
  VISIBILITY_METHODS = { private: :private, protected: :protected, public: :public }.freeze
21
25
 
26
+ def initialize(config = nil, options = nil)
27
+ super
28
+ @skip_method_describe_paths = cop_config.fetch("SkipMethodDescribeFor", [])
29
+ @describe_aliases = cop_config.fetch("DescribeAliases", {})
30
+ end
31
+
22
32
  def on_def(node)
23
33
  return unless checkable_method?(node) && public_method?(node)
24
34
 
@@ -82,24 +92,44 @@ module RuboCop
82
92
  EXCLUDED_PATTERNS.any? { |pattern| pattern.match?(method_name) }
83
93
  end
84
94
 
95
+ def source_file_path
96
+ processed_source.file_path
97
+ end
98
+
99
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
85
100
  def check_method_has_spec(node, instance_method:)
86
- spec_path = expected_spec_path
87
- return unless spec_path && File.exist?(spec_path)
101
+ class_name = extract_class_name(node)
102
+ return unless class_name
103
+
104
+ base_spec_path = expected_spec_path
105
+ spec_paths = find_valid_spec_files(class_name, base_spec_path)
106
+ return if spec_paths.empty?
88
107
 
89
108
  method_name = node.method_name.to_s
90
- return if spec_covers_method?(spec_path, method_name, instance_method)
91
109
 
92
- add_method_offense(node, method_name, spec_path)
110
+ # Check if relaxed validation applies
111
+ if matches_skip_path? && count_public_methods(node) == 1
112
+ # For single-method classes in configured paths, just check for examples
113
+ return if spec_paths.any? { |spec_path| spec_has_examples?(spec_path, class_name) }
114
+ elsif spec_paths.any? { |spec_path| spec_covers_method?(spec_path, method_name, instance_method) }
115
+ # Normal validation: check for method describe
116
+ return
117
+ end
118
+
119
+ add_method_offense(node, method_name, spec_paths.first)
93
120
  end
121
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
94
122
 
95
123
  def spec_covers_method?(spec_path, method_name, instance_method)
96
124
  return true if method_tested_in_spec?(spec_path, method_name, instance_method)
97
125
 
98
- service_call_method?(method_name) && method_tested_in_spec?(spec_path, method_name, !instance_method)
99
- end
100
-
101
- def service_call_method?(method_name)
102
- method_name == "call" && processed_source.file_path&.include?("/app/services/")
126
+ prefix = instance_method ? "#" : "."
127
+ describe_key = "#{prefix}#{method_name}"
128
+ describe_aliases_for(describe_key).any? do |alias_desc|
129
+ alias_name = alias_desc.sub(/^[#.]/, "")
130
+ alias_instance = !alias_desc.start_with?(".")
131
+ method_tested_in_spec?(spec_path, alias_name, alias_instance)
132
+ end
103
133
  end
104
134
 
105
135
  def add_method_offense(node, method_name, spec_path)
@@ -142,6 +172,72 @@ module RuboCop
142
172
  app_index = path.split("/").index("app")
143
173
  app_index ? path.split("/")[0...app_index].join("/") : nil
144
174
  end
175
+
176
+ def matches_skip_path?
177
+ return false if @skip_method_describe_paths.empty?
178
+
179
+ file_path = processed_source.file_path
180
+ return false unless file_path
181
+
182
+ @skip_method_describe_paths.any? do |pattern|
183
+ # Match against both absolute path and relative path
184
+ File.fnmatch?(pattern, file_path, File::FNM_PATHNAME | File::FNM_EXTGLOB) ||
185
+ File.fnmatch?(pattern, extract_relative_path(file_path), File::FNM_PATHNAME | File::FNM_EXTGLOB)
186
+ end
187
+ end
188
+
189
+ def extract_relative_path(file_path)
190
+ # Extract path starting from 'app/' directory
191
+ app_index = file_path.split("/").index("app")
192
+ return file_path unless app_index
193
+
194
+ file_path.split("/")[app_index..].join("/")
195
+ end
196
+
197
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
198
+ def count_public_methods(node)
199
+ class_node = find_class_or_module(node)
200
+ return 0 unless class_node&.body
201
+
202
+ public_methods = []
203
+ visibility = :public
204
+
205
+ # Get all child nodes, handling both single method and begin-wrapped bodies
206
+ children = if class_node.body.begin_type?
207
+ class_node.body.children
208
+ else
209
+ [class_node.body]
210
+ end
211
+
212
+ children.each do |child|
213
+ next unless child
214
+
215
+ case child.type
216
+ when :send
217
+ visibility = VISIBILITY_METHODS[child.method_name] if VISIBILITY_METHODS.key?(child.method_name)
218
+ when :def
219
+ # Only count instance methods (def), not class methods (defs)
220
+ if visibility == :public
221
+ method_name = child.method_name.to_s
222
+ public_methods << method_name unless excluded_method?(method_name)
223
+ end
224
+ end
225
+ end
226
+
227
+ public_methods.size
228
+ end
229
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
230
+
231
+ def spec_has_examples?(spec_path, class_name)
232
+ spec_content = File.read(spec_path)
233
+ escaped_class_name = Regexp.escape(class_name)
234
+
235
+ # Check that the spec describes the correct class
236
+ return false unless spec_content.match?(/(?:RSpec\.)?describe\s+#{escaped_class_name}(?:\s|,|do)/)
237
+
238
+ # Check for any it/example/specify blocks
239
+ spec_content.match?(/^\s*(?:it|example|specify)\s+/)
240
+ end
145
241
  end
146
242
  end
147
243
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpecParity
6
+ # Shared module for finding and validating spec files
7
+ module SpecFileFinder
8
+ private
9
+
10
+ def extract_class_name(node)
11
+ class_node = find_class_or_module(node)
12
+
13
+ if class_node
14
+ # Get the class/module name from the AST
15
+ const_node = class_node.children[0]
16
+ return const_node.const_name if const_node.const_type?
17
+ end
18
+
19
+ # Fallback: infer class name from file path
20
+ infer_class_name_from_path
21
+ end
22
+
23
+ def find_class_or_module(node)
24
+ node.each_ancestor.find { |n| n.class_type? || n.module_type? }
25
+ end
26
+
27
+ def infer_class_name_from_path
28
+ path = source_file_path
29
+ return nil unless path
30
+
31
+ # Extract filename and convert to class name
32
+ # e.g., app/services/user_creator.rb → UserCreator
33
+ filename = File.basename(path, ".rb")
34
+ filename.split("_").map(&:capitalize).join
35
+ end
36
+
37
+ def find_valid_spec_files(class_name, base_spec_path)
38
+ return [] unless base_spec_path
39
+
40
+ valid_files = []
41
+
42
+ # Always include the base spec file if it exists (maintains original behavior)
43
+ valid_files << base_spec_path if File.exist?(base_spec_path)
44
+
45
+ # Also check for wildcard spec files (e.g., user_updates_spec.rb)
46
+ # But only include them if they describe the correct class
47
+ spec_dir = File.dirname(base_spec_path)
48
+ base_name = File.basename(base_spec_path, "_spec.rb")
49
+ wildcard_files = Dir.glob(File.join(spec_dir, "#{base_name}_*_spec.rb"))
50
+
51
+ wildcard_files.each do |file|
52
+ valid_files << file if File.exist?(file) && spec_describes_class?(file, class_name)
53
+ end
54
+
55
+ valid_files.uniq
56
+ end
57
+
58
+ def spec_describes_class?(spec_path, class_name)
59
+ spec_content = File.read(spec_path)
60
+ # Match RSpec.describe ClassName or describe ClassName
61
+ spec_content.match?(/(?:RSpec\.)?describe\s+#{Regexp.escape(class_name)}(?:\s|,|do)/)
62
+ end
63
+
64
+ def describe_aliases_for(describe_key)
65
+ value = @describe_aliases[describe_key]
66
+ return [] unless value
67
+
68
+ Array(value).map(&:to_s)
69
+ end
70
+
71
+ # Override this method in the including class
72
+ def source_file_path
73
+ processed_source.file_path
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "spec_file_finder"
4
+
3
5
  module RuboCop
4
6
  module Cop
5
7
  module RSpecParity
@@ -36,6 +38,8 @@ module RuboCop
36
38
  # context 'when creating a regular user' do
37
39
  # end
38
40
  class SufficientContexts < Base # rubocop:disable Metrics/ClassLength
41
+ include SpecFileFinder
42
+
39
43
  MSG = "Method `%<method_name>s` has %<branches>d %<branch_word>s but only %<contexts>d %<context_word>s " \
40
44
  "in spec. Add %<missing>d more %<missing_word>s to cover all branches."
41
45
 
@@ -61,6 +65,8 @@ module RuboCop
61
65
  def initialize(config = nil, options = nil)
62
66
  super
63
67
  @ignore_memoization = cop_config.fetch("IgnoreMemoization", true)
68
+ @skip_method_describe_paths = cop_config.fetch("SkipMethodDescribeFor", [])
69
+ @describe_aliases = cop_config.fetch("DescribeAliases", {})
64
70
  end
65
71
 
66
72
  def on_def(node)
@@ -73,18 +79,28 @@ module RuboCop
73
79
 
74
80
  private
75
81
 
76
- def check_method(node) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
82
+ def check_method(node) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
77
83
  return unless in_covered_directory?
78
84
  return if excluded_method?(method_name(node))
79
85
 
80
86
  branches = count_branches(node)
81
87
  return if branches < 2 # Only check methods with branches
82
88
 
83
- spec_file = spec_file_path
84
- return unless File.exist?(spec_file)
89
+ class_name = extract_class_name(node)
90
+ return unless class_name
91
+
92
+ base_spec_path = spec_file_path
93
+ spec_files = find_valid_spec_files(class_name, base_spec_path)
94
+ return if spec_files.empty?
85
95
 
86
- spec_content = File.read(spec_file)
87
- contexts = count_contexts_for_method(spec_content, method_name(node))
96
+ # Aggregate contexts from all valid spec files
97
+ contexts = if matches_skip_path? && count_public_methods(node) == 1
98
+ # For single-method classes, count top-level contexts instead
99
+ spec_files.sum { |spec_file| count_top_level_contexts(File.read(spec_file), class_name) }
100
+ else
101
+ # Normal path: look for method describes
102
+ spec_files.sum { |spec_file| count_method_contexts(File.read(spec_file), method_name(node)) }
103
+ end
88
104
 
89
105
  return if contexts.zero? # Method has no specs at all - PublicMethodHasSpec handles this
90
106
  return if contexts >= branches
@@ -123,6 +139,10 @@ module RuboCop
123
139
  EXCLUDED_PATTERNS.any? { |pattern| pattern.match?(method_name) }
124
140
  end
125
141
 
142
+ def source_file_path
143
+ processed_source.path
144
+ end
145
+
126
146
  def spec_file_path
127
147
  path = processed_source.path
128
148
  # Handle both absolute and relative paths
@@ -187,6 +207,15 @@ module RuboCop
187
207
  when_count + (has_else ? 1 : 0)
188
208
  end
189
209
 
210
+ def count_method_contexts(spec_content, mname)
211
+ count = count_contexts_for_method(spec_content, mname)
212
+ describe_aliases_for("##{mname}").each do |alias_desc|
213
+ alias_name = alias_desc.sub(/^[#.]/, "")
214
+ count += count_contexts_for_method(spec_content, alias_name) if alias_name != mname
215
+ end
216
+ count
217
+ end
218
+
190
219
  def count_contexts_for_method(spec_content, method_name)
191
220
  method_pattern = Regexp.escape(method_name)
192
221
  context_count, has_examples = parse_spec_content(spec_content, method_pattern)
@@ -341,6 +370,88 @@ module RuboCop
341
370
  left = node.children[0]
342
371
  left&.ivar_type?
343
372
  end
373
+
374
+ def matches_skip_path?
375
+ return false if @skip_method_describe_paths.empty?
376
+
377
+ path = processed_source.path
378
+ return false unless path
379
+
380
+ @skip_method_describe_paths.any? do |pattern|
381
+ File.fnmatch?(pattern, path, File::FNM_PATHNAME | File::FNM_EXTGLOB)
382
+ end
383
+ end
384
+
385
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
386
+ def count_public_methods(node)
387
+ class_node = find_class_or_module(node)
388
+ return 0 unless class_node&.body
389
+
390
+ public_methods = []
391
+ visibility = :public
392
+ visibility_methods = { private: :private, protected: :protected, public: :public }
393
+
394
+ # Get all child nodes, handling both single method and begin-wrapped bodies
395
+ children = if class_node.body.begin_type?
396
+ class_node.body.children
397
+ else
398
+ [class_node.body]
399
+ end
400
+
401
+ children.each do |child|
402
+ next unless child
403
+
404
+ case child.type
405
+ when :send
406
+ visibility = visibility_methods[child.method_name] if visibility_methods.key?(child.method_name)
407
+ when :def
408
+ # Only count instance methods (def), not class methods (defs)
409
+ if visibility == :public
410
+ method_name = child.method_name.to_s
411
+ public_methods << method_name unless excluded_method?(method_name)
412
+ end
413
+ end
414
+ end
415
+
416
+ public_methods.size
417
+ end
418
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
419
+
420
+ def find_class_or_module(node)
421
+ node.each_ancestor.find { |n| n.class_type? || n.module_type? }
422
+ end
423
+
424
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
425
+ def count_top_level_contexts(spec_content, class_name)
426
+ # Find the class describe block
427
+ describe_pattern = /^\s*(?:RSpec\.)?describe\s+#{Regexp.escape(class_name)}(?:\s|,|do)/
428
+
429
+ lines = spec_content.lines
430
+ describe_line_index = lines.index { |line| line.match?(describe_pattern) }
431
+ return 0 unless describe_line_index
432
+
433
+ # Count contexts/describes and examples at the top level (under class describe)
434
+ base_indent = lines[describe_line_index].match(/^(\s*)/)[1].length
435
+ context_count = 0
436
+ has_examples = false
437
+
438
+ lines[(describe_line_index + 1)..].each do |line|
439
+ indent = line.match(/^(\s*)/)[1].length
440
+ break if indent <= base_indent && !line.strip.empty? && line.match?(/^\s*(?:describe|context|end)/)
441
+
442
+ next unless indent > base_indent
443
+
444
+ if line.match?(/^\s*(?:context|describe)\s+/)
445
+ context_count += 1
446
+ elsif line.match?(/^\s*(?:it|example|specify)\s+/)
447
+ has_examples = true
448
+ end
449
+ end
450
+
451
+ # If no contexts but has examples, count as 1
452
+ context_count.zero? && has_examples ? 1 : context_count
453
+ end
454
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
344
455
  end
345
456
  end
346
457
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module RSpecParity
5
- VERSION = "1.0.0"
5
+ VERSION = "1.1.0"
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.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Povilas Jurcys
@@ -55,6 +55,7 @@ files:
55
55
  - config/default.yml
56
56
  - lib/rubocop-rspec_parity.rb
57
57
  - lib/rubocop/cop/rspec_parity/public_method_has_spec.rb
58
+ - lib/rubocop/cop/rspec_parity/spec_file_finder.rb
58
59
  - lib/rubocop/cop/rspec_parity/sufficient_contexts.rb
59
60
  - lib/rubocop/rspec_parity.rb
60
61
  - lib/rubocop/rspec_parity/plugin.rb