rubocop-vicenzo 0.7.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: 4f2fb87b8d1bfd10a66d52fcd5ede5f1eea78c7c2a8b7b8ea8a692d0efce54b6
4
- data.tar.gz: aa25bb974ce70ab9ef752a1e36b83d779abded472022258a86ff8c2fdc483d77
3
+ metadata.gz: '094cfb18e36f28e2f311e80f435b1b5159f22875cfb8f13b5a0551ca9b5e9218'
4
+ data.tar.gz: b96a9634c956e7cec399e10a2f7f512a5f2894822ba07fecf7294a502fee56b5
5
5
  SHA512:
6
- metadata.gz: 57918353d578d8c2a5840617ccfefe7a996aad212dbce8921ae18a1bf302bf89fb6a5e40f6b9cdd97d082c4fb74d2e26fb2a9148d92e83fbb074e6c094da996b
7
- data.tar.gz: be26bf4c722a8e351112c02d58bba96d06fd62fa230e8feb20e3f80675c82f6ca377875a53281df99d39ccabf8fc5de2822f9eff985a6c7a3cbe188dedd3ff6d
6
+ metadata.gz: ba554984ec7092154352415043f2868cd59bd89015de29b0098e01c1f0b2016b8b3a837845cd22e8219e1115b37f4b6e80eb3d5861d0f3325c0ad2894466de23
7
+ data.tar.gz: 031a968654d68f7fa65e7a57a6178fc97651ae8c20cb4861f25a1f4c9feb5c6d044a3bc7e269e4de392a9a38483ce51296517f852abe61e88ba804ad723140b4
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.7.0"
2
+ ".": "0.8.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
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
+
3
10
  ## [0.7.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.6.0...v0.7.0) (2026-08-31)
4
11
 
5
12
 
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.7.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
@@ -139,6 +139,61 @@ Vicenzo/RSpec/SubjectIsMethodResult:
139
139
  - '**/spec/factories/**/*'
140
140
  VersionAdded: '0.7.0'
141
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
+
142
197
  Vicenzo/Style/JsonParseSymbolizeNames:
143
198
  Description: 'Enforces passing `symbolize_names: true` to `JSON.parse` so keys are symbols.'
144
199
  Enabled: true
@@ -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,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
@@ -14,6 +14,11 @@ require_relative 'vicenzo/rspec/nested_subject_redefinition'
14
14
  require_relative 'vicenzo/rspec/leaky_definition'
15
15
  require_relative 'vicenzo/rspec/subject_defined_as_let'
16
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'
17
22
  require_relative 'vicenzo/rails/enum_inclusion_of_validation'
18
23
  require_relative 'vicenzo/layout/multiline_method_call_line_breaks'
19
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.7.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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bruno Vicenzo
@@ -86,12 +86,18 @@ files:
86
86
  - lib/rubocop/cop/vicenzo/rspec/mixin/described_method.rb
87
87
  - lib/rubocop/cop/vicenzo/rspec/mixin/expectation_target.rb
88
88
  - lib/rubocop/cop/vicenzo/rspec/mixin/premise_tracking.rb
89
+ - lib/rubocop/cop/vicenzo/rspec/mixin/top_level_definition.rb
89
90
  - lib/rubocop/cop/vicenzo/rspec/mutated_premises.rb
90
91
  - lib/rubocop/cop/vicenzo/rspec/nested_context_improper_start.rb
91
92
  - lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb
92
93
  - lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb
93
94
  - lib/rubocop/cop/vicenzo/rspec/subject_defined_as_let.rb
94
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
95
101
  - lib/rubocop/cop/vicenzo/style/json_parse_symbolize_names.rb
96
102
  - lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb
97
103
  - lib/rubocop/cop/vicenzo_cops.rb