rubocop-rspec-guide 0.2.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +37 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +37 -0
- data/LICENSE.txt +21 -0
- data/README.md +296 -0
- data/Rakefile +10 -0
- data/config/default.yml +38 -0
- data/devbox.json +14 -0
- data/devbox.lock +74 -0
- data/lib/rubocop/cop/factory_bot_guide/dynamic_attributes_for_time_and_random.rb +121 -0
- data/lib/rubocop/cop/rspec_guide/characteristics_and_contexts.rb +91 -0
- data/lib/rubocop/cop/rspec_guide/context_setup.rb +88 -0
- data/lib/rubocop/cop/rspec_guide/duplicate_before_hooks.rb +163 -0
- data/lib/rubocop/cop/rspec_guide/duplicate_let_values.rb +193 -0
- data/lib/rubocop/cop/rspec_guide/happy_path_first.rb +116 -0
- data/lib/rubocop/cop/rspec_guide/invariant_examples.rb +143 -0
- data/lib/rubocop/rspec/guide/version.rb +9 -0
- data/lib/rubocop/rspec/guide.rb +12 -0
- data/lib/rubocop-rspec-guide.rb +13 -0
- data/sig/rubocop/rspec/guide.rbs +8 -0
- metadata +136 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dbcc5261e61c2e9d16ccfa146e62046779e7e48f782a8fdb5003203cd55a99a4
|
|
4
|
+
data.tar.gz: 3b8c2dae7e97d1d892ba9077944c93de7370803ed3c92edfeb2e43e33a4f9c3f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: f599508c8ad0870461f04847d6b6c727647557c645231b46df9e8bc63737e2118f50d9590fe7cfbec29668941170fe138187bb939bb0d750938f696c5de4bb2c
|
|
7
|
+
data.tar.gz: 8c9820bc4f21d0a7e82b1711f3abfc85a7131bc7b27b7b46b72c719f02c9426b4ad53f42512e6050677aa41ba903a1db1d45444fba35cd80fe609f0252b75b56
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
require:
|
|
2
|
+
- ./lib/rubocop-rspec-guide
|
|
3
|
+
|
|
4
|
+
inherit_gem:
|
|
5
|
+
standard: config/base.yml
|
|
6
|
+
|
|
7
|
+
AllCops:
|
|
8
|
+
NewCops: enable
|
|
9
|
+
TargetRubyVersion: 3.0
|
|
10
|
+
Exclude:
|
|
11
|
+
- 'vendor/**/*'
|
|
12
|
+
- 'tmp/**/*'
|
|
13
|
+
- 'bin/**/*'
|
|
14
|
+
- '.devbox/**/*'
|
|
15
|
+
|
|
16
|
+
# Enable our custom cops
|
|
17
|
+
RSpecGuide/CharacteristicsAndContexts:
|
|
18
|
+
Enabled: true
|
|
19
|
+
|
|
20
|
+
RSpecGuide/HappyPathFirst:
|
|
21
|
+
Enabled: true
|
|
22
|
+
|
|
23
|
+
RSpecGuide/ContextSetup:
|
|
24
|
+
Enabled: true
|
|
25
|
+
|
|
26
|
+
RSpecGuide/DuplicateLetValues:
|
|
27
|
+
Enabled: true
|
|
28
|
+
|
|
29
|
+
RSpecGuide/DuplicateBeforeHooks:
|
|
30
|
+
Enabled: true
|
|
31
|
+
|
|
32
|
+
RSpecGuide/InvariantExamples:
|
|
33
|
+
Enabled: true
|
|
34
|
+
MinLeafContexts: 3
|
|
35
|
+
|
|
36
|
+
FactoryBotGuide/DynamicAttributesForTimeAndRandom:
|
|
37
|
+
Enabled: true
|
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
## [Unreleased]
|
|
2
|
+
|
|
3
|
+
## [0.2.0] - 2025-10-28
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- **RSpecGuide/ContextSetup**: `subject` is no longer accepted as valid context setup (BREAKING)
|
|
7
|
+
- Subject describes the object under test and should be at describe level
|
|
8
|
+
- Use `RSpec/LeadingSubject` cop to enforce subject placement
|
|
9
|
+
- Context setup now only accepts: `let`, `let!`, `before`
|
|
10
|
+
|
|
11
|
+
### Removed
|
|
12
|
+
- **RSpecGuide/TravelWithoutTravelBack**: Removed cop as it's redundant
|
|
13
|
+
- Rails automatically cleans up time stubs via `after_teardown` in `RailsExampleGroup`
|
|
14
|
+
- `MinitestLifecycleAdapter` is included by default in rspec-rails
|
|
15
|
+
- Manual `after { travel_back }` is not needed
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **RSpecGuide/ContextSetup**: Fixed logic to properly detect setup in context body
|
|
19
|
+
- **RSpecGuide/CharacteristicsAndContexts**: Fixed AST traversal to handle `begin` nodes
|
|
20
|
+
- **RSpecGuide/HappyPathFirst**: Fixed AST traversal to handle `begin` nodes
|
|
21
|
+
- Test expectations now include cop name prefixes (e.g., `RSpecGuide/ContextSetup:`)
|
|
22
|
+
|
|
23
|
+
## [0.1.0] - 2025-10-28
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- Initial release with 7 custom RuboCop cops for RSpec best practices
|
|
28
|
+
- **RSpecGuide/CharacteristicsAndContexts**: Requires at least 2 contexts in describe blocks (Rule 4)
|
|
29
|
+
- **RSpecGuide/HappyPathFirst**: Ensures happy paths come before corner cases (Rule 7)
|
|
30
|
+
- **RSpecGuide/ContextSetup**: Requires setup (let/before) in contexts (Rule 9)
|
|
31
|
+
- **RSpecGuide/DuplicateLetValues**: Detects duplicate let declarations with same values (Rule 6)
|
|
32
|
+
- **RSpecGuide/DuplicateBeforeHooks**: Detects duplicate before hooks (Rule 6)
|
|
33
|
+
- **RSpecGuide/InvariantExamples**: Finds examples repeated in all leaf contexts (Rule 6)
|
|
34
|
+
- **FactoryBotGuide/DynamicAttributesForTimeAndRandom**: Ensures Time.now and SecureRandom are wrapped in blocks
|
|
35
|
+
- Comprehensive test suite with RSpec
|
|
36
|
+
- Default configuration file (config/default.yml)
|
|
37
|
+
- Documentation and usage examples in README
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 installer
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# RuboCop RSpec Guide
|
|
2
|
+
|
|
3
|
+
Custom RuboCop cops that enforce best practices from the [RSpec Style Guide](https://github.com/AlexeyMatskevich/rspec-guide).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'rubocop-rspec-guide', require: false
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install it yourself:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
gem install rubocop-rspec-guide
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Add to your `.rubocop.yml`:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
require:
|
|
25
|
+
- rubocop-rspec-guide
|
|
26
|
+
|
|
27
|
+
# Optionally inherit the default config
|
|
28
|
+
inherit_gem:
|
|
29
|
+
rubocop-rspec-guide: config/default.yml
|
|
30
|
+
|
|
31
|
+
# Recommended: Enable RSpec/LeadingSubject to ensure subject is at describe level
|
|
32
|
+
RSpec/LeadingSubject:
|
|
33
|
+
Enabled: true
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or configure cops individually:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
require:
|
|
40
|
+
- rubocop-rspec-guide
|
|
41
|
+
|
|
42
|
+
RSpecGuide/CharacteristicsAndContexts:
|
|
43
|
+
Enabled: true
|
|
44
|
+
|
|
45
|
+
RSpecGuide/HappyPathFirst:
|
|
46
|
+
Enabled: true
|
|
47
|
+
|
|
48
|
+
RSpecGuide/ContextSetup:
|
|
49
|
+
Enabled: true
|
|
50
|
+
|
|
51
|
+
RSpecGuide/DuplicateLetValues:
|
|
52
|
+
Enabled: true
|
|
53
|
+
|
|
54
|
+
RSpecGuide/DuplicateBeforeHooks:
|
|
55
|
+
Enabled: true
|
|
56
|
+
|
|
57
|
+
RSpecGuide/InvariantExamples:
|
|
58
|
+
Enabled: true
|
|
59
|
+
MinLeafContexts: 3
|
|
60
|
+
|
|
61
|
+
FactoryBotGuide/DynamicAttributesForTimeAndRandom:
|
|
62
|
+
Enabled: true
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Cops
|
|
66
|
+
|
|
67
|
+
### RSpecGuide/CharacteristicsAndContexts
|
|
68
|
+
|
|
69
|
+
Requires at least 2 contexts in a describe block (happy path + edge cases).
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
# bad
|
|
73
|
+
describe '#calculate' do
|
|
74
|
+
it 'works' { expect(result).to eq(100) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# good
|
|
78
|
+
describe '#calculate' do
|
|
79
|
+
context 'with valid data' do
|
|
80
|
+
it { expect(result).to eq(100) }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
context 'with invalid data' do
|
|
84
|
+
it { expect(result).to be_error }
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### RSpecGuide/HappyPathFirst
|
|
90
|
+
|
|
91
|
+
Ensures corner cases are not placed before happy paths.
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# bad
|
|
95
|
+
describe '#process' do
|
|
96
|
+
context 'but user is blocked' do
|
|
97
|
+
# ...
|
|
98
|
+
end
|
|
99
|
+
context 'when user is valid' do
|
|
100
|
+
# ...
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# good
|
|
105
|
+
describe '#process' do
|
|
106
|
+
context 'when user is valid' do
|
|
107
|
+
# ...
|
|
108
|
+
end
|
|
109
|
+
context 'but user is blocked' do
|
|
110
|
+
# ...
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### RSpecGuide/ContextSetup
|
|
116
|
+
|
|
117
|
+
Requires contexts to have setup (let/let!/let_it_be/let_it_be!/before) to distinguish them from parent.
|
|
118
|
+
|
|
119
|
+
**Note:** `subject` should be defined at `describe` level, not in contexts, as it describes the object under test. Use `RSpec/LeadingSubject` cop to ensure subject is defined first.
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
# bad - no setup
|
|
123
|
+
context 'when premium' do
|
|
124
|
+
it { expect(user).to have_access }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# bad - subject in context (should be in describe)
|
|
128
|
+
context 'when premium' do
|
|
129
|
+
subject { user } # Wrong place!
|
|
130
|
+
it { is_expected.to have_access }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# good - let defines context-specific state
|
|
134
|
+
context 'when premium' do
|
|
135
|
+
let(:user) { create(:user, :premium) }
|
|
136
|
+
it { expect(user).to have_access }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# good - let_it_be for performance (from test-prof/rspec-rails)
|
|
140
|
+
context 'when premium' do
|
|
141
|
+
let_it_be(:user) { create(:user, :premium) }
|
|
142
|
+
it { expect(user).to have_access }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# good - before sets up context
|
|
146
|
+
context 'when premium' do
|
|
147
|
+
before { user.upgrade_to_premium! }
|
|
148
|
+
it { expect(user).to have_access }
|
|
149
|
+
end
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### RSpecGuide/DuplicateLetValues
|
|
153
|
+
|
|
154
|
+
Detects duplicate let declarations across sibling contexts with severity levels.
|
|
155
|
+
|
|
156
|
+
**Severity Levels:**
|
|
157
|
+
- **ERROR** - When let is duplicated in ALL sibling contexts → must extract to parent
|
|
158
|
+
- **WARNING** - When let is duplicated in 2+ contexts but not all → suggests bad test hierarchy
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# bad - ERROR (in ALL contexts)
|
|
162
|
+
context 'A' do
|
|
163
|
+
let(:currency) { :usd }
|
|
164
|
+
end
|
|
165
|
+
context 'B' do
|
|
166
|
+
let(:currency) { :usd } # duplicate in all!
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# bad - WARNING (partial duplicate, code smell)
|
|
170
|
+
context 'A' do
|
|
171
|
+
let(:currency) { :usd }
|
|
172
|
+
end
|
|
173
|
+
context 'B' do
|
|
174
|
+
let(:currency) { :usd } # duplicated in 2/3 contexts
|
|
175
|
+
end
|
|
176
|
+
context 'C' do
|
|
177
|
+
let(:currency) { :eur } # different value
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# good
|
|
181
|
+
let(:currency) { :usd } # extract to parent
|
|
182
|
+
context 'A' do; end
|
|
183
|
+
context 'B' do; end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Configuration:**
|
|
187
|
+
```yaml
|
|
188
|
+
RSpecGuide/DuplicateLetValues:
|
|
189
|
+
WarnOnPartialDuplicates: true # Show warnings for partial duplicates (default: true)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### RSpecGuide/DuplicateBeforeHooks
|
|
193
|
+
|
|
194
|
+
Detects duplicate before hooks across sibling contexts with severity levels.
|
|
195
|
+
|
|
196
|
+
**Severity Levels:**
|
|
197
|
+
- **ERROR** - When before hook is duplicated in ALL sibling contexts → must extract to parent
|
|
198
|
+
- **WARNING** - When before hook is duplicated in 2+ contexts but not all → suggests bad test hierarchy
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
# bad - ERROR (in ALL contexts)
|
|
202
|
+
context 'A' do
|
|
203
|
+
before { sign_in(user) }
|
|
204
|
+
end
|
|
205
|
+
context 'B' do
|
|
206
|
+
before { sign_in(user) } # duplicate in all!
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# bad - WARNING (partial duplicate, code smell)
|
|
210
|
+
context 'A' do
|
|
211
|
+
before { setup }
|
|
212
|
+
end
|
|
213
|
+
context 'B' do
|
|
214
|
+
# no before
|
|
215
|
+
end
|
|
216
|
+
context 'C' do
|
|
217
|
+
before { setup } # duplicated in 2/3 contexts
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# good
|
|
221
|
+
before { sign_in(user) } # extract to parent
|
|
222
|
+
context 'A' do; end
|
|
223
|
+
context 'B' do; end
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
**Configuration:**
|
|
227
|
+
```yaml
|
|
228
|
+
RSpecGuide/DuplicateBeforeHooks:
|
|
229
|
+
WarnOnPartialDuplicates: true # Show warnings for partial duplicates (default: true)
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### RSpecGuide/InvariantExamples
|
|
233
|
+
|
|
234
|
+
Detects examples repeated in all leaf contexts.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
# bad - same example in all 3 contexts
|
|
238
|
+
context 'A' do
|
|
239
|
+
it 'responds to valid?' { }
|
|
240
|
+
end
|
|
241
|
+
context 'B' do
|
|
242
|
+
it 'responds to valid?' { }
|
|
243
|
+
end
|
|
244
|
+
context 'C' do
|
|
245
|
+
it 'responds to valid?' { }
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# good - extract to shared_examples
|
|
249
|
+
shared_examples 'a validator' do
|
|
250
|
+
it 'responds to valid?' { }
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
context 'A' do
|
|
254
|
+
it_behaves_like 'a validator'
|
|
255
|
+
end
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### FactoryBotGuide/DynamicAttributesForTimeAndRandom
|
|
259
|
+
|
|
260
|
+
Ensures time and random values are wrapped in blocks.
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
# bad
|
|
264
|
+
factory :user do
|
|
265
|
+
created_at Time.now # evaluated once!
|
|
266
|
+
token SecureRandom.hex # same token for all users!
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# good
|
|
270
|
+
factory :user do
|
|
271
|
+
created_at { Time.now }
|
|
272
|
+
token { SecureRandom.hex }
|
|
273
|
+
end
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
After checking out the repo:
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
bundle install
|
|
282
|
+
bundle exec rspec
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Contributing
|
|
286
|
+
|
|
287
|
+
Bug reports and pull requests are welcome on GitHub.
|
|
288
|
+
|
|
289
|
+
## License
|
|
290
|
+
|
|
291
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
292
|
+
|
|
293
|
+
## References
|
|
294
|
+
|
|
295
|
+
- [RSpec Style Guide](https://github.com/AlexeyMatskevich/rspec-guide)
|
|
296
|
+
- [RuboCop RSpec](https://github.com/rubocop/rubocop-rspec)
|
data/Rakefile
ADDED
data/config/default.yml
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
RSpecGuide/CharacteristicsAndContexts:
|
|
3
|
+
Description: "Require at least 2 contexts in describe block (happy path + edge cases)"
|
|
4
|
+
Enabled: true
|
|
5
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
6
|
+
|
|
7
|
+
RSpecGuide/DuplicateLetValues:
|
|
8
|
+
Description: "Detect duplicate let values in sibling contexts (ERROR if in all, WARNING if partial)"
|
|
9
|
+
Enabled: true
|
|
10
|
+
WarnOnPartialDuplicates: true
|
|
11
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
12
|
+
|
|
13
|
+
RSpecGuide/DuplicateBeforeHooks:
|
|
14
|
+
Description: "Detect duplicate before hooks in sibling contexts (ERROR if in all, WARNING if partial)"
|
|
15
|
+
Enabled: true
|
|
16
|
+
WarnOnPartialDuplicates: true
|
|
17
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
18
|
+
|
|
19
|
+
RSpecGuide/InvariantExamples:
|
|
20
|
+
Description: "Invariant examples should be in leaf contexts or extracted to shared_examples"
|
|
21
|
+
Enabled: true
|
|
22
|
+
MinLeafContexts: 3
|
|
23
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
24
|
+
|
|
25
|
+
RSpecGuide/HappyPathFirst:
|
|
26
|
+
Description: "Corner cases should not be first context (happy path first)"
|
|
27
|
+
Enabled: true
|
|
28
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
29
|
+
|
|
30
|
+
RSpecGuide/ContextSetup:
|
|
31
|
+
Description: "Context must have setup (let/before) to distinguish it from parent. Subject should be at describe level."
|
|
32
|
+
Enabled: true
|
|
33
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
|
34
|
+
|
|
35
|
+
FactoryBotGuide/DynamicAttributesForTimeAndRandom:
|
|
36
|
+
Description: "Wrap Time.now, SecureRandom and method calls in blocks for dynamic evaluation"
|
|
37
|
+
Enabled: true
|
|
38
|
+
StyleGuideUrl: "https://github.com/AlexeyMatskevich/rspec-guide"
|
data/devbox.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json",
|
|
3
|
+
"packages": ["ruby@latest"],
|
|
4
|
+
"shell": {
|
|
5
|
+
"init_hook": [
|
|
6
|
+
"echo 'Welcome to devbox!' > /dev/null"
|
|
7
|
+
],
|
|
8
|
+
"scripts": {
|
|
9
|
+
"test": [
|
|
10
|
+
"echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
]
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
data/devbox.lock
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfile_version": "1",
|
|
3
|
+
"packages": {
|
|
4
|
+
"github:NixOS/nixpkgs/nixpkgs-unstable": {
|
|
5
|
+
"last_modified": "2025-10-23T16:27:14Z",
|
|
6
|
+
"resolved": "github:NixOS/nixpkgs/d5faa84122bc0a1fd5d378492efce4e289f8eac1?lastModified=1761236834&narHash=sha256-%2Bpthv6hrL5VLW2UqPdISGuLiUZ6SnAXdd2DdUE%2BfV2Q%3D"
|
|
7
|
+
},
|
|
8
|
+
"ruby@latest": {
|
|
9
|
+
"last_modified": "2025-10-09T02:37:25Z",
|
|
10
|
+
"plugin_version": "0.0.2",
|
|
11
|
+
"resolved": "github:NixOS/nixpkgs/2dad7af78a183b6c486702c18af8a9544f298377#ruby_3_4",
|
|
12
|
+
"source": "devbox-search",
|
|
13
|
+
"version": "3.4.7",
|
|
14
|
+
"systems": {
|
|
15
|
+
"aarch64-darwin": {
|
|
16
|
+
"outputs": [
|
|
17
|
+
{
|
|
18
|
+
"name": "out",
|
|
19
|
+
"path": "/nix/store/apawwv6ln4qlhar4x31hk83x35d88d4n-ruby-3.4.7",
|
|
20
|
+
"default": true
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "devdoc",
|
|
24
|
+
"path": "/nix/store/hwsjj4lwr4j6i4s71kgn82pq3wqdyk84-ruby-3.4.7-devdoc"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"store_path": "/nix/store/apawwv6ln4qlhar4x31hk83x35d88d4n-ruby-3.4.7"
|
|
28
|
+
},
|
|
29
|
+
"aarch64-linux": {
|
|
30
|
+
"outputs": [
|
|
31
|
+
{
|
|
32
|
+
"name": "out",
|
|
33
|
+
"path": "/nix/store/wxx9v5gnz8nm8rwzy6snbkrbf859j0dp-ruby-3.4.7",
|
|
34
|
+
"default": true
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "devdoc",
|
|
38
|
+
"path": "/nix/store/iij6p6v02ffajhy821hz48q24a5rzc0i-ruby-3.4.7-devdoc"
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"store_path": "/nix/store/wxx9v5gnz8nm8rwzy6snbkrbf859j0dp-ruby-3.4.7"
|
|
42
|
+
},
|
|
43
|
+
"x86_64-darwin": {
|
|
44
|
+
"outputs": [
|
|
45
|
+
{
|
|
46
|
+
"name": "out",
|
|
47
|
+
"path": "/nix/store/8qdy0dzskl8n46ywhmirkmhlrvsshc76-ruby-3.4.7",
|
|
48
|
+
"default": true
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "devdoc",
|
|
52
|
+
"path": "/nix/store/9wlpxwwrxs3bw9aqyl80sr4kvsrpvxan-ruby-3.4.7-devdoc"
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"store_path": "/nix/store/8qdy0dzskl8n46ywhmirkmhlrvsshc76-ruby-3.4.7"
|
|
56
|
+
},
|
|
57
|
+
"x86_64-linux": {
|
|
58
|
+
"outputs": [
|
|
59
|
+
{
|
|
60
|
+
"name": "out",
|
|
61
|
+
"path": "/nix/store/lgf2l2wkr5845485qw254skgc0bdvbnc-ruby-3.4.7",
|
|
62
|
+
"default": true
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "devdoc",
|
|
66
|
+
"path": "/nix/store/shxiyxnzkw8wk1xm1v7l2n61w9wpxqkm-ruby-3.4.7-devdoc"
|
|
67
|
+
}
|
|
68
|
+
],
|
|
69
|
+
"store_path": "/nix/store/lgf2l2wkr5845485qw254skgc0bdvbnc-ruby-3.4.7"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module FactoryBotGuide
|
|
6
|
+
# Checks that time-related and random methods in FactoryBot definitions
|
|
7
|
+
# are wrapped in blocks for dynamic evaluation.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# factory :user do
|
|
12
|
+
# created_at Time.now
|
|
13
|
+
# token SecureRandom.hex
|
|
14
|
+
# expires_at 1.day.from_now
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# factory :user do
|
|
19
|
+
# created_at { Time.now }
|
|
20
|
+
# token { SecureRandom.hex }
|
|
21
|
+
# expires_at { 1.day.from_now }
|
|
22
|
+
# name "John" # Static values are OK
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class DynamicAttributesForTimeAndRandom < Base
|
|
26
|
+
MSG = "Use block syntax for attribute `%<attribute>s` because `%<method>s` " \
|
|
27
|
+
"is evaluated once at factory definition time. " \
|
|
28
|
+
"Wrap in block: `%<attribute>s { %<value>s }`"
|
|
29
|
+
|
|
30
|
+
TIME_CLASSES = %w[Time Date DateTime].freeze
|
|
31
|
+
RANDOM_CLASSES = %w[SecureRandom].freeze
|
|
32
|
+
|
|
33
|
+
# @!method factory_block?(node)
|
|
34
|
+
def_node_matcher :factory_block?, <<~PATTERN
|
|
35
|
+
(block
|
|
36
|
+
(send {nil? (const {nil? cbase} :FactoryBot)} :factory ...)
|
|
37
|
+
...)
|
|
38
|
+
PATTERN
|
|
39
|
+
|
|
40
|
+
# @!method attribute_assignment?(node)
|
|
41
|
+
def_node_matcher :attribute_assignment?, <<~PATTERN
|
|
42
|
+
(send nil? $_ $_value)
|
|
43
|
+
PATTERN
|
|
44
|
+
|
|
45
|
+
def on_block(node)
|
|
46
|
+
return unless factory_block?(node)
|
|
47
|
+
|
|
48
|
+
# Check all attribute assignments within the factory
|
|
49
|
+
node.each_descendant(:send) do |send_node|
|
|
50
|
+
check_attribute(send_node)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def check_attribute(node)
|
|
57
|
+
attribute_assignment?(node) do |attribute_name, value|
|
|
58
|
+
# Skip if value is already a block
|
|
59
|
+
next if value.block_type?
|
|
60
|
+
|
|
61
|
+
# Check if the value is a dangerous method call
|
|
62
|
+
next unless dangerous_method_call?(value)
|
|
63
|
+
|
|
64
|
+
add_offense(
|
|
65
|
+
node,
|
|
66
|
+
message: format(
|
|
67
|
+
MSG,
|
|
68
|
+
attribute: attribute_name,
|
|
69
|
+
method: method_description(value),
|
|
70
|
+
value: value.source
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def dangerous_method_call?(node)
|
|
77
|
+
# Only method calls are potentially dangerous
|
|
78
|
+
return false unless node.send_type?
|
|
79
|
+
|
|
80
|
+
# Time.now, Date.today, DateTime.now, etc.
|
|
81
|
+
return true if time_method?(node)
|
|
82
|
+
|
|
83
|
+
# SecureRandom.hex, SecureRandom.uuid, etc.
|
|
84
|
+
return true if random_method?(node)
|
|
85
|
+
|
|
86
|
+
# Any other method calls (e.g., 1.day.ago, Array.new, etc.)
|
|
87
|
+
# are evaluated at factory load time
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def time_method?(node)
|
|
92
|
+
return false unless node.receiver
|
|
93
|
+
|
|
94
|
+
receiver_name = if node.receiver.const_type?
|
|
95
|
+
node.receiver.const_name
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
TIME_CLASSES.include?(receiver_name)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def random_method?(node)
|
|
102
|
+
return false unless node.receiver
|
|
103
|
+
|
|
104
|
+
receiver_name = if node.receiver.const_type?
|
|
105
|
+
node.receiver.const_name
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
RANDOM_CLASSES.include?(receiver_name)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def method_description(node)
|
|
112
|
+
if node.receiver
|
|
113
|
+
"#{node.receiver.source}.#{node.method_name}"
|
|
114
|
+
else
|
|
115
|
+
node.method_name.to_s
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|