rubocop-rspec-guide 0.2.2 → 0.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +6 -6
  3. data/.yardopts +9 -0
  4. data/CHANGELOG.md +86 -0
  5. data/CONTRIBUTING.md +358 -0
  6. data/INTEGRATION_TESTING.md +324 -0
  7. data/README.md +443 -16
  8. data/Rakefile +49 -0
  9. data/benchmark/README.md +349 -0
  10. data/benchmark/baseline_v0.3.1.txt +67 -0
  11. data/benchmark/baseline_v0.4.0.txt +167 -0
  12. data/benchmark/benchmark_helper.rb +92 -0
  13. data/benchmark/compare_versions.rb +136 -0
  14. data/benchmark/cops_benchmark.rb +428 -0
  15. data/benchmark/cops_performance.rb +109 -0
  16. data/benchmark/quick_comparison.rb +58 -0
  17. data/benchmark/quick_invariant_bench.rb +52 -0
  18. data/benchmark/rspec_base_integration.rb +86 -0
  19. data/benchmark/save_baseline.rb +18 -0
  20. data/benchmark/scalability_benchmark.rb +181 -0
  21. data/config/default.yml +43 -2
  22. data/config/obsoletion.yml +6 -0
  23. data/lib/rubocop/cop/factory_bot_guide/dynamic_attribute_evaluation.rb +193 -0
  24. data/lib/rubocop/cop/factory_bot_guide/dynamic_attributes_for_time_and_random.rb +10 -106
  25. data/lib/rubocop/cop/rspec_guide/characteristics_and_contexts.rb +13 -78
  26. data/lib/rubocop/cop/rspec_guide/context_setup.rb +81 -30
  27. data/lib/rubocop/cop/rspec_guide/duplicate_before_hooks.rb +89 -22
  28. data/lib/rubocop/cop/rspec_guide/duplicate_let_values.rb +89 -22
  29. data/lib/rubocop/cop/rspec_guide/happy_path_first.rb +52 -21
  30. data/lib/rubocop/cop/rspec_guide/invariant_examples.rb +60 -19
  31. data/lib/rubocop/cop/rspec_guide/minimum_behavioral_coverage.rb +165 -0
  32. data/lib/rubocop/rspec/guide/inject.rb +26 -0
  33. data/lib/rubocop/rspec/guide/plugin.rb +45 -0
  34. data/lib/rubocop/rspec/guide/version.rb +1 -1
  35. data/lib/rubocop-rspec-guide.rb +4 -0
  36. metadata +49 -1
@@ -3,11 +3,19 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module RSpecGuide
6
- # Detects duplicate let declarations with identical values across all sibling contexts.
7
- # These should be extracted to the parent context.
6
+ # Detects duplicate let declarations with identical values across sibling contexts.
8
7
  #
9
- # @example
10
- # # bad
8
+ # When the same let with the same value appears in multiple sibling contexts,
9
+ # it indicates one of two problems:
10
+ # 1. ERROR: Duplicate in ALL contexts → must extract to parent
11
+ # 2. WARNING: Duplicate in SOME contexts → suggests poor test hierarchy
12
+ #
13
+ # @safety
14
+ # This cop is safe to run automatically. It only detects duplicates,
15
+ # not semantic issues.
16
+ #
17
+ # @example ERROR - duplicate in ALL contexts
18
+ # # bad - let(:currency) duplicated in all 2 contexts
11
19
  # describe 'Calculator' do
12
20
  # context 'with addition' do
13
21
  # let(:currency) { :usd }
@@ -15,14 +23,14 @@ module RuboCop
15
23
  # end
16
24
  #
17
25
  # context 'with subtraction' do
18
- # let(:currency) { :usd } # Duplicate!
26
+ # let(:currency) { :usd } # ERROR: in all contexts!
19
27
  # it { expect(result).to eq(5) }
20
28
  # end
21
29
  # end
22
30
  #
23
- # # good
31
+ # # good - extracted to parent
24
32
  # describe 'Calculator' do
25
- # let(:currency) { :usd } # Extracted to parent
33
+ # let(:currency) { :usd } # Moved to parent
26
34
  #
27
35
  # context 'with addition' do
28
36
  # it { expect(result).to eq(10) }
@@ -33,33 +41,92 @@ module RuboCop
33
41
  # end
34
42
  # end
35
43
  #
36
- class DuplicateLetValues < Base
44
+ # @example WARNING - duplicate in SOME contexts
45
+ # # bad - let(:mode) duplicated in 2/3 contexts (code smell)
46
+ # describe 'Processor' do
47
+ # context 'scenario A' do
48
+ # let(:mode) { :standard }
49
+ # it { expect(result).to eq('A') }
50
+ # end
51
+ #
52
+ # context 'scenario B' do
53
+ # let(:mode) { :standard } # WARNING: duplicated in 2/3
54
+ # it { expect(result).to eq('B') }
55
+ # end
56
+ #
57
+ # context 'scenario C' do
58
+ # let(:mode) { :advanced } # Different value
59
+ # it { expect(result).to eq('C') }
60
+ # end
61
+ # end
62
+ #
63
+ # # good - refactor hierarchy
64
+ # describe 'Processor' do
65
+ # context 'with standard mode' do
66
+ # let(:mode) { :standard }
67
+ #
68
+ # context 'scenario A' do
69
+ # it { expect(result).to eq('A') }
70
+ # end
71
+ #
72
+ # context 'scenario B' do
73
+ # it { expect(result).to eq('B') }
74
+ # end
75
+ # end
76
+ #
77
+ # context 'with advanced mode' do
78
+ # let(:mode) { :advanced }
79
+ #
80
+ # context 'scenario C' do
81
+ # it { expect(result).to eq('C') }
82
+ # end
83
+ # end
84
+ # end
85
+ #
86
+ # @example Configuration
87
+ # # To disable warnings for partial duplicates:
88
+ # RSpecGuide/DuplicateLetValues:
89
+ # WarnOnPartialDuplicates: false # Only report full duplicates
90
+ #
91
+ # @example Edge case - different values
92
+ # # good - same let name but different values (no duplicate)
93
+ # describe 'Converter' do
94
+ # context 'to USD' do
95
+ # let(:currency) { :usd }
96
+ # it { expect(convert).to eq(100) }
97
+ # end
98
+ #
99
+ # context 'to EUR' do
100
+ # let(:currency) { :eur } # Different value, OK
101
+ # it { expect(convert).to eq(85) }
102
+ # end
103
+ # end
104
+ #
105
+ class DuplicateLetValues < RuboCop::Cop::RSpec::Base
37
106
  MSG_ERROR = "Duplicate `let(:%<name>s)` with same value `%<value>s` " \
38
107
  "in ALL sibling contexts. Extract to parent context."
39
108
  MSG_WARNING = "Let `:%<name>s` with value `%<value>s` duplicated in %<count>d/%<total>d contexts. " \
40
109
  "Consider refactoring test hierarchy - this suggests poor organization."
41
110
 
42
- # @!method context_block?(node)
43
- def_node_matcher :context_block?, <<~PATTERN
111
+ # Using rubocop-rspec API: example_group?(node) from Base
112
+ # Custom matchers:
113
+
114
+ # @!method context_only?(node)
115
+ def_node_matcher :context_only?, <<~PATTERN
44
116
  (block (send nil? :context ...) ...)
45
117
  PATTERN
46
118
 
47
- # @!method let_declaration?(node)
48
- def_node_matcher :let_declaration?, <<~PATTERN
119
+ # @!method let_with_name_and_value?(node)
120
+ def_node_matcher :let_with_name_and_value?, <<~PATTERN
49
121
  (block
50
122
  (send nil? {:let :let!} (sym $_name))
51
123
  (args)
52
124
  $_value)
53
125
  PATTERN
54
126
 
55
- # @!method example_group?(node)
56
- def_node_matcher :example_group?, <<~PATTERN
57
- (block
58
- (send nil? {:describe :context} ...)
59
- ...)
60
- PATTERN
61
-
62
127
  def on_block(node)
128
+ # Fast pre-check: only process describe/context blocks
129
+ return unless node.method?(:describe) || node.method?(:context)
63
130
  return unless example_group?(node)
64
131
 
65
132
  # Collect all sibling contexts
@@ -86,8 +153,8 @@ module RuboCop
86
153
 
87
154
  if body.begin_type?
88
155
  # Multiple children wrapped in begin node
89
- body.children.select { |child| child.block_type? && context_block?(child) }
90
- elsif body.block_type? && context_block?(body)
156
+ body.children.select { |child| child.block_type? && context_only?(child) }
157
+ elsif body.block_type? && context_only?(body)
91
158
  # Single context child
92
159
  [body]
93
160
  else
@@ -105,7 +172,7 @@ module RuboCop
105
172
  (block_node.parent.begin_type? && block_node.parent.parent == context_node)
106
173
  next unless is_immediate_child
107
174
 
108
- let_declaration?(block_node) do |name, value|
175
+ let_with_name_and_value?(block_node) do |name, value|
109
176
  # Only check simple values that can be compared by source
110
177
  if simple_value?(value)
111
178
  lets[name] = {value: value.source, node: block_node}
@@ -4,55 +4,88 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpecGuide
6
6
  # Checks that corner cases are not the first context in a describe block.
7
- # Happy path should come first for better readability.
7
+ #
8
+ # Placing happy path scenarios first improves test readability by establishing
9
+ # the expected behavior before diving into edge cases. This makes it easier for
10
+ # readers to understand the primary purpose of the code being tested.
8
11
  #
9
12
  # The cop allows corner case contexts to appear first if there are
10
13
  # example blocks (it/specify) before the first context, as those examples
11
14
  # represent the happy path.
12
15
  #
13
- # @example
14
- # # bad
16
+ # @safety
17
+ # This cop is safe to run automatically. It detects corner case indicators
18
+ # like 'but', 'however', 'not', 'without', 'except', etc.
19
+ #
20
+ # @example Bad - corner case first
21
+ # # bad - starts with negative case
15
22
  # describe '#process' do
16
23
  # context 'but user is blocked' do
17
- # # ...
24
+ # it { expect { process }.to raise_error }
18
25
  # end
26
+ #
19
27
  # context 'when user is valid' do
20
- # # ...
28
+ # it { expect(process).to be_success }
21
29
  # end
22
30
  # end
23
31
  #
24
- # # bad
32
+ # # bad - starts with NOT condition
25
33
  # describe '#activate' do
26
34
  # context 'when user does NOT exist' do
27
- # # ...
35
+ # it { expect { activate }.to raise_error(NotFound) }
28
36
  # end
37
+ #
29
38
  # context 'when user exists' do
30
- # # ...
39
+ # it { expect(activate).to be_truthy }
31
40
  # end
32
41
  # end
33
42
  #
34
- # # good
43
+ # @example Good - happy path first
44
+ # # good - happy path comes first
35
45
  # describe '#subscribe' do
36
46
  # context 'with valid card' do
37
- # # ...
47
+ # it { expect(subscribe).to be_success }
38
48
  # end
49
+ #
39
50
  # context 'but payment fails' do
40
- # # ...
51
+ # it { expect(subscribe).to be_failure }
52
+ # end
53
+ # end
54
+ #
55
+ # # good - positive case before negative
56
+ # describe '#send_notification' do
57
+ # context 'when user has email' do
58
+ # it { expect(send_notification).to be_sent }
59
+ # end
60
+ #
61
+ # context 'without email' do
62
+ # it { expect(send_notification).to be_skipped }
41
63
  # end
42
64
  # end
43
65
  #
66
+ # @example Edge case - it-blocks represent happy path
44
67
  # # good - examples before first context represent happy path
45
68
  # describe '#add_child' do
46
69
  # it 'adds child to children collection' do
47
- # # ...
70
+ # expect { add_child(child) }.to change(parent.children, :count).by(1)
48
71
  # end
49
72
  #
50
73
  # context 'but child is already in collection' do
51
- # # ...
74
+ # it { expect { add_child(child) }.not_to change(parent.children, :count) }
52
75
  # end
53
76
  # end
54
77
  #
55
- class HappyPathFirst < Base
78
+ # # good - multiple it-blocks as happy path
79
+ # describe '#calculate' do
80
+ # it { expect(calculate).to be_a(Numeric) }
81
+ # it { expect(calculate).to be_positive }
82
+ #
83
+ # context 'with invalid input' do
84
+ # it { expect { calculate }.to raise_error }
85
+ # end
86
+ # end
87
+ #
88
+ class HappyPathFirst < RuboCop::Cop::RSpec::Base
56
89
  MSG = "Place happy path contexts before corner cases. " \
57
90
  "First context appears to be a corner case: %<description>s"
58
91
 
@@ -62,6 +95,9 @@ module RuboCop
62
95
  fails missing absent unavailable
63
96
  ].freeze
64
97
 
98
+ # Using rubocop-rspec API: example_group?(node) from Base
99
+ # Custom matcher for context with description:
100
+
65
101
  # @!method context_with_description?(node)
66
102
  def_node_matcher :context_with_description?, <<~PATTERN
67
103
  (block
@@ -69,14 +105,9 @@ module RuboCop
69
105
  ...)
70
106
  PATTERN
71
107
 
72
- # @!method example_group?(node)
73
- def_node_matcher :example_group?, <<~PATTERN
74
- (block
75
- (send nil? {:describe :context} ...)
76
- ...)
77
- PATTERN
78
-
79
108
  def on_block(node)
109
+ # Fast pre-check: only process describe/context blocks
110
+ return unless node.method?(:describe) || node.method?(:context)
80
111
  return unless example_group?(node)
81
112
 
82
113
  contexts = collect_direct_child_contexts(node)
@@ -4,10 +4,21 @@ module RuboCop
4
4
  module Cop
5
5
  module RSpecGuide
6
6
  # Detects examples that repeat in all leaf contexts.
7
- # These invariants should be extracted to shared_examples.
8
7
  #
9
- # @example
10
- # # bad
8
+ # When the same test appears in all leaf contexts, it indicates an invariant -
9
+ # a property that holds true regardless of the context. These invariants represent
10
+ # interface contracts and should be extracted to shared_examples for reusability
11
+ # and clarity.
12
+ #
13
+ # The cop only reports when examples appear in MinLeafContexts or more contexts
14
+ # (default: 3) to avoid false positives.
15
+ #
16
+ # @safety
17
+ # This cop is safe to run automatically. It compares example descriptions
18
+ # for exact string matches.
19
+ #
20
+ # @example Bad - repeated invariant
21
+ # # bad - 'responds to valid?' repeated in all 3 contexts
11
22
  # describe 'Validator' do
12
23
  # context 'with valid data' do
13
24
  # it 'responds to valid?' do
@@ -28,7 +39,8 @@ module RuboCop
28
39
  # end
29
40
  # end
30
41
  #
31
- # # good
42
+ # @example Good - extracted to shared_examples
43
+ # # good - invariant extracted to shared_examples
32
44
  # shared_examples 'a validator' do
33
45
  # it 'responds to valid?' do
34
46
  # expect(subject).to respond_to(:valid?)
@@ -38,44 +50,72 @@ module RuboCop
38
50
  # describe 'Validator' do
39
51
  # context 'with valid data' do
40
52
  # it_behaves_like 'a validator'
53
+ # it { expect(subject.valid?).to be true }
41
54
  # end
42
55
  #
43
56
  # context 'with invalid data' do
44
57
  # it_behaves_like 'a validator'
58
+ # it { expect(subject.valid?).to be false }
45
59
  # end
46
60
  #
47
61
  # context 'with empty data' do
48
62
  # it_behaves_like 'a validator'
63
+ # it { expect(subject.valid?).to be false }
49
64
  # end
50
65
  # end
51
66
  #
52
- class InvariantExamples < Base
67
+ # @example Configuration
68
+ # # Adjust minimum contexts threshold:
69
+ # RSpecGuide/InvariantExamples:
70
+ # MinLeafContexts: 3 # Default: report if in 3+ contexts
71
+ #
72
+ # # For larger test suites, use higher threshold:
73
+ # RSpecGuide/InvariantExamples:
74
+ # MinLeafContexts: 5 # Only report if in 5+ contexts
75
+ #
76
+ # @example Edge case - not in all contexts
77
+ # # good - test only in 2 out of 3 contexts (not invariant)
78
+ # describe 'Calculator' do
79
+ # context 'with addition' do
80
+ # it 'returns numeric' { expect(result).to be_a(Numeric) }
81
+ # end
82
+ #
83
+ # context 'with subtraction' do
84
+ # it 'returns numeric' { expect(result).to be_a(Numeric) }
85
+ # end
86
+ #
87
+ # context 'with division by zero' do
88
+ # it 'raises error' { expect { result }.to raise_error }
89
+ # # 'returns numeric' not here - not an invariant
90
+ # end
91
+ # end
92
+ #
93
+ class InvariantExamples < RuboCop::Cop::RSpec::Base
53
94
  MSG = "Example `%<description>s` repeats in all %<count>d leaf contexts. " \
54
95
  "Consider extracting to shared_examples as an interface invariant."
55
96
 
56
- # @!method example_with_description?(node)
57
- def_node_matcher :example_with_description?, <<~PATTERN
58
- (block
59
- (send nil? {:it :specify :example} (str $_description))
60
- ...)
61
- PATTERN
97
+ # Using rubocop-rspec API: example_group?(node) from Base for top-level check
98
+ # Custom matchers for performance-critical internal checks:
62
99
 
63
- # @!method context_or_describe?(node)
64
- def_node_matcher :context_or_describe?, <<~PATTERN
100
+ # Fast local matcher for nested context/describe checks (performance-critical)
101
+ # @!method context_or_describe_block?(node)
102
+ def_node_matcher :context_or_describe_block?, <<~PATTERN
65
103
  (block
66
104
  (send nil? {:describe :context} ...)
67
105
  ...)
68
106
  PATTERN
69
107
 
70
- # @!method top_level_describe?(node)
71
- def_node_matcher :top_level_describe?, <<~PATTERN
108
+ # @!method example_with_description?(node)
109
+ def_node_matcher :example_with_description?, <<~PATTERN
72
110
  (block
73
- (send nil? :describe ...)
111
+ (send nil? {:it :specify :example} (str $_description))
74
112
  ...)
75
113
  PATTERN
76
114
 
77
115
  def on_block(node)
78
- return unless top_level_describe?(node)
116
+ # Fast pre-check: only process top-level describe (not context)
117
+ return unless node.method?(:describe)
118
+ return unless example_group?(node)
79
119
 
80
120
  # Find all leaf contexts (contexts with no nested contexts)
81
121
  leaf_contexts = find_leaf_contexts(node)
@@ -113,11 +153,12 @@ module RuboCop
113
153
  leaves = []
114
154
 
115
155
  node.each_descendant(:block) do |child|
116
- next unless context_or_describe?(child)
156
+ # Use fast local matcher for performance-critical nested checks
157
+ next unless context_or_describe_block?(child)
117
158
 
118
159
  # Check if this context has nested contexts
119
160
  has_nested = child.each_descendant(:block).any? do |nested|
120
- context_or_describe?(nested) && nested != child
161
+ context_or_describe_block?(nested) && nested != child
121
162
  end
122
163
 
123
164
  leaves << child unless has_nested
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module RSpecGuide
6
+ # Checks that describe blocks test at least 2 behavioral variations.
7
+ #
8
+ # Testing only a single scenario (happy path OR edge case) provides
9
+ # insufficient coverage. Tests should verify both expected behavior
10
+ # and edge case handling to ensure comprehensive validation.
11
+ #
12
+ # This can be achieved in two ways:
13
+ # 1. Use 2+ sibling context blocks (happy path + edge cases)
14
+ # 2. Combine it-blocks (default behavior) with context-blocks (edge cases)
15
+ #
16
+ # @safety
17
+ # This cop is safe to run automatically. For simple methods like getters
18
+ # with no edge cases, use `# rubocop:disable RSpecGuide/MinimumBehavioralCoverage`
19
+ #
20
+ # @example Traditional approach - 2+ sibling contexts
21
+ # # bad - only one scenario (no edge case testing)
22
+ # describe '#calculate' do
23
+ # context 'with valid data' do
24
+ # it { expect(result).to eq(100) }
25
+ # end
26
+ # end
27
+ #
28
+ # # good - multiple scenarios (happy path + edge cases)
29
+ # describe '#calculate' do
30
+ # context 'with valid data' do
31
+ # it { expect(result).to eq(100) }
32
+ # end
33
+ #
34
+ # context 'with invalid data' do
35
+ # it { expect { result }.to raise_error(ValidationError) }
36
+ # end
37
+ # end
38
+ #
39
+ # # good - more comprehensive coverage
40
+ # describe '#calculate' do
41
+ # context 'with positive numbers' do
42
+ # it { expect(result).to eq(100) }
43
+ # end
44
+ #
45
+ # context 'with zero' do
46
+ # it { expect(result).to eq(0) }
47
+ # end
48
+ #
49
+ # context 'with negative numbers' do
50
+ # it { expect { result }.to raise_error(ArgumentError) }
51
+ # end
52
+ # end
53
+ #
54
+ # @example New pattern - it-blocks + context-blocks
55
+ # # bad - only default behavior, no edge cases
56
+ # describe '#calculate' do
57
+ # it 'calculates sum' { expect(result).to eq(100) }
58
+ # end
59
+ #
60
+ # # good - default behavior + edge case
61
+ # describe '#calculate' do
62
+ # it 'calculates sum with defaults' { expect(result).to eq(100) }
63
+ #
64
+ # context 'with invalid input' do
65
+ # it { expect { result }.to raise_error(ValidationError) }
66
+ # end
67
+ # end
68
+ #
69
+ # # good - multiple it-blocks for defaults, context for edge case
70
+ # describe '#calculate' do
71
+ # it 'returns numeric result' { expect(result).to be_a(Numeric) }
72
+ # it 'is positive' { expect(result).to be > 0 }
73
+ #
74
+ # context 'with special conditions' do
75
+ # it { expect(result).to eq(0) }
76
+ # end
77
+ # end
78
+ #
79
+ # @example Edge case - setup before tests (allowed)
80
+ # # good - setup + it-blocks + contexts
81
+ # describe '#calculate' do
82
+ # let(:calculator) { Calculator.new }
83
+ # before { calculator.configure }
84
+ #
85
+ # it 'works with defaults' { expect(result).to eq(100) }
86
+ #
87
+ # context 'with custom config' do
88
+ # it { expect(result).to eq(200) }
89
+ # end
90
+ # end
91
+ #
92
+ # @example When to disable this cop
93
+ # # Simple getter with no edge cases - disable is acceptable
94
+ # describe '#name' do # rubocop:disable RSpecGuide/MinimumBehavioralCoverage
95
+ # it { expect(subject.name).to eq('test') }
96
+ # end
97
+ #
98
+ class MinimumBehavioralCoverage < RuboCop::Cop::RSpec::Base
99
+ MSG = "Describe block should test at least 2 behavioral variations: " \
100
+ "either use 2+ sibling contexts (happy path + edge cases), " \
101
+ "or combine it-blocks for default behavior with context-blocks for edge cases. " \
102
+ "Use `# rubocop:disable RSpecGuide/MinimumBehavioralCoverage` " \
103
+ "for simple cases (e.g., getters) with no edge cases."
104
+
105
+ # Using rubocop-rspec API matchers (inherited from RuboCop::Cop::RSpec::Base):
106
+ # - example_group?(node) - checks describe/context blocks
107
+ # - example?(node) - checks it/specify blocks
108
+ # Custom matcher for context-only:
109
+
110
+ # @!method context_only?(node)
111
+ def_node_matcher :context_only?, <<~PATTERN
112
+ (block (send nil? :context ...) ...)
113
+ PATTERN
114
+
115
+ def on_block(node)
116
+ # Fast pre-check: only process describe blocks (not context)
117
+ return unless node.method?(:describe)
118
+ return unless example_group?(node)
119
+
120
+ children = collect_children(node)
121
+ contexts = children.select { |child| context_only?(child) }
122
+ its = children.select { |child| example?(child) }
123
+
124
+ # Valid if: 2+ contexts OR (1+ it-blocks before contexts AND 1+ contexts)
125
+ return if contexts.size >= 2
126
+ return if valid_it_then_context_pattern?(children, its, contexts)
127
+
128
+ add_offense(node)
129
+ end
130
+
131
+ private
132
+
133
+ def collect_children(node)
134
+ # The body of a describe/context block may be:
135
+ # 1. A single block node (if only one child)
136
+ # 2. A begin node containing multiple children
137
+ body = node.body
138
+ return [] unless body
139
+
140
+ if body.begin_type?
141
+ # Multiple children wrapped in begin node
142
+ body.children.select(&:block_type?)
143
+ elsif body.block_type?
144
+ # Single child
145
+ [body]
146
+ else
147
+ []
148
+ end
149
+ end
150
+
151
+ def valid_it_then_context_pattern?(children, its, contexts)
152
+ # Need at least one it-block and at least one context-block
153
+ return false if its.empty? || contexts.empty?
154
+
155
+ # Find positions of first it-block and first context-block
156
+ first_it_index = children.index { |child| example?(child) }
157
+ first_context_index = children.index { |child| context_only?(child) }
158
+
159
+ # All it-blocks must come before all context-blocks
160
+ first_it_index < first_context_index
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Inject our default configuration into RuboCop
4
+ # This ensures config/default.yml is loaded automatically
5
+ module RuboCop
6
+ module RSpec
7
+ module Guide
8
+ # Inject default configuration
9
+ module Inject
10
+ DEFAULT_FILE = File.expand_path("../../../../../config/default.yml", __FILE__)
11
+
12
+ def self.defaults!
13
+ path = DEFAULT_FILE
14
+ hash = ConfigLoader.send(:load_yaml_configuration, path)
15
+ config = Config.new(hash, path).tap(&:make_excludes_absolute)
16
+ puts "configuration from #{path}" if ConfigLoader.debug?
17
+ config = ConfigLoader.merge_with_default(config, path)
18
+ ConfigLoader.instance_variable_set(:@default_configuration, config)
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ # Inject defaults when the gem is loaded
26
+ RuboCop::RSpec::Guide::Inject.defaults!
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module RSpec
5
+ module Guide
6
+ # Plugin integration for RuboCop 1.72+
7
+ #
8
+ # This class provides the modern plugin API for RuboCop.
9
+ # It allows loading the gem via the `plugins:` configuration
10
+ # in .rubocop.yml instead of the legacy `require:` approach.
11
+ #
12
+ # @example Modern approach (RuboCop 1.72+)
13
+ # # .rubocop.yml
14
+ # plugins:
15
+ # - rubocop-rspec-guide
16
+ #
17
+ # @example Legacy approach (still supported)
18
+ # # .rubocop.yml
19
+ # require:
20
+ # - rubocop-rspec-guide
21
+ class Plugin
22
+ # Initialize the plugin
23
+ #
24
+ # @param config [RuboCop::Config, nil] the RuboCop configuration
25
+ def initialize(config = nil)
26
+ @config = config
27
+ end
28
+
29
+ # Plugin metadata
30
+ #
31
+ # @return [String] the plugin name
32
+ def name
33
+ "rubocop-rspec-guide"
34
+ end
35
+
36
+ # Plugin version
37
+ #
38
+ # @return [String] the plugin version
39
+ def version
40
+ RuboCop::RSpec::Guide::VERSION
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -3,7 +3,7 @@
3
3
  module RuboCop
4
4
  module RSpec
5
5
  module Guide
6
- VERSION = "0.2.2"
6
+ VERSION = "0.4.0"
7
7
  end
8
8
  end
9
9
  end