rubocop-vicenzo 0.8.0 → 0.9.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/.rubocop.yml +2 -22
- data/CHANGELOG.md +10 -0
- data/README.md +26 -1
- data/Rakefile +2 -2
- data/config/default.yml +39 -0
- data/config/style.yml +228 -0
- data/lib/rubocop/cop/vicenzo/naming/anonymous_unused_name.rb +164 -0
- data/lib/rubocop/cop/vicenzo/naming/short_name.rb +133 -0
- data/lib/rubocop/cop/vicenzo/rspec/describe_inside_context.rb +94 -0
- data/lib/rubocop/cop/vicenzo/rspec/dynamic_example_generation.rb +19 -4
- data/lib/rubocop/cop/vicenzo/rspec/mutated_premises.rb +13 -2
- data/lib/rubocop/cop/vicenzo/rspec/nested_let_redefinition.rb +1 -1
- data/lib/rubocop/cop/vicenzo/rspec/nested_subject_redefinition.rb +1 -1
- data/lib/rubocop/cop/vicenzo/rspec/unnested_context_improper_start.rb +89 -0
- data/lib/rubocop/cop/vicenzo_cops.rb +4 -0
- data/lib/rubocop/vicenzo/version.rb +1 -1
- data/rakelib/docs.rake +35 -10
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5b813b05ed3c69c557d8b01bb90a4f142cbbb4ff06240d2df571f23ca5b720b
|
|
4
|
+
data.tar.gz: c121942b02a939b96de5a264fd53db21ebba6dd85e85c9e95424538e59c5240c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d6832e1b1d8a011f1ded3b21451e79f1722234bd7424ccf418a59550a79b20edd4b8a3632345df748982b356b32d868d530fe96267ce5b81689f32d8e9559ae
|
|
7
|
+
data.tar.gz: b2513e3ab2f35243a88029d8f08ff2bd1bd29c08552c305e919cb76b10717e113cccf837276bc77b390bb87b5f34b276b08415adc1fd64c05566028107f65471
|
data/.rubocop.yml
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
inherit_from: config/style.yml
|
|
2
|
+
|
|
1
3
|
plugins:
|
|
2
4
|
- rubocop-performance
|
|
3
5
|
- rubocop-rake
|
|
@@ -5,39 +7,17 @@ plugins:
|
|
|
5
7
|
- rubocop-vicenzo
|
|
6
8
|
- rubocop-internal_affairs
|
|
7
9
|
|
|
8
|
-
AllCops:
|
|
9
|
-
Exclude:
|
|
10
|
-
- 'bin/**/*'
|
|
11
|
-
- 'vendor/**/*'
|
|
12
|
-
NewCops: enable
|
|
13
|
-
|
|
14
10
|
InternalAffairs/UndefinedConfig: # False positive because conflicts with Rubocop Layout namespace
|
|
15
11
|
Exclude:
|
|
16
12
|
- 'lib/rubocop/cop/vicenzo/layout/**/*'
|
|
17
13
|
|
|
18
|
-
Metrics/BlockLength:
|
|
19
|
-
Exclude:
|
|
20
|
-
- 'rakelib/**/*'
|
|
21
|
-
- 'Rakefile'
|
|
22
|
-
|
|
23
14
|
Naming/FileName:
|
|
24
15
|
Exclude:
|
|
25
16
|
- lib/rubocop-vicenzo.rb
|
|
26
17
|
|
|
27
|
-
RSpec/ContextWording:
|
|
28
|
-
Prefixes:
|
|
29
|
-
- and
|
|
30
|
-
- but
|
|
31
|
-
- when
|
|
32
|
-
- with
|
|
33
|
-
- without
|
|
34
|
-
|
|
35
18
|
RSpec/ExampleLength:
|
|
36
19
|
Enabled: false
|
|
37
20
|
|
|
38
|
-
RSpec/NestedGroups:
|
|
39
|
-
Enabled: false
|
|
40
|
-
|
|
41
21
|
Vicenzo/RSpec/LeakyDefinition:
|
|
42
22
|
Exclude:
|
|
43
23
|
- 'spec/support/**/*'
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.8.0...v0.9.0) (2026-09-23)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add Vicenzo/Naming/AnonymousUnusedName cop ([#33](https://github.com/bvicenzo/rubocop-vicenzo/issues/33)) ([fabbad6](https://github.com/bvicenzo/rubocop-vicenzo/commit/fabbad655dac0c362924dd0aaf245abd2c7023e1))
|
|
9
|
+
* add Vicenzo/Naming/ShortName cop and an opinionated config/style.yml ([#35](https://github.com/bvicenzo/rubocop-vicenzo/issues/35)) ([1b67f46](https://github.com/bvicenzo/rubocop-vicenzo/commit/1b67f4643b4348f5768a8fbb3b88cea35046e5e2))
|
|
10
|
+
* add Vicenzo/RSpec/DescribeInsideContext cop ([#37](https://github.com/bvicenzo/rubocop-vicenzo/issues/37)) ([3f3d6ae](https://github.com/bvicenzo/rubocop-vicenzo/commit/3f3d6aeb95d087e32594e8ded3d1ce0e3715dee7))
|
|
11
|
+
* add Vicenzo/RSpec/UnnestedContextImproperStart cop ([#36](https://github.com/bvicenzo/rubocop-vicenzo/issues/36)) ([0a8fb0f](https://github.com/bvicenzo/rubocop-vicenzo/commit/0a8fb0fec62370ac331b9a3275b95a0b22998b1d))
|
|
12
|
+
|
|
3
13
|
## [0.8.0](https://github.com/bvicenzo/rubocop-vicenzo/compare/v0.7.0...v0.8.0) (2026-09-09)
|
|
4
14
|
|
|
5
15
|
|
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.9.0', require: false
|
|
15
15
|
end
|
|
16
16
|
```
|
|
17
17
|
<!-- x-release-please-end-version -->
|
|
@@ -64,6 +64,31 @@ cops together with the standard cops.
|
|
|
64
64
|
rubocop --plugin rubocop-vicenzo
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
## Opinionated style guide
|
|
68
|
+
|
|
69
|
+
Loading the plugin gives you the `Vicenzo/*` cops and nothing else: their defaults
|
|
70
|
+
live in `config/default.yml` and are applied automatically.
|
|
71
|
+
|
|
72
|
+
The gem also ships `config/style.yml`, an opinionated configuration of the cops the
|
|
73
|
+
`Vicenzo/*` ones build on — line length, multiline layout, metrics, and the naming
|
|
74
|
+
rules that back `Vicenzo/Naming/ShortName` (notably `Naming/BlockParameterName`,
|
|
75
|
+
which RuboCop ships with `MinNameLength: 1` and which therefore never fires). It is
|
|
76
|
+
**opt-in**:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
inherit_gem:
|
|
80
|
+
rubocop-vicenzo: config/style.yml
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
That single line brings the configuration **and** the plugins it builds on
|
|
84
|
+
(`rubocop-rspec` and this gem's own cops), so there is no `plugins:` key left for you
|
|
85
|
+
to write.
|
|
86
|
+
|
|
87
|
+
> [!NOTE]
|
|
88
|
+
> `AllCops: CacheRootDirectory` is not inheritable. RuboCop reads it from the
|
|
89
|
+
> top-level config file before resolving `inherit_from`/`inherit_gem`, so it has to
|
|
90
|
+
> stay in your own `.rubocop.yml`.
|
|
91
|
+
|
|
67
92
|
## Development
|
|
68
93
|
|
|
69
94
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/Rakefile
CHANGED
|
@@ -4,8 +4,8 @@ require 'bundler/setup'
|
|
|
4
4
|
require 'bundler/gem_tasks'
|
|
5
5
|
require 'rspec/core/rake_task'
|
|
6
6
|
|
|
7
|
-
RSpec::Core::RakeTask.new(:spec) do |
|
|
8
|
-
|
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |rspec_task|
|
|
8
|
+
rspec_task.rspec_opts = '--format progress'
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
require 'rubocop/rake_task'
|
data/config/default.yml
CHANGED
|
@@ -5,6 +5,19 @@ Vicenzo/Layout/MultilineMethodCallLineBreaks:
|
|
|
5
5
|
IndentationWidth: 2
|
|
6
6
|
VersionAdded: '0.3.0'
|
|
7
7
|
|
|
8
|
+
Vicenzo/Naming/AnonymousUnusedName:
|
|
9
|
+
Description: 'An unused declaration must not be anonymous; name it after what it stands for.'
|
|
10
|
+
Enabled: true
|
|
11
|
+
Severity: convention
|
|
12
|
+
VersionAdded: '0.9.0'
|
|
13
|
+
|
|
14
|
+
Vicenzo/Naming/ShortName:
|
|
15
|
+
Description: 'A name too short to carry meaning; spell out whole words from the domain.'
|
|
16
|
+
Enabled: true
|
|
17
|
+
Severity: convention
|
|
18
|
+
MinNameLength: 3
|
|
19
|
+
VersionAdded: '0.9.0'
|
|
20
|
+
|
|
8
21
|
Vicenzo/Rails/EnumInclusionOfValidation:
|
|
9
22
|
Description: 'Check if the enum has the inclusion of validation defined.'
|
|
10
23
|
Enabled: true
|
|
@@ -45,6 +58,17 @@ Vicenzo/RSpec/DerivedPremises:
|
|
|
45
58
|
- '**/spec/factories/**/*'
|
|
46
59
|
VersionAdded: '0.6.0'
|
|
47
60
|
|
|
61
|
+
Vicenzo/RSpec/DescribeInsideContext:
|
|
62
|
+
Description: 'A describe must not be nested in a context, which is a scenario of the behaviour a describe names.'
|
|
63
|
+
Enabled: true
|
|
64
|
+
Severity: warning
|
|
65
|
+
Include:
|
|
66
|
+
- '**/spec/**/*_spec.rb'
|
|
67
|
+
Exclude:
|
|
68
|
+
- '**/spec/support/**/*'
|
|
69
|
+
- '**/spec/factories/**/*'
|
|
70
|
+
VersionAdded: '0.9.0'
|
|
71
|
+
|
|
48
72
|
Vicenzo/RSpec/DynamicExampleGeneration:
|
|
49
73
|
Description: 'Do not use iteration to dynamically generate example groups or examples.'
|
|
50
74
|
Enabled: true
|
|
@@ -194,6 +218,21 @@ Vicenzo/RSpec/TopLevelSubject:
|
|
|
194
218
|
- '**/spec/factories/**/*'
|
|
195
219
|
VersionAdded: '0.8.0'
|
|
196
220
|
|
|
221
|
+
Vicenzo/RSpec/UnnestedContextImproperStart:
|
|
222
|
+
Description: 'Check if a context with no parent scenario starts with a conjunction.'
|
|
223
|
+
Enabled: true
|
|
224
|
+
Severity: convention
|
|
225
|
+
ForbiddenPrefixes:
|
|
226
|
+
- and
|
|
227
|
+
- but
|
|
228
|
+
- however
|
|
229
|
+
Include:
|
|
230
|
+
- '**/spec/**/*_spec.rb'
|
|
231
|
+
Exclude:
|
|
232
|
+
- '**/spec/support/**/*'
|
|
233
|
+
- '**/spec/factories/**/*'
|
|
234
|
+
VersionAdded: '0.9.0'
|
|
235
|
+
|
|
197
236
|
Vicenzo/Style/JsonParseSymbolizeNames:
|
|
198
237
|
Description: 'Enforces passing `symbolize_names: true` to `JSON.parse` so keys are symbols.'
|
|
199
238
|
Enabled: true
|
data/config/style.yml
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
plugins:
|
|
2
|
+
- rubocop-vicenzo
|
|
3
|
+
- rubocop-rspec
|
|
4
|
+
|
|
5
|
+
AllCops:
|
|
6
|
+
SuggestExtensions: false
|
|
7
|
+
NewCops: enable
|
|
8
|
+
Exclude:
|
|
9
|
+
- 'node_modules/**/*'
|
|
10
|
+
- 'vendor/**/*'
|
|
11
|
+
- 'bin/**/*'
|
|
12
|
+
- '.git/**/*'
|
|
13
|
+
- 'db/**/*'
|
|
14
|
+
|
|
15
|
+
##### LAYOUT #####
|
|
16
|
+
|
|
17
|
+
Layout/ArgumentAlignment:
|
|
18
|
+
Enabled: true
|
|
19
|
+
EnforcedStyle: with_fixed_indentation
|
|
20
|
+
|
|
21
|
+
Layout/CaseIndentation:
|
|
22
|
+
Enabled: true
|
|
23
|
+
EnforcedStyle: end
|
|
24
|
+
|
|
25
|
+
Layout/EndAlignment:
|
|
26
|
+
Enabled: true
|
|
27
|
+
EnforcedStyleAlignWith: variable
|
|
28
|
+
|
|
29
|
+
Layout/FirstArgumentIndentation:
|
|
30
|
+
Enabled: true
|
|
31
|
+
EnforcedStyle: consistent
|
|
32
|
+
|
|
33
|
+
Layout/FirstArrayElementIndentation:
|
|
34
|
+
Enabled: true
|
|
35
|
+
EnforcedStyle: consistent
|
|
36
|
+
|
|
37
|
+
Layout/FirstHashElementIndentation:
|
|
38
|
+
Enabled: true
|
|
39
|
+
EnforcedStyle: consistent
|
|
40
|
+
|
|
41
|
+
Layout/FirstMethodArgumentLineBreak:
|
|
42
|
+
Enabled: true
|
|
43
|
+
|
|
44
|
+
Layout/LineLength:
|
|
45
|
+
Max: 120
|
|
46
|
+
# (\A|\s) -> Start of line or whitespace
|
|
47
|
+
# # -> The hash character
|
|
48
|
+
# (?!\{) -> Negative lookahead: NOT followed by `{` (prevents matching interpolation)
|
|
49
|
+
AllowedPatterns: ['(\A|\s)#(?!\{)']
|
|
50
|
+
|
|
51
|
+
Layout/MultilineMethodArgumentLineBreaks:
|
|
52
|
+
Enabled: true
|
|
53
|
+
|
|
54
|
+
Layout/MultilineMethodCallBraceLayout:
|
|
55
|
+
Enabled: true
|
|
56
|
+
EnforcedStyle: new_line
|
|
57
|
+
|
|
58
|
+
Layout/MultilineMethodCallIndentation:
|
|
59
|
+
Enabled: true
|
|
60
|
+
EnforcedStyle: indented
|
|
61
|
+
|
|
62
|
+
Layout/FirstArrayElementLineBreak:
|
|
63
|
+
Enabled: true
|
|
64
|
+
|
|
65
|
+
Layout/MultilineArrayLineBreaks:
|
|
66
|
+
Enabled: true
|
|
67
|
+
|
|
68
|
+
Layout/MultilineArrayBraceLayout:
|
|
69
|
+
Enabled: true
|
|
70
|
+
EnforcedStyle: new_line
|
|
71
|
+
|
|
72
|
+
Layout/FirstHashElementLineBreak:
|
|
73
|
+
Enabled: true
|
|
74
|
+
|
|
75
|
+
Layout/MultilineHashKeyLineBreaks:
|
|
76
|
+
Enabled: true
|
|
77
|
+
|
|
78
|
+
Layout/MultilineHashBraceLayout:
|
|
79
|
+
Enabled: true
|
|
80
|
+
EnforcedStyle: new_line
|
|
81
|
+
|
|
82
|
+
##### STYLE #####
|
|
83
|
+
|
|
84
|
+
Style/PreferredHashMethods:
|
|
85
|
+
Enabled: true
|
|
86
|
+
Safe: false
|
|
87
|
+
EnforcedStyle: verbose
|
|
88
|
+
|
|
89
|
+
Style/SymbolArray:
|
|
90
|
+
Enabled: true
|
|
91
|
+
EnforcedStyle: percent
|
|
92
|
+
MinSize: 3
|
|
93
|
+
|
|
94
|
+
Style/WordArray:
|
|
95
|
+
Enabled: true
|
|
96
|
+
EnforcedStyle: percent
|
|
97
|
+
MinSize: 3
|
|
98
|
+
|
|
99
|
+
Style/MultilineBlockChain:
|
|
100
|
+
Enabled: false
|
|
101
|
+
|
|
102
|
+
Style/Lambda:
|
|
103
|
+
Enabled: true
|
|
104
|
+
EnforcedStyle: literal
|
|
105
|
+
|
|
106
|
+
Style/LambdaCall:
|
|
107
|
+
Enabled: false
|
|
108
|
+
|
|
109
|
+
Style/Documentation:
|
|
110
|
+
Enabled: false
|
|
111
|
+
|
|
112
|
+
##### LINT #####
|
|
113
|
+
|
|
114
|
+
Lint/MissingSuper:
|
|
115
|
+
Enabled: false
|
|
116
|
+
|
|
117
|
+
##### NAMING #####
|
|
118
|
+
|
|
119
|
+
Naming/InclusiveLanguage:
|
|
120
|
+
Enabled: false
|
|
121
|
+
|
|
122
|
+
Naming/PredicatePrefix:
|
|
123
|
+
ForbiddenPrefixes:
|
|
124
|
+
- is_
|
|
125
|
+
- have_
|
|
126
|
+
# allows `has_` predicate
|
|
127
|
+
|
|
128
|
+
# A single letter says nothing about what was rescued.
|
|
129
|
+
Naming/RescuedExceptionsVariableName:
|
|
130
|
+
Enabled: true
|
|
131
|
+
PreferredName: error
|
|
132
|
+
|
|
133
|
+
# Vicenzo/Naming/ShortName covers local variables. Block parameters are the core
|
|
134
|
+
# cop's job, but it ships with MinNameLength: 1 and therefore never fires — raise
|
|
135
|
+
# it so both halves of the rule agree.
|
|
136
|
+
Naming/BlockParameterName:
|
|
137
|
+
MinNameLength: 3
|
|
138
|
+
|
|
139
|
+
###### METRICS #####
|
|
140
|
+
|
|
141
|
+
Metrics/AbcSize:
|
|
142
|
+
Enabled: true
|
|
143
|
+
Max: 22
|
|
144
|
+
|
|
145
|
+
Metrics/BlockLength:
|
|
146
|
+
Enabled: true
|
|
147
|
+
CountComments: false
|
|
148
|
+
CountAsOne:
|
|
149
|
+
- array
|
|
150
|
+
- hash
|
|
151
|
+
- heredoc
|
|
152
|
+
- method_call
|
|
153
|
+
AllowedMethods:
|
|
154
|
+
- 'describe'
|
|
155
|
+
- 'context'
|
|
156
|
+
- 'it'
|
|
157
|
+
- 'let'
|
|
158
|
+
- 'subject'
|
|
159
|
+
- 'namespace'
|
|
160
|
+
- 'task'
|
|
161
|
+
- 'configure'
|
|
162
|
+
Exclude:
|
|
163
|
+
- '**/*.gemspec'
|
|
164
|
+
- 'Rakefile'
|
|
165
|
+
- 'rakelib/**/*'
|
|
166
|
+
- 'spec/**/*'
|
|
167
|
+
|
|
168
|
+
Metrics/ClassLength:
|
|
169
|
+
Enabled: true
|
|
170
|
+
Max: 200
|
|
171
|
+
CountAsOne:
|
|
172
|
+
- array
|
|
173
|
+
- hash
|
|
174
|
+
- heredoc
|
|
175
|
+
- method_call
|
|
176
|
+
|
|
177
|
+
Metrics/MethodLength:
|
|
178
|
+
Enabled: true
|
|
179
|
+
CountAsOne:
|
|
180
|
+
- array
|
|
181
|
+
- hash
|
|
182
|
+
- heredoc
|
|
183
|
+
- method_call
|
|
184
|
+
Max: 20
|
|
185
|
+
|
|
186
|
+
Metrics/ModuleLength:
|
|
187
|
+
Enabled: true
|
|
188
|
+
Max: 200
|
|
189
|
+
CountAsOne:
|
|
190
|
+
- array
|
|
191
|
+
- hash
|
|
192
|
+
- heredoc
|
|
193
|
+
- method_call
|
|
194
|
+
|
|
195
|
+
##### RSPEC #####
|
|
196
|
+
|
|
197
|
+
RSpec/ContextWording:
|
|
198
|
+
Prefixes:
|
|
199
|
+
- and
|
|
200
|
+
- but
|
|
201
|
+
- when
|
|
202
|
+
- with
|
|
203
|
+
- without
|
|
204
|
+
|
|
205
|
+
RSpec/ExampleLength:
|
|
206
|
+
Max: 20
|
|
207
|
+
CountAsOne:
|
|
208
|
+
- array
|
|
209
|
+
- hash
|
|
210
|
+
- heredoc
|
|
211
|
+
- method_call
|
|
212
|
+
|
|
213
|
+
RSpec/MultipleMemoizedHelpers:
|
|
214
|
+
Max: 10
|
|
215
|
+
|
|
216
|
+
RSpec/NestedGroups:
|
|
217
|
+
Enabled: false
|
|
218
|
+
|
|
219
|
+
RSpec/PredicateMatcher:
|
|
220
|
+
Strict: false
|
|
221
|
+
|
|
222
|
+
##### VICENZO #####
|
|
223
|
+
|
|
224
|
+
Vicenzo/Style/MultilineMethodCallParentheses:
|
|
225
|
+
AllowedMethods:
|
|
226
|
+
- to
|
|
227
|
+
- not_to
|
|
228
|
+
- to_not
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module Naming
|
|
7
|
+
# Requires every declaration to carry a name, even when the value it
|
|
8
|
+
# holds is not used.
|
|
9
|
+
#
|
|
10
|
+
# A bare `_` states that a value is being ignored, but erases what that
|
|
11
|
+
# value is. Unused today does not mean unusable tomorrow: when the code
|
|
12
|
+
# grows, nobody reading the signature can tell whether the discarded
|
|
13
|
+
# value is the one the new problem needs.
|
|
14
|
+
#
|
|
15
|
+
# The cop only demands that a name exists — it cannot judge whether a
|
|
16
|
+
# name is a good one. That is the author's call, and the offense message
|
|
17
|
+
# asks for a name taken from the domain (`_previous_owner`) rather than
|
|
18
|
+
# one taken from the shape of the value (`_value`, `_key`, `_item`).
|
|
19
|
+
#
|
|
20
|
+
# Anonymous forwarding parameters (`*`, `**`, `&`, `...`) are ignored:
|
|
21
|
+
# they forward arguments instead of discarding them, and `Style/ArgumentsForwarding`
|
|
22
|
+
# actively pushes code towards that form.
|
|
23
|
+
#
|
|
24
|
+
# @example block parameters
|
|
25
|
+
# # bad
|
|
26
|
+
# catalog.each { |_, copies| copies.each(&:reserve) }
|
|
27
|
+
#
|
|
28
|
+
# # good
|
|
29
|
+
# catalog.each { |_isbn, copies| copies.each(&:reserve) }
|
|
30
|
+
#
|
|
31
|
+
# @example destructured block parameters
|
|
32
|
+
# # bad
|
|
33
|
+
# entries.each { |(_, id)| track(id) }
|
|
34
|
+
#
|
|
35
|
+
# # good
|
|
36
|
+
# entries.each { |(_label, id)| track(id) }
|
|
37
|
+
#
|
|
38
|
+
# @example method parameters
|
|
39
|
+
# # bad
|
|
40
|
+
# def notify(reader, _)
|
|
41
|
+
# reader.deliver
|
|
42
|
+
# end
|
|
43
|
+
#
|
|
44
|
+
# # good
|
|
45
|
+
# def notify(reader, _delivery_channel)
|
|
46
|
+
# reader.deliver
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# @example multiple assignment
|
|
50
|
+
# # bad
|
|
51
|
+
# title, _ = line.split(';')
|
|
52
|
+
#
|
|
53
|
+
# # good
|
|
54
|
+
# title, _author = line.split(';')
|
|
55
|
+
#
|
|
56
|
+
# @example rescued errors
|
|
57
|
+
# # bad
|
|
58
|
+
# begin
|
|
59
|
+
# shelf.reorder
|
|
60
|
+
# rescue ShelfLocked => _
|
|
61
|
+
# shelf.skip
|
|
62
|
+
# end
|
|
63
|
+
#
|
|
64
|
+
# # good
|
|
65
|
+
# begin
|
|
66
|
+
# shelf.reorder
|
|
67
|
+
# rescue ShelfLocked => _lock_conflict
|
|
68
|
+
# shelf.skip
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# @example anonymous forwarding (ignored)
|
|
72
|
+
# # good
|
|
73
|
+
# def notify(*)
|
|
74
|
+
# channel.deliver(*)
|
|
75
|
+
# end
|
|
76
|
+
#
|
|
77
|
+
# # good
|
|
78
|
+
# def notify(...)
|
|
79
|
+
# channel.deliver(...)
|
|
80
|
+
# end
|
|
81
|
+
class AnonymousUnusedName < Base
|
|
82
|
+
MSG = 'Name this %<subject>s: an unused value still means something, and `_` hides what. ' \
|
|
83
|
+
'Name it after what it stands for in the domain (`_previous_owner`), not after its ' \
|
|
84
|
+
'shape (`_value`, `_key`, `_item`) — that tells the next reader nothing.'
|
|
85
|
+
|
|
86
|
+
BLOCK_PARAMETER = 'block parameter'
|
|
87
|
+
METHOD_PARAMETER = 'method parameter'
|
|
88
|
+
DISCARDED_VALUE = 'discarded value'
|
|
89
|
+
RESCUED_ERROR = 'rescued error'
|
|
90
|
+
|
|
91
|
+
ANONYMOUS_NAME_PATTERN = /\A_+\z/
|
|
92
|
+
|
|
93
|
+
NAMED_ARGUMENT_TYPES = %i[arg optarg restarg kwarg kwoptarg kwrestarg blockarg shadowarg].freeze
|
|
94
|
+
DEFAULT_VALUE_ARGUMENT_TYPES = %i[optarg kwoptarg].freeze
|
|
95
|
+
|
|
96
|
+
# Numbered (`_1`) and implicit (`it`) block parameters are never
|
|
97
|
+
# declared, so they carry no name to check — both node types answer
|
|
98
|
+
# `arguments` with an empty list.
|
|
99
|
+
def on_block(node)
|
|
100
|
+
check_arguments(node.arguments, BLOCK_PARAMETER)
|
|
101
|
+
end
|
|
102
|
+
alias on_numblock on_block
|
|
103
|
+
alias on_itblock on_block
|
|
104
|
+
|
|
105
|
+
def on_def(node)
|
|
106
|
+
check_arguments(node.arguments, METHOD_PARAMETER)
|
|
107
|
+
end
|
|
108
|
+
alias on_defs on_def
|
|
109
|
+
|
|
110
|
+
def on_masgn(node)
|
|
111
|
+
node.lhs.assignments.each do |assignment|
|
|
112
|
+
next unless assignment.respond_to?(:lvasgn_type?) && assignment.lvasgn_type?
|
|
113
|
+
|
|
114
|
+
register_offense(assignment, DISCARDED_VALUE) if anonymous_name?(assignment.children.first)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def on_resbody(node)
|
|
119
|
+
error = node.exception_variable
|
|
120
|
+
return if error.nil?
|
|
121
|
+
|
|
122
|
+
register_offense(error, RESCUED_ERROR) if anonymous_name?(error.children.first)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
private
|
|
126
|
+
|
|
127
|
+
def check_arguments(arguments, subject)
|
|
128
|
+
arguments.each do |argument|
|
|
129
|
+
if argument.mlhs_type?
|
|
130
|
+
check_arguments(argument.children, subject)
|
|
131
|
+
elsif anonymous_argument?(argument)
|
|
132
|
+
register_offense(argument_range(argument), subject)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# An anonymous forwarding parameter (`*`, `**`, `&`) carries no name
|
|
138
|
+
# child at all, and `...` is a `forward_arg`, outside the listed types.
|
|
139
|
+
# Both fall through as allowed.
|
|
140
|
+
def anonymous_argument?(argument)
|
|
141
|
+
NAMED_ARGUMENT_TYPES.include?(argument.type) && anonymous_name?(argument.children.first)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# An argument carrying a default value spans the whole expression, so
|
|
145
|
+
# only its name is highlighted; every other form is short enough that
|
|
146
|
+
# showing it whole (`*_`, `**_`, `&_`) tells the reader more.
|
|
147
|
+
def argument_range(argument)
|
|
148
|
+
return argument.loc.name if DEFAULT_VALUE_ARGUMENT_TYPES.include?(argument.type)
|
|
149
|
+
|
|
150
|
+
argument.source_range
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def anonymous_name?(name)
|
|
154
|
+
!name.nil? && ANONYMOUS_NAME_PATTERN.match?(name.to_s)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def register_offense(node, subject)
|
|
158
|
+
add_offense(node, message: format(MSG, subject: subject))
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module Naming
|
|
7
|
+
# Checks local variable names for how many letters they actually spell.
|
|
8
|
+
#
|
|
9
|
+
# A single letter or a two-letter abbreviation almost never carries the
|
|
10
|
+
# business meaning of the value it holds. `p`, `w`, `v`, `i` say nothing,
|
|
11
|
+
# and `_p` is still a single letter with an underscore in front of it.
|
|
12
|
+
#
|
|
13
|
+
# Underscores do not count towards the length, so `a_b` spells two
|
|
14
|
+
# letters, not three. The cop does not judge which name is a good one —
|
|
15
|
+
# it only measures how much was written down; whether the name belongs to
|
|
16
|
+
# the domain is the author's call, and the message asks for it.
|
|
17
|
+
#
|
|
18
|
+
# Block and method parameters are left to `Naming/BlockParameterName` and
|
|
19
|
+
# `Naming/MethodParameterName`; this gem's `config/style.yml` raises the
|
|
20
|
+
# former to the same minimum. Rescued errors are left to
|
|
21
|
+
# `Naming/RescuedExceptionsVariableName`, and names made only of
|
|
22
|
+
# underscores belong to `Vicenzo/Naming/AnonymousUnusedName`.
|
|
23
|
+
#
|
|
24
|
+
# A short name that exists only to feed a keyword argument shorthand is
|
|
25
|
+
# allowed: the name was imposed by the signature that receives it, not
|
|
26
|
+
# chosen, and it is that signature the parameter cops point at. The fix
|
|
27
|
+
# cascades from there.
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# # bad
|
|
31
|
+
# v = shelf.capacity
|
|
32
|
+
#
|
|
33
|
+
# # good
|
|
34
|
+
# volume = shelf.capacity
|
|
35
|
+
#
|
|
36
|
+
# @example multiple assignment
|
|
37
|
+
# # bad
|
|
38
|
+
# x, y = coordinates
|
|
39
|
+
#
|
|
40
|
+
# # good
|
|
41
|
+
# column, row = coordinates
|
|
42
|
+
#
|
|
43
|
+
# @example underscores do not count as letters
|
|
44
|
+
# # bad
|
|
45
|
+
# a_b = shelf.capacity
|
|
46
|
+
#
|
|
47
|
+
# # good
|
|
48
|
+
# available_books = shelf.capacity
|
|
49
|
+
#
|
|
50
|
+
# @example an unused name is still a name
|
|
51
|
+
# # bad
|
|
52
|
+
# _p = catalog.first
|
|
53
|
+
#
|
|
54
|
+
# # good
|
|
55
|
+
# _publisher = catalog.first
|
|
56
|
+
#
|
|
57
|
+
# @example keyword argument shorthand (allowed)
|
|
58
|
+
# # good
|
|
59
|
+
# height = 10
|
|
60
|
+
# width = 15
|
|
61
|
+
# area(height:, width:)
|
|
62
|
+
#
|
|
63
|
+
# # good — `h` and `w` are imposed by the signature of `area`
|
|
64
|
+
# h = 10
|
|
65
|
+
# w = 15
|
|
66
|
+
# area(h:, w:)
|
|
67
|
+
#
|
|
68
|
+
# @example MinNameLength: 5
|
|
69
|
+
# # bad
|
|
70
|
+
# book = build
|
|
71
|
+
#
|
|
72
|
+
# # good
|
|
73
|
+
# book_title = build
|
|
74
|
+
class ShortName < Base
|
|
75
|
+
MSG = 'Name `%<name>s` spells %<length>d letter(s); at least %<minimum>d are required. ' \
|
|
76
|
+
'A letter or an abbreviation carries no meaning to the next reader — write the ' \
|
|
77
|
+
'whole word, in the language of the domain (`waiting_time`), not a shapeless ' \
|
|
78
|
+
'label (`key`, `value`, `item`).'
|
|
79
|
+
|
|
80
|
+
DEFAULT_MIN_NAME_LENGTH = 3
|
|
81
|
+
|
|
82
|
+
ANONYMOUS_NAME_PATTERN = /\A_+\z/
|
|
83
|
+
|
|
84
|
+
SCOPE_TYPES = %i[def defs block numblock itblock].freeze
|
|
85
|
+
|
|
86
|
+
def on_lvasgn(node)
|
|
87
|
+
name = node.children.first
|
|
88
|
+
return if anonymous?(name) || rescued_error?(node) || forwarded_by_shorthand?(node, name)
|
|
89
|
+
|
|
90
|
+
length = spelled_length(name)
|
|
91
|
+
return if length >= min_name_length
|
|
92
|
+
|
|
93
|
+
add_offense(node.loc.name, message: format(MSG, name: name, length: length, minimum: min_name_length))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
# Underscores separate words, they do not spell any, so they are not
|
|
99
|
+
# counted: `a_b` is two letters long.
|
|
100
|
+
def spelled_length(name)
|
|
101
|
+
name.to_s.delete('_').length
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def anonymous?(name)
|
|
105
|
+
ANONYMOUS_NAME_PATTERN.match?(name.to_s)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def rescued_error?(node)
|
|
109
|
+
parent = node.parent
|
|
110
|
+
parent&.resbody_type? && parent.exception_variable == node
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# `area(h:)` omits the value, so `h` is the name the receiving signature
|
|
114
|
+
# asks for. Looking within the enclosing scope keeps unrelated calls
|
|
115
|
+
# elsewhere in the file from excusing the name.
|
|
116
|
+
def forwarded_by_shorthand?(node, name)
|
|
117
|
+
enclosing_scope(node)
|
|
118
|
+
.each_node(:pair)
|
|
119
|
+
.any? { |pair| pair.value_omission? && pair.key.value == name }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def enclosing_scope(node)
|
|
123
|
+
node.each_ancestor(*SCOPE_TYPES).first || processed_source.ast
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def min_name_length
|
|
127
|
+
cop_config.fetch('MinNameLength', DEFAULT_MIN_NAME_LENGTH)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# A describe nested in a context names a behaviour after the scenario was already set. Describe the
|
|
8
|
+
# behaviour first, and put the scenario inside it.
|
|
9
|
+
#
|
|
10
|
+
# The tree answers three questions, in this order: `describe` - what is being described;
|
|
11
|
+
# `context` - under which circumstances; `it` - with which outcome. A context only means something
|
|
12
|
+
# as a scenario of a behaviour, so a describe below it turns the question upside down.
|
|
13
|
+
#
|
|
14
|
+
# It usually comes from wanting to write a scenario once for two behaviours. Repeat the context
|
|
15
|
+
# under each describe that needs it instead: each behaviour then reads on its own. Do not reach for
|
|
16
|
+
# a `shared_context` to avoid the repetition - it becomes a single place every caller leans on,
|
|
17
|
+
# conditionals creep in to serve each of them, and years later nobody knows why they are there.
|
|
18
|
+
#
|
|
19
|
+
# When the describe states a circumstance rather than a behaviour, it is a context written with the
|
|
20
|
+
# wrong keyword.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# # bad - the scenario is set before the behaviours it belongs to
|
|
24
|
+
# RSpec.describe Person do
|
|
25
|
+
# context 'when the person is a minor' do
|
|
26
|
+
# subject(:person) { described_class.new(age: 17) }
|
|
27
|
+
#
|
|
28
|
+
# describe '#adult?' do
|
|
29
|
+
# it { is_expected.not_to be_adult }
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# describe '#voter?' do
|
|
33
|
+
# it { is_expected.not_to be_voter }
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # good - each behaviour carries its own scenario
|
|
39
|
+
# RSpec.describe Person do
|
|
40
|
+
# describe '#adult?' do
|
|
41
|
+
# context 'when the person is a minor' do
|
|
42
|
+
# subject(:person) { described_class.new(age: 17) }
|
|
43
|
+
#
|
|
44
|
+
# it { is_expected.not_to be_adult }
|
|
45
|
+
# end
|
|
46
|
+
# end
|
|
47
|
+
#
|
|
48
|
+
# describe '#voter?' do
|
|
49
|
+
# context 'when the person is a minor' do
|
|
50
|
+
# subject(:person) { described_class.new(age: 17) }
|
|
51
|
+
#
|
|
52
|
+
# it { is_expected.not_to be_voter }
|
|
53
|
+
# end
|
|
54
|
+
# end
|
|
55
|
+
# end
|
|
56
|
+
class DescribeInsideContext < RuboCop::Cop::RSpec::Base
|
|
57
|
+
MSG = 'Do not describe inside a context: a context is a scenario of the behaviour a describe names. ' \
|
|
58
|
+
'Describe the behaviour first and repeat the context under each describe that needs it. ' \
|
|
59
|
+
'If this block states a circumstance, it is a context.'
|
|
60
|
+
|
|
61
|
+
# Only the groups that name a behaviour. Aliases a project adds to the RSpec language, such as
|
|
62
|
+
# rswag's `response`, are left out: they do not describe anything.
|
|
63
|
+
DESCRIBES = %i[describe fdescribe xdescribe feature ffeature xfeature example_group].freeze
|
|
64
|
+
CONTEXTS = %i[context fcontext xcontext].freeze
|
|
65
|
+
|
|
66
|
+
# @!method describe_definition?(node)
|
|
67
|
+
def_node_matcher :describe_definition?, <<~PATTERN
|
|
68
|
+
(any_block (send #rspec? {#{DESCRIBES.map(&:inspect).join(' ')}} ...) ...)
|
|
69
|
+
PATTERN
|
|
70
|
+
|
|
71
|
+
# @!method context_definition?(node)
|
|
72
|
+
def_node_matcher :context_definition?, <<~PATTERN
|
|
73
|
+
(any_block (send nil? {#{CONTEXTS.map(&:inspect).join(' ')}} ...) ...)
|
|
74
|
+
PATTERN
|
|
75
|
+
|
|
76
|
+
def on_block(node)
|
|
77
|
+
add_offense(node.send_node) if describe_definition?(node) && inside_a_context?(node)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
alias on_numblock on_block
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# A shared group's scenario lives where it is included, so it cannot be judged here.
|
|
85
|
+
def inside_a_context?(node)
|
|
86
|
+
parent = node.each_ancestor(:any_block).find { |ancestor| spec_group?(ancestor) }
|
|
87
|
+
|
|
88
|
+
parent && context_definition?(parent)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -46,10 +46,25 @@ module RuboCop
|
|
|
46
46
|
ENUMERATION_METHODS = %i[each each_with_index each_with_object map flat_map].freeze
|
|
47
47
|
|
|
48
48
|
EXAMPLE_GROUP_DSL = %i[
|
|
49
|
-
context
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
context
|
|
50
|
+
describe
|
|
51
|
+
feature
|
|
52
|
+
experiment
|
|
53
|
+
it
|
|
54
|
+
specify
|
|
55
|
+
example
|
|
56
|
+
scenario
|
|
57
|
+
focus
|
|
58
|
+
let
|
|
59
|
+
let!
|
|
60
|
+
subject
|
|
61
|
+
subject!
|
|
62
|
+
before
|
|
63
|
+
after
|
|
64
|
+
around
|
|
65
|
+
shared_examples
|
|
66
|
+
shared_context
|
|
67
|
+
shared_examples_for
|
|
53
68
|
].freeze
|
|
54
69
|
|
|
55
70
|
# @!method enumeration_block?(node)
|
|
@@ -76,8 +76,19 @@ module RuboCop
|
|
|
76
76
|
'trait/transient, or declare the final state in the context that needs it.'
|
|
77
77
|
|
|
78
78
|
PERSISTENCE_METHODS = %i[
|
|
79
|
-
update
|
|
80
|
-
|
|
79
|
+
update
|
|
80
|
+
update!
|
|
81
|
+
update_attribute
|
|
82
|
+
update_column
|
|
83
|
+
update_columns
|
|
84
|
+
save
|
|
85
|
+
save!
|
|
86
|
+
destroy
|
|
87
|
+
destroy!
|
|
88
|
+
touch
|
|
89
|
+
increment!
|
|
90
|
+
decrement!
|
|
91
|
+
toggle!
|
|
81
92
|
].freeze
|
|
82
93
|
|
|
83
94
|
COLLECTION_METHODS = %i[<< push concat unshift store merge! deep_merge! clear delete].freeze
|
|
@@ -92,7 +92,7 @@ module RuboCop
|
|
|
92
92
|
def check_let(let_node, let_definitions)
|
|
93
93
|
name = (let_name(let_node) || let_it_be_name(let_node))&.to_s&.to_sym
|
|
94
94
|
|
|
95
|
-
if let_definitions.
|
|
95
|
+
if let_definitions.has_key?(name)
|
|
96
96
|
add_offense(let_node, message: redefined_let_message(name, let_definitions))
|
|
97
97
|
|
|
98
98
|
let_definitions[name] += [line_location(let_node)]
|
|
@@ -94,7 +94,7 @@ module RuboCop
|
|
|
94
94
|
def check_subject(subject_node, subject_definitions)
|
|
95
95
|
name = name_for(subject_node)
|
|
96
96
|
|
|
97
|
-
if subject_definitions.
|
|
97
|
+
if subject_definitions.has_key?(name)
|
|
98
98
|
add_offense(subject_node, message: redefined_subject_message(name, subject_definitions))
|
|
99
99
|
subject_definitions[name] += [line_location(subject_node)]
|
|
100
100
|
else
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Vicenzo
|
|
6
|
+
module RSpec
|
|
7
|
+
# Checks for `context` blocks with no parent scenario that start with "and", "but", or "however".
|
|
8
|
+
# It suggests starting them with "when", "with", or "without" instead.
|
|
9
|
+
#
|
|
10
|
+
# A conjunction continues the scenario of the enclosing context. When the closest example group is
|
|
11
|
+
# not a context - a `describe`, or no group at all - there is no scenario to continue, and the
|
|
12
|
+
# description reads as a fragment. It usually shows up after a subtree is moved up a level: the
|
|
13
|
+
# parent goes away, the titles stay as they were.
|
|
14
|
+
#
|
|
15
|
+
# The closest example group decides. A shared group is left alone, since the scenario it continues
|
|
16
|
+
# lives where it is included.
|
|
17
|
+
#
|
|
18
|
+
# The words that continue a scenario come from `ForbiddenPrefixes`, compared against the first word
|
|
19
|
+
# of the description, ignoring case. Setting it replaces the defaults; to add words to them, declare
|
|
20
|
+
# `inherit_mode: { merge: [ForbiddenPrefixes] }` for the cop.
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# # bad
|
|
24
|
+
# describe '#available_colors' do
|
|
25
|
+
# context 'but the color pink is not available' do
|
|
26
|
+
# it 'does not show the pink option'
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# # good
|
|
31
|
+
# describe '#available_colors' do
|
|
32
|
+
# context 'when the color pink is not available' do
|
|
33
|
+
# it 'does not show the pink option'
|
|
34
|
+
# end
|
|
35
|
+
# end
|
|
36
|
+
#
|
|
37
|
+
# @example ForbiddenPrefixes: ['and', 'but', 'however', 'also']
|
|
38
|
+
# # bad
|
|
39
|
+
# describe '#available_colors' do
|
|
40
|
+
# context 'also when the color pink is not available' do
|
|
41
|
+
# it 'does not show the pink option'
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
class UnnestedContextImproperStart < RuboCop::Cop::RSpec::Base
|
|
45
|
+
MSG = 'Unnested `context` should start with `when`, `with`, or `without`, not `%<prefix>s`.'
|
|
46
|
+
|
|
47
|
+
CONTEXTS = %i[context fcontext xcontext].freeze
|
|
48
|
+
DEFAULT_FORBIDDEN_PREFIXES = %w[and but however].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
|
+
def on_block(node)
|
|
56
|
+
return unless context_definition?(node)
|
|
57
|
+
return if continues_a_scenario?(node)
|
|
58
|
+
|
|
59
|
+
prefix = first_word(node)
|
|
60
|
+
|
|
61
|
+
add_offense(node.send_node, message: format(MSG, prefix:)) if forbidden_prefixes.include?(prefix)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alias on_numblock on_block
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# A context continues its scenario, and a shared group's scenario lives where it is included, so
|
|
69
|
+
# neither can be judged here.
|
|
70
|
+
def continues_a_scenario?(node)
|
|
71
|
+
parent = node.each_ancestor(:any_block).find { |ancestor| spec_group?(ancestor) }
|
|
72
|
+
|
|
73
|
+
parent && (shared_group?(parent) || context_definition?(parent))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def first_word(node)
|
|
77
|
+
description = node.send_node.first_argument
|
|
78
|
+
|
|
79
|
+
description.value.lstrip[/\A[[:alpha:]]+/]&.downcase if description&.str_type?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def forbidden_prefixes
|
|
83
|
+
cop_config.fetch('ForbiddenPrefixes', DEFAULT_FORBIDDEN_PREFIXES).map(&:downcase)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'vicenzo/rspec/competing_subjects'
|
|
4
4
|
require_relative 'vicenzo/rspec/conditional_in_spec'
|
|
5
5
|
require_relative 'vicenzo/rspec/derived_premises'
|
|
6
|
+
require_relative 'vicenzo/rspec/describe_inside_context'
|
|
6
7
|
require_relative 'vicenzo/rspec/dynamic_example_generation'
|
|
7
8
|
require_relative 'vicenzo/rspec/inconsistent_sibling_structure'
|
|
8
9
|
require_relative 'vicenzo/rspec/iteration_inside_example'
|
|
@@ -19,7 +20,10 @@ require_relative 'vicenzo/rspec/top_level_example'
|
|
|
19
20
|
require_relative 'vicenzo/rspec/top_level_hook'
|
|
20
21
|
require_relative 'vicenzo/rspec/top_level_let'
|
|
21
22
|
require_relative 'vicenzo/rspec/top_level_subject'
|
|
23
|
+
require_relative 'vicenzo/rspec/unnested_context_improper_start'
|
|
22
24
|
require_relative 'vicenzo/rails/enum_inclusion_of_validation'
|
|
23
25
|
require_relative 'vicenzo/layout/multiline_method_call_line_breaks'
|
|
26
|
+
require_relative 'vicenzo/naming/anonymous_unused_name'
|
|
27
|
+
require_relative 'vicenzo/naming/short_name'
|
|
24
28
|
require_relative 'vicenzo/style/json_parse_symbolize_names'
|
|
25
29
|
require_relative 'vicenzo/style/multiline_method_call_parentheses'
|
data/rakelib/docs.rake
CHANGED
|
@@ -155,16 +155,26 @@ end
|
|
|
155
155
|
def render_metadata_table(cop)
|
|
156
156
|
enabled = cop[:enabled] ? 'Enabled' : 'Disabled'
|
|
157
157
|
autocorrect = cop[:autocorrect] ? 'Yes' : 'No'
|
|
158
|
-
[
|
|
159
|
-
|
|
158
|
+
[
|
|
159
|
+
'[cols="1,1,1,1"]',
|
|
160
|
+
'|===',
|
|
161
|
+
'| Enabled by default | Safe | Supports autocorrection | Version Added',
|
|
162
|
+
'',
|
|
163
|
+
"| #{enabled}",
|
|
164
|
+
'| Yes',
|
|
165
|
+
"| #{autocorrect}",
|
|
166
|
+
"| #{cop[:version]}",
|
|
167
|
+
'|===',
|
|
168
|
+
''
|
|
169
|
+
]
|
|
160
170
|
end
|
|
161
171
|
|
|
162
172
|
def render_example(example, index)
|
|
163
173
|
title = if example[:title].empty?
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
174
|
+
index.zero? ? 'Example' : "Example #{index + 1}"
|
|
175
|
+
else
|
|
176
|
+
example[:title]
|
|
177
|
+
end
|
|
168
178
|
["=== #{title}", '', '[source,ruby]', '----', *example[:code], '----', '']
|
|
169
179
|
end
|
|
170
180
|
|
|
@@ -198,13 +208,28 @@ def installation_section_lines
|
|
|
198
208
|
end
|
|
199
209
|
|
|
200
210
|
def gemfile_installation_lines
|
|
201
|
-
[
|
|
202
|
-
|
|
211
|
+
[
|
|
212
|
+
'Add to your `Gemfile`:',
|
|
213
|
+
'',
|
|
214
|
+
'[source,ruby]',
|
|
215
|
+
'----',
|
|
216
|
+
"gem 'rubocop-vicenzo', require: false",
|
|
217
|
+
'----',
|
|
218
|
+
''
|
|
219
|
+
]
|
|
203
220
|
end
|
|
204
221
|
|
|
205
222
|
def rubocop_yml_installation_lines
|
|
206
|
-
[
|
|
207
|
-
|
|
223
|
+
[
|
|
224
|
+
'Then add to your `.rubocop.yml`:',
|
|
225
|
+
'',
|
|
226
|
+
'[source,yaml]',
|
|
227
|
+
'----',
|
|
228
|
+
'plugins:',
|
|
229
|
+
' - rubocop-vicenzo',
|
|
230
|
+
'----',
|
|
231
|
+
''
|
|
232
|
+
]
|
|
208
233
|
end
|
|
209
234
|
|
|
210
235
|
def index_cops_table_lines(cop_data)
|
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.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bruno Vicenzo
|
|
@@ -70,14 +70,18 @@ files:
|
|
|
70
70
|
- Rakefile
|
|
71
71
|
- antora-playbook.yml
|
|
72
72
|
- config/default.yml
|
|
73
|
+
- config/style.yml
|
|
73
74
|
- docs/antora.yml
|
|
74
75
|
- docs/supplemental-ui/partials/header-content.hbs
|
|
75
76
|
- lib/rubocop-vicenzo.rb
|
|
76
77
|
- lib/rubocop/cop/vicenzo/layout/multiline_method_call_line_breaks.rb
|
|
78
|
+
- lib/rubocop/cop/vicenzo/naming/anonymous_unused_name.rb
|
|
79
|
+
- lib/rubocop/cop/vicenzo/naming/short_name.rb
|
|
77
80
|
- lib/rubocop/cop/vicenzo/rails/enum_inclusion_of_validation.rb
|
|
78
81
|
- lib/rubocop/cop/vicenzo/rspec/competing_subjects.rb
|
|
79
82
|
- lib/rubocop/cop/vicenzo/rspec/conditional_in_spec.rb
|
|
80
83
|
- lib/rubocop/cop/vicenzo/rspec/derived_premises.rb
|
|
84
|
+
- lib/rubocop/cop/vicenzo/rspec/describe_inside_context.rb
|
|
81
85
|
- lib/rubocop/cop/vicenzo/rspec/dynamic_example_generation.rb
|
|
82
86
|
- lib/rubocop/cop/vicenzo/rspec/inconsistent_sibling_structure.rb
|
|
83
87
|
- lib/rubocop/cop/vicenzo/rspec/iteration_inside_example.rb
|
|
@@ -98,6 +102,7 @@ files:
|
|
|
98
102
|
- lib/rubocop/cop/vicenzo/rspec/top_level_hook.rb
|
|
99
103
|
- lib/rubocop/cop/vicenzo/rspec/top_level_let.rb
|
|
100
104
|
- lib/rubocop/cop/vicenzo/rspec/top_level_subject.rb
|
|
105
|
+
- lib/rubocop/cop/vicenzo/rspec/unnested_context_improper_start.rb
|
|
101
106
|
- lib/rubocop/cop/vicenzo/style/json_parse_symbolize_names.rb
|
|
102
107
|
- lib/rubocop/cop/vicenzo/style/multiline_method_call_parentheses.rb
|
|
103
108
|
- lib/rubocop/cop/vicenzo_cops.rb
|
|
@@ -131,7 +136,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
131
136
|
- !ruby/object:Gem::Version
|
|
132
137
|
version: '0'
|
|
133
138
|
requirements: []
|
|
134
|
-
rubygems_version: 4.0.
|
|
139
|
+
rubygems_version: 4.0.20
|
|
135
140
|
specification_version: 4
|
|
136
141
|
summary: Cops of Bruno Vicenzo
|
|
137
142
|
test_files: []
|