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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/README.md +1 -1
- data/config/default.yml +55 -0
- data/lib/rubocop/cop/vicenzo/rspec/mixin/top_level_definition.rb +52 -0
- data/lib/rubocop/cop/vicenzo/rspec/top_level_context.rb +62 -0
- data/lib/rubocop/cop/vicenzo/rspec/top_level_example.rb +48 -0
- data/lib/rubocop/cop/vicenzo/rspec/top_level_hook.rb +55 -0
- data/lib/rubocop/cop/vicenzo/rspec/top_level_let.rb +93 -0
- data/lib/rubocop/cop/vicenzo/rspec/top_level_subject.rb +92 -0
- data/lib/rubocop/cop/vicenzo_cops.rb +5 -0
- data/lib/rubocop/vicenzo/version.rb +1 -1
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '094cfb18e36f28e2f311e80f435b1b5159f22875cfb8f13b5a0551ca9b5e9218'
|
|
4
|
+
data.tar.gz: b96a9634c956e7cec399e10a2f7f512a5f2894822ba07fecf7294a502fee56b5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ba554984ec7092154352415043f2868cd59bd89015de29b0098e01c1f0b2016b8b3a837845cd22e8219e1115b37f4b6e80eb3d5861d0f3325c0ad2894466de23
|
|
7
|
+
data.tar.gz: 031a968654d68f7fa65e7a57a6178fc97651ae8c20cb4861f25a1f4c9feb5c6d044a3bc7e269e4de392a9a38483ce51296517f852abe61e88ba804ad723140b4
|
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.
|
|
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'
|
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.
|
|
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
|