rubocop-vicenzo 0.6.0 → 0.8.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: a5b82e09fef23aac921adb4f653bfbdc1117af279326d24339f68db8abe25ffd
4
- data.tar.gz: f5f4d255aacd3851e4f8b7968b663d22db0b0ed9be99bab01a17df43d819c80f
3
+ metadata.gz: '094cfb18e36f28e2f311e80f435b1b5159f22875cfb8f13b5a0551ca9b5e9218'
4
+ data.tar.gz: b96a9634c956e7cec399e10a2f7f512a5f2894822ba07fecf7294a502fee56b5
5
5
  SHA512:
6
- metadata.gz: 79d36dbf73503ca38cca4d5ffd77fd84d56cf4b8d320ff389035c359116e36d66d15a3aad4fd3de10d9787fe29f4b4af66b0b669ce98754415fac8ad954106fa
7
- data.tar.gz: 84055ca6301311a752670e04fa5fcd9646fc38844993428a5f952be17a7384cfa5842b9a2daaa7f3be3242d70e0641b7ba02fdd718fee111f995bd1cd4a8b3e0
6
+ metadata.gz: ba554984ec7092154352415043f2868cd59bd89015de29b0098e01c1f0b2016b8b3a837845cd22e8219e1115b37f4b6e80eb3d5861d0f3325c0ad2894466de23
7
+ data.tar.gz: 031a968654d68f7fa65e7a57a6178fc97651ae8c20cb4861f25a1f4c9feb5c6d044a3bc7e269e4de392a9a38483ce51296517f852abe61e88ba804ad723140b4
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.0"
2
+ ".": "0.8.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.8.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.7.0...v0.8.0) (2026-09-09)
4
+
5
+
6
+ ### Features
7
+
8
+ * add RSpec cops against top-level declarations ([#31](https://github.com/bvicenzo/rubocop-vicenzo/issues/31)) ([ef5e207](https://github.com/bvicenzo/rubocop-vicenzo/commit/ef5e20728c0c4bc94dbb5eec54a48ff5197d1b25))
9
+
10
+ ## [0.7.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.6.0...v0.7.0) (2026-08-31)
11
+
12
+
13
+ ### Features
14
+
15
+ * add RSpec cops against subjects hidden in lets ([#30](https://github.com/bvicenzo/rubocop-vicenzo/issues/30)) ([9daf259](https://github.com/bvicenzo/rubocop-vicenzo/commit/9daf25972252fdfdb92a51bba9cfe70e63446d17))
16
+ * add Vicenzo/RSpec/SubjectIsMethodResult cop ([#28](https://github.com/bvicenzo/rubocop-vicenzo/issues/28)) ([b69245a](https://github.com/bvicenzo/rubocop-vicenzo/commit/b69245af973f3341bbe62bca628333c5acb909d7))
17
+
3
18
  ## [0.6.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.5.0...v0.6.0) (2026-08-19)
4
19
 
5
20
 
data/README.md CHANGED
@@ -11,7 +11,7 @@ Add it to the `development`/`test` group of your `Gemfile`:
11
11
  <!-- x-release-please-start-version -->
12
12
  ```ruby
13
13
  group :development, :test do
14
- gem 'rubocop-vicenzo', '~> 0.6.0', require: false
14
+ gem 'rubocop-vicenzo', '~> 0.8.0', require: false
15
15
  end
16
16
  ```
17
17
  <!-- x-release-please-end-version -->
data/config/default.yml CHANGED
@@ -11,6 +11,18 @@ Vicenzo/Rails/EnumInclusionOfValidation:
11
11
  Severity: convention
12
12
  VersionAdded: '0.1.0'
13
13
 
14
+ Vicenzo/RSpec/CompetingSubjects:
15
+ Description: 'Definitions the expectations assert on, two or more in a group declaring no subject, are either hidden contexts or a premise that belongs in a before.'
16
+ Enabled: true
17
+ Severity: convention
18
+ AllowedMethods: []
19
+ Include:
20
+ - '**/spec/**/*_spec.rb'
21
+ Exclude:
22
+ - '**/spec/support/**/*'
23
+ - '**/spec/factories/**/*'
24
+ VersionAdded: '0.7.0'
25
+
14
26
  Vicenzo/RSpec/ConditionalInSpec:
15
27
  Description: 'Do not use conditional logic in specs. Extract each branch into an explicit context instead.'
16
28
  Enabled: true
@@ -91,6 +103,97 @@ Vicenzo/RSpec/LeakyDefinition:
91
103
  - '**/spec/support/**/*'
92
104
  - '**/spec/factories/**/*'
93
105
 
106
+ Vicenzo/RSpec/LetIsMethodResult:
107
+ Description: 'A let under an example group describing a method must not hold the result of calling that method on the subject.'
108
+ Enabled: true
109
+ Severity: convention
110
+ AllowedMethods: []
111
+ Include:
112
+ - '**/spec/**/*_spec.rb'
113
+ Exclude:
114
+ - '**/spec/support/**/*'
115
+ - '**/spec/factories/**/*'
116
+ VersionAdded: '0.7.0'
117
+
118
+ Vicenzo/RSpec/SubjectDefinedAsLet:
119
+ Description: 'A let the expectations assert on, in a group declaring no subject, must be declared as the subject.'
120
+ Enabled: true
121
+ Severity: convention
122
+ AllowedMethods: []
123
+ Include:
124
+ - '**/spec/**/*_spec.rb'
125
+ Exclude:
126
+ - '**/spec/support/**/*'
127
+ - '**/spec/factories/**/*'
128
+ VersionAdded: '0.7.0'
129
+
130
+ Vicenzo/RSpec/SubjectIsMethodResult:
131
+ Description: 'The subject of an example group describing a method must be the object under test, not the method result.'
132
+ Enabled: true
133
+ Severity: convention
134
+ AllowedMethods: []
135
+ Include:
136
+ - '**/spec/**/*_spec.rb'
137
+ Exclude:
138
+ - '**/spec/support/**/*'
139
+ - '**/spec/factories/**/*'
140
+ VersionAdded: '0.7.0'
141
+
142
+ Vicenzo/RSpec/TopLevelContext:
143
+ Description: 'A context must not be declared in the top-level example group, where it is a scenario for every example in the file.'
144
+ Enabled: true
145
+ Severity: warning
146
+ Include:
147
+ - '**/spec/**/*_spec.rb'
148
+ Exclude:
149
+ - '**/spec/support/**/*'
150
+ - '**/spec/factories/**/*'
151
+ VersionAdded: '0.8.0'
152
+
153
+ Vicenzo/RSpec/TopLevelExample:
154
+ Description: 'An example must not be declared in the top-level example group, which names no behaviour.'
155
+ Enabled: true
156
+ Severity: warning
157
+ Include:
158
+ - '**/spec/**/*_spec.rb'
159
+ Exclude:
160
+ - '**/spec/support/**/*'
161
+ - '**/spec/factories/**/*'
162
+ VersionAdded: '0.8.0'
163
+
164
+ Vicenzo/RSpec/TopLevelHook:
165
+ Description: 'A hook must not be declared in the top-level example group, where it runs for every example in the file.'
166
+ Enabled: true
167
+ Severity: warning
168
+ Include:
169
+ - '**/spec/**/*_spec.rb'
170
+ Exclude:
171
+ - '**/spec/support/**/*'
172
+ - '**/spec/factories/**/*'
173
+ VersionAdded: '0.8.0'
174
+
175
+ Vicenzo/RSpec/TopLevelLet:
176
+ Description: 'A let must not be declared in the top-level example group, where it reaches every example in the file.'
177
+ Enabled: true
178
+ Severity: warning
179
+ Include:
180
+ - '**/spec/**/*_spec.rb'
181
+ Exclude:
182
+ - '**/spec/support/**/*'
183
+ - '**/spec/factories/**/*'
184
+ VersionAdded: '0.8.0'
185
+
186
+ Vicenzo/RSpec/TopLevelSubject:
187
+ Description: 'A subject must not be declared in the top-level example group, where it reaches every example in the file.'
188
+ Enabled: true
189
+ Severity: warning
190
+ Include:
191
+ - '**/spec/**/*_spec.rb'
192
+ Exclude:
193
+ - '**/spec/support/**/*'
194
+ - '**/spec/factories/**/*'
195
+ VersionAdded: '0.8.0'
196
+
94
197
  Vicenzo/Style/JsonParseSymbolizeNames:
95
198
  Description: 'Enforces passing `symbolize_names: true` to `JSON.parse` so keys are symbols.'
96
199
  Enabled: true
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/described_method'
4
+ require_relative 'mixin/expectation_target'
5
+ require_relative 'mixin/premise_tracking'
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Vicenzo
10
+ module RSpec
11
+ # Two or more definitions the expectations assert on, in a group that declares no subject, are
12
+ # contexts that were never written.
13
+ #
14
+ # A group speaks about one subject. When its examples each assert on a different definition, the
15
+ # group is not one scenario with several facts: it is several scenarios sharing a roof, and the
16
+ # circumstance that tells them apart - the one a `context` would have named - lives only in the
17
+ # definitions' names and in the reader's head.
18
+ #
19
+ # The fix is structural, and it is never to pick one of them to promote. Which shape it takes
20
+ # depends on what the definitions are to this group, and working that out is the whole job -
21
+ # which is why the offense names what was found rather than prescribing a repair:
22
+ #
23
+ # 1. They are different scenarios. Give each its own `context`, saying out loud the circumstance
24
+ # it stands for, and let each declare the subject it is about. What was encoded in
25
+ # `record_1` / `record_2` becomes a sentence, and the example shrinks to the outcome alone.
26
+ # 2. One outcome covers them all. Then they were never subjects: they are the premise. Move them
27
+ # into a `before`, unnamed, and assert on the collection they belong to.
28
+ #
29
+ # Both are corrections to the spec. Neither is silencing the cop: a definition named
30
+ # `record_1` is the shape this smell takes, so renaming it settles nothing, and an inline
31
+ # disable directive keeps the hidden scenario hidden - which is the cost being paid here.
32
+ #
33
+ # A group that already declares a subject is left alone - there the subject is settled and the
34
+ # other definitions are premises, whatever their names suggest.
35
+ #
36
+ # @example
37
+ # # bad - two subjects under one roof; the circumstances are only in the names
38
+ # describe '#approved?' do
39
+ # let!(:reviewed_post) { create(:post, reviewed_at: Time.current) }
40
+ # let!(:draft_post) { create(:post) }
41
+ #
42
+ # it { expect(reviewed_post).to be_approved }
43
+ # it { expect(draft_post).not_to be_approved }
44
+ # end
45
+ #
46
+ # # good - one context per scenario, each with its own subject
47
+ # describe '#approved?' do
48
+ # context 'when the post has been reviewed' do
49
+ # subject(:post) { create(:post, reviewed_at: Time.current) }
50
+ #
51
+ # it { is_expected.to be_approved }
52
+ # end
53
+ #
54
+ # context 'without a review' do
55
+ # subject(:post) { create(:post) }
56
+ #
57
+ # it { is_expected.not_to be_approved }
58
+ # end
59
+ # end
60
+ #
61
+ # @example when one outcome covers every record, they are the premise
62
+ # # bad - two records named as if each were a subject
63
+ # describe '#deactivate' do
64
+ # let!(:pending_alert) { create(:alert, :pending) }
65
+ # let!(:firing_alert) { create(:alert, :firing) }
66
+ #
67
+ # it 'dismisses every alert', :aggregate_failures do
68
+ # monitor.deactivate
69
+ #
70
+ # expect(pending_alert.reload).to be_dismissed
71
+ # expect(firing_alert.reload).to be_dismissed
72
+ # end
73
+ # end
74
+ #
75
+ # # good - the records are the premise, and the outcome is about the collection
76
+ # describe '#deactivate' do
77
+ # before do
78
+ # create(:alert, :pending)
79
+ # create(:alert, :firing)
80
+ # end
81
+ #
82
+ # it 'dismisses every alert' do
83
+ # monitor.deactivate
84
+ #
85
+ # expect(monitor.alerts).to contain_exactly(be_dismissed, be_dismissed)
86
+ # end
87
+ # end
88
+ #
89
+ # @example records a single expectation compares are not competing subjects
90
+ # # good - both records build the one answer the example is about
91
+ # describe '.recent' do
92
+ # let!(:recent_order) { create(:order, created_at: 1.day.ago) }
93
+ # let!(:old_order) { create(:order, created_at: 1.year.ago) }
94
+ #
95
+ # it { expect(described_class.recent).to contain_exactly(recent_order) }
96
+ # end
97
+ class CompetingSubjects < RuboCop::Cop::RSpec::Base
98
+ include DescribedMethod
99
+ include ExpectationTarget
100
+ include PremiseTracking
101
+
102
+ MSG = 'The expectations here are about %<names>s, so this group has no one subject. Work out ' \
103
+ 'what these definitions are to it, and let the structure say so.'
104
+
105
+ def on_block(node)
106
+ return unless example_group?(node)
107
+
108
+ names = competing_names(node)
109
+
110
+ return if names.size < 2
111
+
112
+ add_offense(node.send_node, message: format(MSG, names: names.join(', ')))
113
+ end
114
+
115
+ alias on_numblock on_block
116
+ alias on_itblock on_block
117
+
118
+ private
119
+
120
+ def competing_names(group)
121
+ return [] if declares_subject?(group)
122
+
123
+ asserted_definitions(group).reject { |name| exempt?(group, name) }.map { |name| "`:#{name}`" }
124
+ end
125
+
126
+ def exempt?(group, name)
127
+ return false if allowed_methods.empty?
128
+
129
+ definition = definition_named(group, name)
130
+
131
+ !definition.nil? && !call_in_chain(definition.body) do |call|
132
+ allowed_methods.include?(call.method_name.to_s)
133
+ end.nil?
134
+ end
135
+
136
+ def definition_named(group, name)
137
+ found = nil
138
+
139
+ each_child_block(group) do |child|
140
+ found ||= child if premise?(child) && premise_name(child)&.to_sym == name
141
+ end
142
+
143
+ found
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/described_method'
4
+ require_relative 'mixin/premise_tracking'
5
+
6
+ module RuboCop
7
+ module Cop
8
+ module Vicenzo
9
+ module RSpec
10
+ # A `let` under an example group that describes a method must not hold what calling that method
11
+ # on the subject returned.
12
+ #
13
+ # This is the same inversion `Vicenzo/RSpec/SubjectIsMethodResult` reports, moved one definition
14
+ # away: the subject stays the object under test, and the result is parked in a `let` that the
15
+ # expectation then asserts on. What the example says out loud becomes a name, and the action -
16
+ # the subject receiving the method - is nowhere in the example that is supposed to describe it.
17
+ #
18
+ # The fix is not to rename the `let` or to move it: it is to undo it. Even when the `let` is read
19
+ # inside an `expect`, what it holds belongs in the expectation itself, where the sentence reads
20
+ # whole - the cat catches the ball, and that is what is asserted.
21
+ #
22
+ # Only calls on the subject count, which is what separates the result from the setup: a `let`
23
+ # calling the described method on some other object is building a premise, not the outcome the
24
+ # example is about. `AllowedMethods` is shared with the sibling cop and ships empty, for the
25
+ # layers whose convention is a single entry point.
26
+ #
27
+ # @example
28
+ # # bad - the expectation asserts on a name; the cat never catches anything in the example
29
+ # describe '#catch' do
30
+ # subject(:cat) { Cat.new(name: 'Bixano') }
31
+ #
32
+ # let(:catch) { cat.catch(object: Ball.new) }
33
+ #
34
+ # it { expect(catch).to eq(:success) }
35
+ # end
36
+ #
37
+ # # good - the action is in the expectation
38
+ # describe '#catch' do
39
+ # subject(:cat) { Cat.new(name: 'Bixano') }
40
+ #
41
+ # it { expect(cat.catch(object: Ball.new)).to eq(:success) }
42
+ # end
43
+ #
44
+ # @example a call on another object is a premise, not the result
45
+ # # good - the described method builds the state the example starts from
46
+ # describe '#catch' do
47
+ # subject(:cat) { Cat.new(name: 'Bixano') }
48
+ #
49
+ # let(:taken_ball) { other_cat.catch(object: Ball.new) }
50
+ # end
51
+ class LetIsMethodResult < RuboCop::Cop::RSpec::Base
52
+ include DescribedMethod
53
+ include PremiseTracking
54
+
55
+ MSG = 'Let `:%<name>s` holds what `%<method>s` returned. Undo it and call `%<method>s` on the ' \
56
+ 'subject inside the expectation.'
57
+
58
+ def on_block(node)
59
+ return unless let?(node) || let_it_be?(node)
60
+
61
+ method_name = described_method_name(node)
62
+ return if method_name.nil? || allowed_methods.include?(method_name)
63
+
64
+ invocation = invocation_on_subject(node, method_name)
65
+ return unless invocation
66
+
67
+ add_offense(invocation, message: format(MSG, name: premise_name(node), method: method_name))
68
+ end
69
+
70
+ alias on_numblock on_block
71
+ alias on_itblock on_block
72
+
73
+ private
74
+
75
+ def invocation_on_subject(node, method_name)
76
+ names = subject_names(node)
77
+
78
+ call_in_chain(node.body) do |call|
79
+ call.method?(method_name) && subject_reference?(call.receiver, names)
80
+ end
81
+ end
82
+
83
+ # `subject` always refers to the subject, declared or implicit; a named one answers to its name
84
+ # as well.
85
+ def subject_names(node)
86
+ names = Set[:subject]
87
+
88
+ node.each_ancestor(:any_block).each do |ancestor|
89
+ next unless example_group?(ancestor)
90
+
91
+ each_child_block(ancestor) { |child| names << subject_name(child) if subject?(child) }
92
+ end
93
+
94
+ names
95
+ end
96
+
97
+ # An anonymous `subject!` is still read as `subject`.
98
+ def subject_name(node)
99
+ name = premise_name(node).to_sym
100
+
101
+ name == :subject! ? :subject : name
102
+ end
103
+
104
+ def subject_reference?(receiver, names)
105
+ return false unless receiver&.send_type? && receiver.receiver.nil? && receiver.arguments.empty?
106
+
107
+ names.include?(receiver.method_name)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vicenzo
6
+ module RSpec
7
+ # Shared helpers for the cops that read an example group naming a method - `#instance_method`,
8
+ # `.class_method`, `#method(signature)`, or the bare method name - and ask what the definitions
9
+ # under it are built from.
10
+ #
11
+ # Meant for cops inheriting from `RuboCop::Cop::RSpec::Base`: `example_group?` and `cop_config`
12
+ # come from there.
13
+ module DescribedMethod
14
+ # `#catch`, `.catch`, and the form carrying a signature such as `#catch(object:)`.
15
+ PREFIXED_METHOD_DESCRIPTION = /\A[#.](?<name>.+?)(?:\(.*\))?\z/
16
+ # `catch` - a description that is nothing but a method name.
17
+ BARE_METHOD_DESCRIPTION = /\A(?<name>\w+[?!=]?)\z/
18
+
19
+ private
20
+
21
+ # The nearest enclosing example group naming a method: a definition sitting in a `context` still
22
+ # answers to the `describe '#method'` wrapping it.
23
+ def described_method_name(node)
24
+ node
25
+ .each_ancestor(:any_block)
26
+ .lazy
27
+ .filter_map { |ancestor| method_name_from(ancestor) if example_group?(ancestor) }
28
+ .first
29
+ end
30
+
31
+ def method_name_from(node)
32
+ description = node.send_node.first_argument
33
+ return unless description.respond_to?(:str_type?) && description.str_type?
34
+
35
+ match = PREFIXED_METHOD_DESCRIPTION.match(description.value) ||
36
+ BARE_METHOD_DESCRIPTION.match(description.value)
37
+
38
+ match&.[](:name)
39
+ end
40
+
41
+ def allowed_methods
42
+ cop_config.fetch('AllowedMethods', [])
43
+ end
44
+
45
+ # Walks the expression a definition returns down its receiver chain, yielding each call, so that
46
+ # `Cat.new.catch(ball).to_s` is seen as built on `catch` while `Cat.new(catches: other.catch)`
47
+ # is not - there, nothing of `catch` reaches the value.
48
+ def call_in_chain(body)
49
+ node = returned_expression(body)
50
+
51
+ while node
52
+ node = node.send_node if node.any_block_type?
53
+ break unless node.type?(:call)
54
+ return node if yield(node)
55
+
56
+ node = node.receiver
57
+ end
58
+
59
+ nil
60
+ end
61
+
62
+ def returned_expression(body)
63
+ return if body.nil?
64
+
65
+ body.begin_type? ? body.children.last : body
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Vicenzo
6
+ module RSpec
7
+ # Shared helpers to tell which definitions an example group's expectations are *about*.
8
+ #
9
+ # Only the left-hand side of an expectation counts - `expect(order)` and `expect { order }`.
10
+ # What sits inside the matcher is the expected answer, not the subject: in
11
+ # `expect(described_class.recent).to contain_exactly(recent_order)`, `recent_order` is part of the
12
+ # answer, and reading it as the subject would turn the correct way of specifying a scope into an
13
+ # offense.
14
+ #
15
+ # Meant for cops inheriting from `RuboCop::Cop::RSpec::Base` and including `PremiseTracking`:
16
+ # `let?`, `example_group?` and `premise_name` come from there.
17
+ module ExpectationTarget
18
+ EXPECTATION_RUNNERS = %i[to not_to to_not].freeze
19
+
20
+ # Verifying a message is a statement about a collaborator, never about the subject.
21
+ MESSAGE_MATCHERS = %i[receive have_received receive_messages receive_message_chain].freeze
22
+
23
+ private
24
+
25
+ # The definitions declared directly in this group that its expectations assert on.
26
+ def asserted_definitions(group)
27
+ names = []
28
+
29
+ each_child_block(group) do |child|
30
+ next unless let?(child) || let_it_be?(child)
31
+
32
+ name = premise_name(child)&.to_sym
33
+
34
+ names << name if name && asserted_on?(group, name)
35
+ end
36
+
37
+ names
38
+ end
39
+
40
+ def asserted_on?(group, name)
41
+ return false if group.nil?
42
+
43
+ group.each_descendant(:send).any? { |send_node| expectation_on?(send_node, name) }
44
+ end
45
+
46
+ def expectation_on?(node, name)
47
+ return false unless node.method?(:expect) && node.receiver.nil?
48
+ return false unless reference?(node.first_argument || node.block_node&.body, name)
49
+
50
+ !message_expectation?(node)
51
+ end
52
+
53
+ def reference?(node, name)
54
+ return false unless node&.send_type? && node.receiver.nil? && node.arguments.empty?
55
+
56
+ node.method?(name)
57
+ end
58
+
59
+ def message_expectation?(node)
60
+ MESSAGE_MATCHERS.include?(matcher_name(expectation_matcher(node)))
61
+ end
62
+
63
+ def expectation_matcher(node)
64
+ runner = node.block_node&.parent || node.parent
65
+
66
+ return unless runner&.send_type? && EXPECTATION_RUNNERS.include?(runner.method_name)
67
+
68
+ runner.first_argument
69
+ end
70
+
71
+ # The matcher a chained expectation starts from: `receive(:set).with(key)` starts at `receive`.
72
+ def matcher_name(node)
73
+ node = node.receiver while node&.receiver
74
+
75
+ node&.method_name
76
+ end
77
+
78
+ def enclosing_example_group(node)
79
+ node.each_ancestor(:any_block).find { |ancestor| example_group?(ancestor) }
80
+ end
81
+
82
+ def subject_declared?(node)
83
+ node.each_ancestor(:any_block).any? do |ancestor|
84
+ next false unless example_group?(ancestor)
85
+
86
+ declares_subject?(ancestor)
87
+ end
88
+ end
89
+
90
+ def declares_subject?(group)
91
+ declared = false
92
+
93
+ each_child_block(group) { |child| declared ||= subject?(child) }
94
+
95
+ declared
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'premise_tracking'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # Shared machinery for the cops that read what a spec declares straight inside the top-level
10
+ # example group - the widest scope a file has, and the one every example written later inherits
11
+ # whether it wanted to or not.
12
+ #
13
+ # The including cop says which children it reads, through `#definition?`, and how to word the
14
+ # offense, through `MSG`. A cop that names what it found overrides `#message_for`, and may read
15
+ # the name through `#declared_name`.
16
+ #
17
+ # Meant for cops inheriting from `RuboCop::Cop::RSpec::Base`: `example_group?` comes from there.
18
+ module TopLevelDefinition
19
+ include PremiseTracking
20
+
21
+ def on_block(node)
22
+ return unless top_level_example_group?(node)
23
+
24
+ each_child_block(node) do |child|
25
+ add_offense(child.send_node, message: message_for(child)) if definition?(child)
26
+ end
27
+ end
28
+
29
+ alias on_numblock on_block
30
+
31
+ private
32
+
33
+ def top_level_example_group?(node)
34
+ example_group?(node) && outermost_example_group?(node)
35
+ end
36
+
37
+ def message_for(_node)
38
+ self.class::MSG
39
+ end
40
+
41
+ # The name the definition was given, or `nil` when there is none to read: an anonymous
42
+ # `subject { ... }`, or a name only known at runtime.
43
+ def declared_name(node)
44
+ argument = node.send_node.first_argument
45
+
46
+ argument.value.to_sym if argument&.type?(:sym, :str)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/described_method'
4
+ require_relative 'mixin/expectation_target'
5
+ require_relative 'mixin/premise_tracking'
6
+
7
+ module RuboCop
8
+ module Cop
9
+ module Vicenzo
10
+ module RSpec
11
+ # A `let` the expectations assert on, in an example group that declares no subject, is the
12
+ # subject wearing another name. Declare it as one.
13
+ #
14
+ # The subject of a specification is what the sentences are about. When every expectation in a
15
+ # group reads `expect(service)`, `service` is that - and calling it a `let` costs the group the
16
+ # one declaration that says so out loud, leaves `is_expected` unavailable, and hides the
17
+ # redefinitions from the cops that watch subjects for a living.
18
+ #
19
+ # This says nothing about what the subject should hold - that is
20
+ # `Vicenzo/RSpec/SubjectIsMethodResult`'s question, and it already knows the conventions the
21
+ # project declared. The two compose without arguing: declare the subject, and if the value it
22
+ # holds is not fit to be the subject either, the sibling cop says so next.
23
+ #
24
+ # `AllowedMethods` exempts a `let` whose value comes from calling one of the listed methods, for
25
+ # the projects where such a definition is deliberately a `let`. It ships empty.
26
+ #
27
+ # @example
28
+ # # bad - every expectation is about `service`, yet the group declares no subject
29
+ # describe '.call' do
30
+ # context 'when the order is unknown' do
31
+ # let(:service) { described_class.(order_id: 42) }
32
+ #
33
+ # it { expect(service).to be_failure }
34
+ # end
35
+ # end
36
+ #
37
+ # # good - the subject is declared, and `is_expected` reads the sentence back
38
+ # describe '.call' do
39
+ # context 'when the order is unknown' do
40
+ # subject(:service) { described_class.(order_id: 42) }
41
+ #
42
+ # it { is_expected.to be_failure }
43
+ # end
44
+ # end
45
+ #
46
+ # @example a let the expectations only read through is a premise
47
+ # # good - the expectation is about the book, not about the author
48
+ # describe '#author' do
49
+ # subject(:book) { create(:book, author:) }
50
+ #
51
+ # let(:author) { create(:author) }
52
+ #
53
+ # it { expect(book.author).to eq(author) }
54
+ # end
55
+ class SubjectDefinedAsLet < RuboCop::Cop::RSpec::Base
56
+ include DescribedMethod
57
+ include ExpectationTarget
58
+ include PremiseTracking
59
+
60
+ MSG = 'Let `:%<name>s` is what the expectations assert on, so it is the subject. ' \
61
+ 'Declare it with `subject(:%<name>s)`.'
62
+
63
+ def on_block(node)
64
+ name = subject_in_disguise(node)
65
+
66
+ return if name.nil?
67
+
68
+ add_offense(node.send_node, message: format(MSG, name:))
69
+ end
70
+
71
+ alias on_numblock on_block
72
+ alias on_itblock on_block
73
+
74
+ private
75
+
76
+ # The name a definition should have been declared under, or `nil` when it is a `let` like any
77
+ # other: not a definition at all, one the group already has a subject for, one whose value the
78
+ # project exempted, or one no expectation is about.
79
+ def subject_in_disguise(node)
80
+ return unless candidate?(node)
81
+
82
+ group = enclosing_example_group(node)
83
+
84
+ return if group.nil?
85
+
86
+ name = premise_name(node)&.to_sym
87
+
88
+ # Two definitions asserted side by side cannot both be this group's subject: they are
89
+ # hidden contexts, which `Vicenzo/RSpec/CompetingSubjects` reads.
90
+ return unless asserted_definitions(group) == [name]
91
+
92
+ name
93
+ end
94
+
95
+ def candidate?(node)
96
+ (let?(node) || let_it_be?(node)) && !subject_declared?(node) && !allowed_value?(node)
97
+ end
98
+
99
+ def allowed_value?(node)
100
+ return false if allowed_methods.empty?
101
+
102
+ !call_in_chain(node.body) { |call| allowed_methods.include?(call.method_name.to_s) }.nil?
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/described_method'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # The subject of an example group that describes a method must be the object under test,
10
+ # not what the method returned.
11
+ #
12
+ # A specification is read as a sentence. In "the cat caught the ball", the subject is the
13
+ # cat, not the ball. Storing the return value in the subject inverts that: the object the
14
+ # example is about disappears from the text, the subject's name starts to lie (a
15
+ # `subject(:cat)` holding a `Symbol`), and every scenario that would need the same object
16
+ # in a different state has to redefine the whole call.
17
+ #
18
+ # An example group names a method when its description reads `#instance_method`,
19
+ # `.class_method`, or the bare method name. The cop then looks at the subjects declared
20
+ # under it - including the ones nested in a `context` - and reports the ones whose value
21
+ # comes from calling that method.
22
+ #
23
+ # Some layers legitimately specify a single entry point, where the result is the subject
24
+ # (a service object always invoked through `call`, for instance). `AllowedMethods` exists
25
+ # for those and ships empty: the convention belongs to the project that adopts it, not to
26
+ # this gem. Declaring it there - or excluding a whole directory through the standard
27
+ # `Exclude` - keeps the exemption visible and reviewable in `.rubocop.yml`.
28
+ #
29
+ # An inline disable directive is not that: it is an exemption nobody reviewed. Dropping
30
+ # the `#` from the description is not either - the cop reads bare descriptions too, and the
31
+ # only thing lost is the reader's clue that a method is being specified there.
32
+ #
33
+ # @example
34
+ # # bad - the subject is the ball
35
+ # describe '#catch' do
36
+ # subject(:catch) { Cat.new(name: 'Bixano').catch(object: Ball.new) }
37
+ #
38
+ # it { is_expected.to eq(:success) }
39
+ # end
40
+ #
41
+ # # good - the subject is the cat
42
+ # describe '#catch' do
43
+ # subject(:cat) { Cat.new(name: 'Bixano') }
44
+ #
45
+ # it { expect(cat.catch(object: Ball.new)).to eq(:success) }
46
+ # end
47
+ #
48
+ # @example a description without a prefix names a method too
49
+ # # bad - dropping the '#' silences nothing and costs the reader the
50
+ # # one clue that a method is being specified here
51
+ # describe 'catch' do
52
+ # subject(:catch) { Cat.new(name: 'Bixano').catch(object: Ball.new) }
53
+ # end
54
+ #
55
+ # @example AllowedMethods: ['call']
56
+ # # good - a layer whose convention is a single entry point. Declared in
57
+ # # .rubocop.yml, the exemption stays visible and reviewable, which an
58
+ # # inline disable directive never is
59
+ # describe '.call' do
60
+ # subject(:result) { described_class.(object: Ball.new) }
61
+ # end
62
+ class SubjectIsMethodResult < RuboCop::Cop::RSpec::Base
63
+ include DescribedMethod
64
+
65
+ MSG = 'Subject holds what `%<method>s` returned, not the object under test. ' \
66
+ 'Make the subject the object that receives `%<method>s`, and call it inside the example.'
67
+
68
+ def on_block(node)
69
+ return unless subject?(node)
70
+
71
+ method_name = described_method_name(node)
72
+ return if method_name.nil? || allowed_methods.include?(method_name)
73
+
74
+ invocation = call_in_chain(node.body) { |call| call.method?(method_name) }
75
+ return unless invocation
76
+
77
+ add_offense(invocation, message: format(MSG, method: method_name))
78
+ end
79
+
80
+ alias on_numblock on_block
81
+ alias on_itblock on_block
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/top_level_definition'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # A context declared straight inside the top-level example group is a scenario for the whole
10
+ # file. Nest it in the group whose behaviour it is a scenario of.
11
+ #
12
+ # A context answers "under which circumstances?", and the question only means something once a
13
+ # behaviour has been named. Declared at the root, the scenario applies to every example in the
14
+ # file at once - the same overreach a root subject or a root let has, since the premises the
15
+ # context declares are handed to everything written under it later.
16
+ #
17
+ # It is also how the file loses the describe it never had: the first behaviour goes under
18
+ # `context 'when ...'`, the second one needs the same circumstances worded slightly differently,
19
+ # and what should have been two groups naming two behaviours becomes a tree of scenarios naming
20
+ # none.
21
+ #
22
+ # @example
23
+ # # bad - the scenario is about `#adult?`, yet it covers the whole file
24
+ # RSpec.describe Person do
25
+ # context 'when the person is a minor' do
26
+ # subject(:person) { described_class.new(age: 17) }
27
+ #
28
+ # it { is_expected.not_to be_adult }
29
+ # end
30
+ # end
31
+ #
32
+ # # good - the behaviour is named first, the scenario belongs to it
33
+ # RSpec.describe Person do
34
+ # describe '#adult?' do
35
+ # context 'when the person is a minor' do
36
+ # subject(:person) { described_class.new(age: 17) }
37
+ #
38
+ # it { is_expected.not_to be_adult }
39
+ # end
40
+ # end
41
+ # end
42
+ class TopLevelContext < RuboCop::Cop::RSpec::Base
43
+ include TopLevelDefinition
44
+
45
+ MSG = 'This context is declared in the top-level example group, so it is a scenario for every ' \
46
+ 'example in the file. Nest it in the group whose behaviour it is a scenario of.'
47
+
48
+ CONTEXTS = %i[context fcontext xcontext].freeze
49
+
50
+ # @!method context_definition?(node)
51
+ def_node_matcher :context_definition?, <<~PATTERN
52
+ (any_block (send nil? {#{CONTEXTS.map(&:inspect).join(' ')}} ...) ...)
53
+ PATTERN
54
+
55
+ private
56
+
57
+ def definition?(node) = context_definition?(node)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/top_level_definition'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # An example declared straight inside the top-level example group is about no behaviour in
10
+ # particular. Move it into the group describing the behaviour it exercises.
11
+ #
12
+ # The top-level group names the object under test, not one of the things it does. An example
13
+ # hanging directly off it therefore reads as a statement about the object as a whole, when it is
14
+ # always a statement about one behaviour - the group naming that behaviour is simply missing, and
15
+ # with it the place where the next example about the same behaviour would go.
16
+ #
17
+ # It is also the shape that keeps premises in the top-level group alive: as long as examples live
18
+ # at the root, the subject and the lets they read have nowhere else to be declared, which is what
19
+ # `Vicenzo/RSpec/TopLevelSubject` and `Vicenzo/RSpec/TopLevelLet` report.
20
+ #
21
+ # @example
22
+ # # bad - the example is about `#adult?`, but no group says so
23
+ # RSpec.describe Person do
24
+ # it { is_expected.to be_adult }
25
+ # end
26
+ #
27
+ # # good - the behaviour the example is about has a name, and a home
28
+ # RSpec.describe Person do
29
+ # describe '#adult?' do
30
+ # subject(:person) { described_class.new(age: 18) }
31
+ #
32
+ # it { is_expected.to be_adult }
33
+ # end
34
+ # end
35
+ class TopLevelExample < RuboCop::Cop::RSpec::Base
36
+ include TopLevelDefinition
37
+
38
+ MSG = 'This example is declared in the top-level example group, which names no behaviour. ' \
39
+ 'Move it into the group describing the behaviour it exercises.'
40
+
41
+ private
42
+
43
+ def definition?(node) = example?(node)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/top_level_definition'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # A hook declared straight inside the top-level example group runs for every example in the
10
+ # file. Declare it in the group whose examples need it.
11
+ #
12
+ # A hook is a premise like any other - it just builds the state through side effects instead of
13
+ # a name. Declared at the root it reaches every example, including the ones written later that
14
+ # never asked for that state, and the group that needs the state built differently can only
15
+ # undo it, override it, or work around it.
16
+ #
17
+ # It is also the escape hatch left open once the root cannot hold a `subject` or a `let` any
18
+ # more: the premise stops being declared and starts being performed, which is harder to read and
19
+ # invisible to the cops that watch definitions. `Vicenzo/RSpec/TopLevelSubject` and
20
+ # `Vicenzo/RSpec/TopLevelLet` close the front door; this one closes the back.
21
+ #
22
+ # @example
23
+ # # bad - every example in the file runs inside this state
24
+ # RSpec.describe Person do
25
+ # before { travel_to(Time.zone.local(2026, 1, 1)) }
26
+ #
27
+ # describe '#adult?' do
28
+ # it { is_expected.to be_adult }
29
+ # end
30
+ # end
31
+ #
32
+ # # good - the state belongs to the examples that need it
33
+ # RSpec.describe Person do
34
+ # describe '#adult?' do
35
+ # before { travel_to(Time.zone.local(2026, 1, 1)) }
36
+ #
37
+ # it { is_expected.to be_adult }
38
+ # end
39
+ # end
40
+ class TopLevelHook < RuboCop::Cop::RSpec::Base
41
+ include TopLevelDefinition
42
+
43
+ MSG = 'Hook `%<name>s` is declared in the top-level example group, so it runs for every ' \
44
+ 'example in the file. Declare it in the group whose examples need it.'
45
+
46
+ private
47
+
48
+ def message_for(node) = format(MSG, name: node.method_name)
49
+
50
+ def definition?(node) = hook?(node)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/top_level_definition'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # A let declared straight inside the top-level example group is the root of the nested
10
+ # redefinition problem. Declare it in the group whose examples read it.
11
+ #
12
+ # The top-level group is the widest scope a file has: a let born there is handed to every
13
+ # example, including all the ones written later that were never considered when it was written.
14
+ # The first sibling group that needs the value to be slightly different has nowhere to go but
15
+ # over the top of it - a redefinition, a near-copy under another name, or a `before` that mutates
16
+ # the value back into shape. None of those read as a specification any more, and each one is a
17
+ # scenario nobody named.
18
+ #
19
+ # Declaring the let in the group that reads it costs one extra line per group and buys back the
20
+ # naming: each group states the premises its examples start from, and a sibling that starts from
21
+ # a different value is a different group with its own declaration, not an override of someone
22
+ # else's.
23
+ #
24
+ # This is the preventive half of `Vicenzo/RSpec/NestedLetRedefinition`, which reports the
25
+ # redefinition once it exists. Keeping the top-level group free of lets means there is nothing to
26
+ # redefine.
27
+ #
28
+ # @example
29
+ # # bad - the let reaches every example, so `#adult?` can only override it
30
+ # RSpec.describe Person do
31
+ # let(:age) { 42 }
32
+ #
33
+ # describe '#tall?' do
34
+ # it { is_expected.to be_tall }
35
+ # end
36
+ #
37
+ # describe '#adult?' do
38
+ # let(:age) { 17 }
39
+ #
40
+ # it { is_expected.not_to be_adult }
41
+ # end
42
+ # end
43
+ #
44
+ # # good - each group declares the premises its examples start from
45
+ # RSpec.describe Person do
46
+ # describe '#tall?' do
47
+ # subject(:person) { described_class.new(height: 1.75) }
48
+ #
49
+ # it { is_expected.to be_tall }
50
+ # end
51
+ #
52
+ # describe '#adult?' do
53
+ # subject(:person) { described_class.new(age: 17) }
54
+ #
55
+ # it { is_expected.not_to be_adult }
56
+ # end
57
+ # end
58
+ #
59
+ # @example a context is as good a home as a describe
60
+ # # good - the premise belongs to the scenario, and the scenario is a context
61
+ # RSpec.describe Person do
62
+ # describe '#adult?' do
63
+ # context 'when the person is a minor' do
64
+ # subject(:person) { described_class.new(age:) }
65
+ #
66
+ # let(:age) { 17 }
67
+ #
68
+ # it { is_expected.not_to be_adult }
69
+ # end
70
+ # end
71
+ # end
72
+ class TopLevelLet < RuboCop::Cop::RSpec::Base
73
+ include TopLevelDefinition
74
+
75
+ MSG = 'Let `:%<name>s` is declared in the top-level example group, where it reaches every ' \
76
+ 'example in the file. Declare it in the group whose examples read it.'
77
+ MSG_UNNAMED = 'A let is declared in the top-level example group, where it reaches every ' \
78
+ 'example in the file. Declare it in the group whose examples read it.'
79
+
80
+ private
81
+
82
+ def message_for(node)
83
+ name = declared_name(node)
84
+
85
+ name.nil? ? MSG_UNNAMED : format(MSG, name:)
86
+ end
87
+
88
+ def definition?(node) = let?(node) || let_it_be?(node)
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'mixin/top_level_definition'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Vicenzo
8
+ module RSpec
9
+ # A subject declared straight inside the top-level example group is the root of the nested
10
+ # redefinition problem. Declare it in the group whose examples read it.
11
+ #
12
+ # The top-level group is the widest scope a file has: a subject born there is handed to every
13
+ # example, including all the ones written later that were never considered when it was written.
14
+ # The first sibling group that needs the object built slightly differently has nowhere to go but
15
+ # over the top of it - a redefinition, a second subject under another name, or a `before` that
16
+ # patches the object back into shape. None of those read as a specification any more, and each
17
+ # one is a scenario nobody named.
18
+ #
19
+ # Declaring the subject in the group that asserts on it costs one extra line per group and buys
20
+ # back the naming: each group states the object it is about, and a sibling that needs a different
21
+ # object is a different group with its own declaration, not an override of someone else's.
22
+ #
23
+ # This is the preventive half of `Vicenzo/RSpec/NestedSubjectRedefinition`, which reports the
24
+ # redefinition once it exists. Keeping the top-level group free of subjects means there is
25
+ # nothing to redefine.
26
+ #
27
+ # @example
28
+ # # bad - the subject reaches every example, so `#tall?` can only override it
29
+ # RSpec.describe Person do
30
+ # subject(:person) { described_class.new }
31
+ #
32
+ # describe '#adult?' do
33
+ # before { person.age = 18 }
34
+ #
35
+ # it { is_expected.to be_adult }
36
+ # end
37
+ #
38
+ # describe '#tall?' do
39
+ # subject(:person) { described_class.new(height: 1.75) }
40
+ #
41
+ # it { is_expected.to be_tall }
42
+ # end
43
+ # end
44
+ #
45
+ # # good - each group declares the person its examples are about
46
+ # RSpec.describe Person do
47
+ # describe '#adult?' do
48
+ # subject(:person) { described_class.new(age: 18) }
49
+ #
50
+ # it { is_expected.to be_adult }
51
+ # end
52
+ #
53
+ # describe '#tall?' do
54
+ # subject(:person) { described_class.new(height: 1.75) }
55
+ #
56
+ # it { is_expected.to be_tall }
57
+ # end
58
+ # end
59
+ #
60
+ # @example a context is as good a home as a describe
61
+ # # good - the subject belongs to the scenario, and the scenario is a context
62
+ # RSpec.describe Person do
63
+ # describe '#adult?' do
64
+ # context 'when the person is of age' do
65
+ # subject(:person) { described_class.new(age: 18) }
66
+ #
67
+ # it { is_expected.to be_adult }
68
+ # end
69
+ # end
70
+ # end
71
+ class TopLevelSubject < RuboCop::Cop::RSpec::Base
72
+ include TopLevelDefinition
73
+
74
+ MSG = 'Subject `:%<name>s` is declared in the top-level example group, where it reaches every ' \
75
+ 'example in the file. Declare it in the group whose examples read it.'
76
+ MSG_UNNAMED = 'The subject is declared in the top-level example group, where it reaches every ' \
77
+ 'example in the file. Declare it in the group whose examples read it.'
78
+
79
+ private
80
+
81
+ def message_for(node)
82
+ name = declared_name(node)
83
+
84
+ name.nil? ? MSG_UNNAMED : format(MSG, name:)
85
+ end
86
+
87
+ def definition?(node) = subject?(node)
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -1,15 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'vicenzo/rspec/competing_subjects'
3
4
  require_relative 'vicenzo/rspec/conditional_in_spec'
4
5
  require_relative 'vicenzo/rspec/derived_premises'
5
6
  require_relative 'vicenzo/rspec/dynamic_example_generation'
6
7
  require_relative 'vicenzo/rspec/inconsistent_sibling_structure'
7
8
  require_relative 'vicenzo/rspec/iteration_inside_example'
9
+ require_relative 'vicenzo/rspec/let_is_method_result'
8
10
  require_relative 'vicenzo/rspec/mutated_premises'
9
11
  require_relative 'vicenzo/rspec/nested_context_improper_start'
10
12
  require_relative 'vicenzo/rspec/nested_let_redefinition'
11
13
  require_relative 'vicenzo/rspec/nested_subject_redefinition'
12
14
  require_relative 'vicenzo/rspec/leaky_definition'
15
+ require_relative 'vicenzo/rspec/subject_defined_as_let'
16
+ require_relative 'vicenzo/rspec/subject_is_method_result'
17
+ require_relative 'vicenzo/rspec/top_level_context'
18
+ require_relative 'vicenzo/rspec/top_level_example'
19
+ require_relative 'vicenzo/rspec/top_level_hook'
20
+ require_relative 'vicenzo/rspec/top_level_let'
21
+ require_relative 'vicenzo/rspec/top_level_subject'
13
22
  require_relative 'vicenzo/rails/enum_inclusion_of_validation'
14
23
  require_relative 'vicenzo/layout/multiline_method_call_line_breaks'
15
24
  require_relative 'vicenzo/style/json_parse_symbolize_names'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Vicenzo
5
- VERSION = '0.6.0'
5
+ VERSION = '0.8.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-vicenzo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Vicenzo
@@ -75,17 +75,29 @@ files:
75
75
  - lib/rubocop-vicenzo.rb
76
76
  - lib/rubocop/cop/vicenzo/layout/multiline_method_call_line_breaks.rb
77
77
  - lib/rubocop/cop/vicenzo/rails/enum_inclusion_of_validation.rb
78
+ - lib/rubocop/cop/vicenzo/rspec/competing_subjects.rb
78
79
  - lib/rubocop/cop/vicenzo/rspec/conditional_in_spec.rb
79
80
  - lib/rubocop/cop/vicenzo/rspec/derived_premises.rb
80
81
  - lib/rubocop/cop/vicenzo/rspec/dynamic_example_generation.rb
81
82
  - lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
82
83
  - lib/rubocop/cop/vicenzo/rspec/iteration_inside_example.rb
83
84
  - lib/rubocop/cop/vicenzo/rspec/leaky_definition.rb
85
+ - lib/rubocop/cop/vicenzo/rspec/let_is_method_result.rb
86
+ - lib/rubocop/cop/vicenzo/rspec/mixin/described_method.rb
87
+ - lib/rubocop/cop/vicenzo/rspec/mixin/expectation_target.rb
84
88
  - lib/rubocop/cop/vicenzo/rspec/mixin/premise_tracking.rb
89
+ - lib/rubocop/cop/vicenzo/rspec/mixin/top_level_definition.rb
85
90
  - lib/rubocop/cop/vicenzo/rspec/mutated_premises.rb
86
91
  - lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
87
92
  - lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
88
93
  - lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb
94
+ - lib/rubocop/cop/vicenzo/rspec/subject_defined_as_let.rb
95
+ - lib/rubocop/cop/vicenzo/rspec/subject_is_method_result.rb
96
+ - lib/rubocop/cop/vicenzo/rspec/top_level_context.rb
97
+ - lib/rubocop/cop/vicenzo/rspec/top_level_example.rb
98
+ - lib/rubocop/cop/vicenzo/rspec/top_level_hook.rb
99
+ - lib/rubocop/cop/vicenzo/rspec/top_level_let.rb
100
+ - lib/rubocop/cop/vicenzo/rspec/top_level_subject.rb
89
101
  - lib/rubocop/cop/vicenzo/style/json_parse_symbolize_names.rb
90
102
  - lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb
91
103
  - lib/rubocop/cop/vicenzo_cops.rb