rubocop-gusto 10.10.0 → 11.1.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/CHANGELOG.md +20 -0
- data/README.md +18 -2
- data/config/default.yml +9 -113
- data/config/gusto_cops.yml +135 -0
- data/config/sidekiq.yml +7 -0
- data/lib/rubocop/cop/gusto/all.rb +7 -0
- data/lib/rubocop/cop/gusto/bootsnap_load_file.rb +11 -1
- data/lib/rubocop/cop/gusto/datadog_constant.rb +10 -0
- data/lib/rubocop/cop/gusto/described_class_constant_reference.rb +139 -0
- data/lib/rubocop/cop/gusto/discouraged_gem.rb +8 -5
- data/lib/rubocop/cop/gusto/execute_migration.rb +14 -0
- data/lib/rubocop/cop/gusto/factory_classes_or_modules.rb +18 -0
- data/lib/rubocop/cop/gusto/paperclip_or_attachable.rb +9 -0
- data/lib/rubocop/cop/gusto/polymorphic_type_validation.rb +18 -3
- data/lib/rubocop/cop/gusto/sidekiq_params.rb +12 -0
- data/lib/rubocop/cop/gusto/unreferenced_let.rb +284 -0
- data/lib/rubocop/cop/sidekiq/perform_async_stub.rb +90 -0
- data/lib/rubocop/gusto/init.rb +20 -7
- data/lib/rubocop/gusto/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5814e91aac6e402768ff64bd32f0c196838e20d90cbd927286238f68bb9d25b9
|
|
4
|
+
data.tar.gz: d4e988b9973101b7ea9a683900ae7489cbfe128b681d4137c7317f06a45cc247
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82385ed24fdf344897513f4f034e1f8ae662a5d41b62ccc8dc2c14e735d479deb31acf4c28047fbad1f59d828485798a4677148ca203989f15489d768cb88995
|
|
7
|
+
data.tar.gz: 403192785be424b372d709d2c7d9bac18491603c2a9449a2edeb5b35c981652e7ee87c307265a381b51d5285134fda5a0178adb04087bb6d2db7c10b51d41ae9
|
data/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
- Remove redundant `Rails: Enabled: true` from `config/rails.yml` (already set by rubocop-rails' own defaults)
|
|
4
4
|
- Enable `Rails/DefaultScope` cop (disabled by default in rubocop-rails)
|
|
5
5
|
|
|
6
|
+
## [11.1.0](https://github.com/Gusto/rubocop-gusto/compare/v11.0.0...v11.1.0) (2026-06-17)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add `Sidekiq/PerformAsyncStub` in a separate config ([#131](https://github.com/Gusto/rubocop-gusto/issues/131)) ([22670d4](https://github.com/Gusto/rubocop-gusto/commit/22670d4a34487417bdb50b8be25e82fb0cac3614))
|
|
12
|
+
* add cops-only entrypoints for selective adoption ([#132](https://github.com/Gusto/rubocop-gusto/issues/132)) ([f0911c2](https://github.com/Gusto/rubocop-gusto/commit/f0911c2f9fc6d272bc8ccb5af830daf659c6ba62))
|
|
13
|
+
|
|
14
|
+
## [11.0.0](https://github.com/Gusto/rubocop-gusto/compare/v10.10.0...v11.0.0) (2026-06-08)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### ⚠ BREAKING CHANGES
|
|
18
|
+
|
|
19
|
+
* rubocop-gusto now requires Ruby >= 3.4.
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* add Gusto/DescribedClassConstantReference cop ([#127](https://github.com/Gusto/rubocop-gusto/issues/127)) ([f7cf636](https://github.com/Gusto/rubocop-gusto/commit/f7cf6362f3f522f322251da0fdf5c8affa35bc0f))
|
|
24
|
+
* add Gusto/UnreferencedLet cop (requires Ruby >= 3.4) ([#128](https://github.com/Gusto/rubocop-gusto/issues/128)) ([99a2df7](https://github.com/Gusto/rubocop-gusto/commit/99a2df761b52ce11f4f6bf65a5c8e414153efa53))
|
|
25
|
+
|
|
6
26
|
## [10.10.0](https://github.com/Gusto/rubocop-gusto/compare/v10.9.4...v10.10.0) (2026-06-01)
|
|
7
27
|
|
|
8
28
|
|
data/README.md
CHANGED
|
@@ -28,15 +28,31 @@ $ bundle
|
|
|
28
28
|
bundle exec rubocop-gusto init
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
This adds `rubocop-gusto` to your `.rubocop.yml` `plugins:` list and includes any relevant configuration (e.g. Rails-specific rules when Rails is detected).
|
|
31
|
+
This adds `rubocop-gusto` to your `.rubocop.yml` `plugins:` list and includes any relevant configuration (e.g. Rails-specific rules when Rails is detected, Sidekiq-specific rules when the Sidekiq gem is present).
|
|
32
32
|
|
|
33
33
|
If this is an existing project, it is recommended to run the autocorrector (`bundle exec rubocop -a`) and then to regenerate the `.rubocop_todo.yml` (`bundle exec rubocop --auto-gen-config`), so issues can be dealt with piecemeal.
|
|
34
34
|
|
|
35
|
+
#### Sidekiq configuration
|
|
36
|
+
|
|
37
|
+
Sidekiq-specific cops live in `config/sidekiq.yml` and are **not** included in `config/default.yml`, so projects without Sidekiq are not linted for those patterns. Running `bundle exec rubocop-gusto init` adds `config/sidekiq.yml` to your `inherit_gem` list automatically when Sidekiq is listed in your `Gemfile` or `Gemfile.lock`.
|
|
38
|
+
|
|
39
|
+
For an existing project that already uses Sidekiq, add the Sidekiq config to your `.rubocop.yml`:
|
|
40
|
+
|
|
41
|
+
```yaml
|
|
42
|
+
inherit_gem:
|
|
43
|
+
rubocop-gusto:
|
|
44
|
+
- config/default.yml
|
|
45
|
+
- config/sidekiq.yml
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
If your project also uses Rails, include `config/rails.yml` as well (order does not matter). Re-run `bundle exec rubocop-gusto init` to merge this in automatically.
|
|
49
|
+
|
|
35
50
|
### Available cops
|
|
36
51
|
|
|
37
52
|
Custom cops live under the following namespaces:
|
|
38
53
|
|
|
39
54
|
- `Gusto/` — general Gusto-specific cops (see [`lib/rubocop/cop/gusto/`](lib/rubocop/cop/gusto/))
|
|
55
|
+
- `Sidekiq/` — cops scoped to Sidekiq patterns (see [`lib/rubocop/cop/sidekiq/`](lib/rubocop/cop/sidekiq/)); configured in [`config/sidekiq.yml`](config/sidekiq.yml)
|
|
40
56
|
- `Rack/` — cops scoped to Rack middleware patterns (see [`lib/rubocop/cop/rack/`](lib/rubocop/cop/rack/))
|
|
41
57
|
|
|
42
58
|
## Publishing New Versions
|
|
@@ -66,7 +82,7 @@ PR titles must use [Conventional Commits](https://www.conventionalcommits.org/)
|
|
|
66
82
|
|
|
67
83
|
### Adding a new cop
|
|
68
84
|
|
|
69
|
-
1. Create `lib/rubocop/cop/gusto/<cop_name>.rb
|
|
85
|
+
1. Create `lib/rubocop/cop/gusto/<cop_name>.rb`, see [how to create a new cop](https://docs.rubocop.org/rubocop/latest/development.html#create-a-new-cop) and [how to choose a name](https://docs.rubocop.org/rubocop-rspec/development.html#choose-a-name)
|
|
70
86
|
2. Add an entry to `config/default.yml`, then sort it:
|
|
71
87
|
```sh
|
|
72
88
|
bundle exec rubocop-gusto sort config/default.yml
|
data/config/default.yml
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
inherit_from: gusto_cops.yml
|
|
2
|
+
|
|
1
3
|
plugins:
|
|
2
4
|
- rubocop-rspec
|
|
3
5
|
- rubocop-performance
|
|
@@ -30,119 +32,6 @@ Gemspec/RequiredRubyVersion:
|
|
|
30
32
|
# We manage our Ruby version in our version file and import that to our Gemfile.
|
|
31
33
|
Enabled: false
|
|
32
34
|
|
|
33
|
-
Gusto/BootsnapLoadFile:
|
|
34
|
-
Description: 'Do not use Bootsnap to load files. Use `require` instead.'
|
|
35
|
-
|
|
36
|
-
Gusto/DatadogConstant:
|
|
37
|
-
Exclude:
|
|
38
|
-
# calling DataDog directly only allowed in initializers, its library, and tests
|
|
39
|
-
- 'config/application.rb'
|
|
40
|
-
- 'config/initializers/datadog.rb'
|
|
41
|
-
- 'lib/datadog/**/*'
|
|
42
|
-
- '**/spec/**/*'
|
|
43
|
-
Description: 'Do not call Datadog directly, use an appropriate wrapper library.'
|
|
44
|
-
|
|
45
|
-
Gusto/DiscouragedGem:
|
|
46
|
-
Description: 'Flags installation of discouraged gems in Gemfiles and gemspecs.'
|
|
47
|
-
Enabled: false
|
|
48
|
-
Gems: {}
|
|
49
|
-
|
|
50
|
-
Gusto/ExecuteMigration:
|
|
51
|
-
Description: "Don't use `execute` in migrations. Use a backfill rake task instead."
|
|
52
|
-
Include:
|
|
53
|
-
- 'db/migrate/*.rb'
|
|
54
|
-
|
|
55
|
-
Gusto/FactoryClassesOrModules:
|
|
56
|
-
Description: 'Do not define modules or classes in factory directories - they break reloading.'
|
|
57
|
-
Include:
|
|
58
|
-
- 'spec/**/factories/*.rb'
|
|
59
|
-
|
|
60
|
-
Gusto/MinByMaxBy:
|
|
61
|
-
Description: 'Checks for the use of `min` or `max` with a proc. Corrects to `min_by` or `max_by`.'
|
|
62
|
-
Safe: false
|
|
63
|
-
Severity: error
|
|
64
|
-
|
|
65
|
-
Gusto/NoMetaprogramming:
|
|
66
|
-
Description: 'Avoid using metaprogramming techniques like define_method and instance_eval which make code harder to understand and debug.'
|
|
67
|
-
|
|
68
|
-
Gusto/NoRescueErrorMessageChecking:
|
|
69
|
-
Description: 'Checks for the presence of error message checking within rescue blocks.'
|
|
70
|
-
|
|
71
|
-
Gusto/NoSend:
|
|
72
|
-
Description: 'Do not call a private method via `__send__`.'
|
|
73
|
-
Exclude:
|
|
74
|
-
- '**/spec/**/*'
|
|
75
|
-
|
|
76
|
-
Gusto/PaperclipOrAttachable:
|
|
77
|
-
Description: 'No more new paperclip or Attachable are allowed. Use ActiveStorage instead.'
|
|
78
|
-
|
|
79
|
-
Gusto/PerformClassMethod:
|
|
80
|
-
Description: 'Prevents accidental definition of `perform` class methods (should be instance methods instead).'
|
|
81
|
-
# List of modules that include Sidekiq::Worker.
|
|
82
|
-
# Add your other base modules here if they include Sidekiq::Worker too.
|
|
83
|
-
WorkerModules:
|
|
84
|
-
- Sidekiq::Worker
|
|
85
|
-
|
|
86
|
-
Gusto/PolymorphicTypeValidation:
|
|
87
|
-
Description: 'Ensures that polymorphic relations include a type validation, which is necessary for generating Sorbet types.'
|
|
88
|
-
Include:
|
|
89
|
-
- '**/models/**/*.rb'
|
|
90
|
-
|
|
91
|
-
Gusto/PreferProcessLastStatus:
|
|
92
|
-
Description: 'Prefer using `Process.last_status` instead of the global variables: `$?` and `$CHILD_STATUS`.'
|
|
93
|
-
|
|
94
|
-
Gusto/RablExtends:
|
|
95
|
-
Description: 'Disallows the use of `extends` in Rabl templates due to poor caching performance. Inline the templating to generate your JSON instead.'
|
|
96
|
-
Include:
|
|
97
|
-
- '**/*.json.rabl'
|
|
98
|
-
|
|
99
|
-
Gusto/RailsEnv:
|
|
100
|
-
Description: 'Use Feature Flags or config instead of `Rails.env`.'
|
|
101
|
-
|
|
102
|
-
Gusto/RakeConstants:
|
|
103
|
-
Description: 'Do not define a constant in rake file, because they are sometimes `load`ed, instead of `require`d which can lead to warnings about redefining constants.'
|
|
104
|
-
Include:
|
|
105
|
-
- '**/*.rake'
|
|
106
|
-
- 'Rakefile'
|
|
107
|
-
|
|
108
|
-
Gusto/RegexpBypass:
|
|
109
|
-
Description: 'Ensures regular expressions use \A and \z anchors instead of ^ and $ for security validation.'
|
|
110
|
-
Exclude:
|
|
111
|
-
- '**/spec/**/*'
|
|
112
|
-
Safe: false
|
|
113
|
-
|
|
114
|
-
Gusto/RspecDateTimeMock:
|
|
115
|
-
Description: "Don't mock Date/Time/DateTime directly. Use Rails Testing Time Helpers (eg `freeze_time` and `travel_to`) instead."
|
|
116
|
-
Include:
|
|
117
|
-
- '**/spec/**/*'
|
|
118
|
-
Safe: false
|
|
119
|
-
|
|
120
|
-
Gusto/SidekiqParams:
|
|
121
|
-
Description: 'Sidekiq perform methods cannot take keyword arguments.'
|
|
122
|
-
|
|
123
|
-
Gusto/ToplevelConstants:
|
|
124
|
-
Description: 'Prevents top-level constants from being defined outside of initializers.'
|
|
125
|
-
Include:
|
|
126
|
-
- '**/*.rb'
|
|
127
|
-
Exclude:
|
|
128
|
-
- '**/bin/*'
|
|
129
|
-
- 'bin/*'
|
|
130
|
-
- 'config/**/*'
|
|
131
|
-
- 'lib/*.rb'
|
|
132
|
-
- 'packs/**/{db/seeds,lib,config/initializers}/**/*'
|
|
133
|
-
- 'script/**/*'
|
|
134
|
-
- 'spec/rails_helper.rb'
|
|
135
|
-
- '**/spec/support/**/*'
|
|
136
|
-
- '**/*/spec_helper.rb'
|
|
137
|
-
- 'spec/support/**/*.rb'
|
|
138
|
-
|
|
139
|
-
Gusto/UsePaintNotColorize:
|
|
140
|
-
Description: 'Use Paint instead of colorize for terminal colors.'
|
|
141
|
-
SafeAutoCorrect: false
|
|
142
|
-
|
|
143
|
-
Gusto/VcrRecordings:
|
|
144
|
-
Description: 'VCR should be set to not record in tests. Use vcr: {record: :none}.'
|
|
145
|
-
|
|
146
35
|
Layout/BlockAlignment:
|
|
147
36
|
EnforcedStyleAlignWith: start_of_block
|
|
148
37
|
|
|
@@ -373,6 +262,13 @@ Rack/LowercaseHeaderKeys:
|
|
|
373
262
|
Rake/ClassDefinitionInTask:
|
|
374
263
|
Enabled: false
|
|
375
264
|
|
|
265
|
+
Sidekiq/PerformAsyncStub:
|
|
266
|
+
Description: 'Prefer checking enqueued jobs over stubbing `perform_async`.'
|
|
267
|
+
Enabled: false
|
|
268
|
+
SafeAutoCorrect: false # Autocorrect appends `.and_call_original`, which runs real `perform_async` during the example
|
|
269
|
+
Include:
|
|
270
|
+
- '**/spec/**/*_spec.rb'
|
|
271
|
+
|
|
376
272
|
Sorbet/ForbidTUnsafe:
|
|
377
273
|
# T.unsafe completely disables typechecking. Prefer T.cast, shims, or
|
|
378
274
|
# reorganizing the code over silently turning off the type checker.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Gusto cop defaults only — no built-in cop overrides.
|
|
2
|
+
# Use this with `inherit_gem` when you want Gusto cop scoping (Include/Exclude/Severity)
|
|
3
|
+
# without inheriting rubocop-gusto's full configuration layer.
|
|
4
|
+
#
|
|
5
|
+
# In your .rubocop.yml:
|
|
6
|
+
# require:
|
|
7
|
+
# - rubocop/cop/gusto/all
|
|
8
|
+
# inherit_gem:
|
|
9
|
+
# rubocop-gusto:
|
|
10
|
+
# - config/gusto_cops.yml
|
|
11
|
+
|
|
12
|
+
Gusto/BootsnapLoadFile:
|
|
13
|
+
Description: 'Do not use Bootsnap to load files. Use `require` instead.'
|
|
14
|
+
|
|
15
|
+
Gusto/DatadogConstant:
|
|
16
|
+
Exclude:
|
|
17
|
+
# calling DataDog directly only allowed in initializers, its library, and tests
|
|
18
|
+
- 'config/application.rb'
|
|
19
|
+
- 'config/initializers/datadog.rb'
|
|
20
|
+
- 'lib/datadog/**/*'
|
|
21
|
+
- '**/spec/**/*'
|
|
22
|
+
Description: 'Do not call Datadog directly, use an appropriate wrapper library.'
|
|
23
|
+
|
|
24
|
+
Gusto/DescribedClassConstantReference:
|
|
25
|
+
Description: 'Flags constants scoped through `described_class` (e.g. `described_class::Foo`), which Sorbet cannot resolve statically.'
|
|
26
|
+
Enabled: true
|
|
27
|
+
SafeAutoCorrect: false
|
|
28
|
+
Include:
|
|
29
|
+
- '**/spec/**/*'
|
|
30
|
+
|
|
31
|
+
Gusto/DiscouragedGem:
|
|
32
|
+
Description: 'Flags installation of discouraged gems in Gemfiles and gemspecs.'
|
|
33
|
+
Enabled: false
|
|
34
|
+
Gems: {}
|
|
35
|
+
|
|
36
|
+
Gusto/ExecuteMigration:
|
|
37
|
+
Description: "Don't use `execute` in migrations. Use a backfill rake task instead."
|
|
38
|
+
Include:
|
|
39
|
+
- 'db/migrate/*.rb'
|
|
40
|
+
|
|
41
|
+
Gusto/FactoryClassesOrModules:
|
|
42
|
+
Description: 'Do not define modules or classes in factory directories - they break reloading.'
|
|
43
|
+
Include:
|
|
44
|
+
- 'spec/**/factories/*.rb'
|
|
45
|
+
|
|
46
|
+
Gusto/MinByMaxBy:
|
|
47
|
+
Description: 'Checks for the use of `min` or `max` with a proc. Corrects to `min_by` or `max_by`.'
|
|
48
|
+
Safe: false
|
|
49
|
+
Severity: error
|
|
50
|
+
|
|
51
|
+
Gusto/NoMetaprogramming:
|
|
52
|
+
Description: 'Avoid using metaprogramming techniques like define_method and instance_eval which make code harder to understand and debug.'
|
|
53
|
+
|
|
54
|
+
Gusto/NoRescueErrorMessageChecking:
|
|
55
|
+
Description: 'Checks for the presence of error message checking within rescue blocks.'
|
|
56
|
+
|
|
57
|
+
Gusto/NoSend:
|
|
58
|
+
Description: 'Do not call a private method via `__send__`.'
|
|
59
|
+
Exclude:
|
|
60
|
+
- '**/spec/**/*'
|
|
61
|
+
|
|
62
|
+
Gusto/PaperclipOrAttachable:
|
|
63
|
+
Description: 'No more new paperclip or Attachable are allowed. Use ActiveStorage instead.'
|
|
64
|
+
|
|
65
|
+
Gusto/PerformClassMethod:
|
|
66
|
+
Description: 'Prevents accidental definition of `perform` class methods (should be instance methods instead).'
|
|
67
|
+
WorkerModules:
|
|
68
|
+
- Sidekiq::Worker
|
|
69
|
+
|
|
70
|
+
Gusto/PolymorphicTypeValidation:
|
|
71
|
+
Description: 'Ensures that polymorphic relations include a type validation, which is necessary for generating Sorbet types.'
|
|
72
|
+
Include:
|
|
73
|
+
- '**/models/**/*.rb'
|
|
74
|
+
|
|
75
|
+
Gusto/PreferProcessLastStatus:
|
|
76
|
+
Description: 'Prefer using `Process.last_status` instead of the global variables: `$?` and `$CHILD_STATUS`.'
|
|
77
|
+
|
|
78
|
+
Gusto/RablExtends:
|
|
79
|
+
Description: 'Disallows the use of `extends` in Rabl templates due to poor caching performance. Inline the templating to generate your JSON instead.'
|
|
80
|
+
Include:
|
|
81
|
+
- '**/*.json.rabl'
|
|
82
|
+
|
|
83
|
+
Gusto/RailsEnv:
|
|
84
|
+
Description: 'Use Feature Flags or config instead of `Rails.env`.'
|
|
85
|
+
|
|
86
|
+
Gusto/RakeConstants:
|
|
87
|
+
Description: 'Do not define a constant in rake file, because they are sometimes `load`ed, instead of `require`d which can lead to warnings about redefining constants.'
|
|
88
|
+
Include:
|
|
89
|
+
- '**/*.rake'
|
|
90
|
+
- 'Rakefile'
|
|
91
|
+
|
|
92
|
+
Gusto/RegexpBypass:
|
|
93
|
+
Description: 'Ensures regular expressions use \A and \z anchors instead of ^ and $ for security validation.'
|
|
94
|
+
Exclude:
|
|
95
|
+
- '**/spec/**/*'
|
|
96
|
+
Safe: false
|
|
97
|
+
|
|
98
|
+
Gusto/RspecDateTimeMock:
|
|
99
|
+
Description: "Don't mock Date/Time/DateTime directly. Use Rails Testing Time Helpers (eg `freeze_time` and `travel_to`) instead."
|
|
100
|
+
Include:
|
|
101
|
+
- '**/spec/**/*'
|
|
102
|
+
Safe: false
|
|
103
|
+
|
|
104
|
+
Gusto/SidekiqParams:
|
|
105
|
+
Description: 'Sidekiq perform methods cannot take keyword arguments.'
|
|
106
|
+
|
|
107
|
+
Gusto/ToplevelConstants:
|
|
108
|
+
Description: 'Prevents top-level constants from being defined outside of initializers.'
|
|
109
|
+
Include:
|
|
110
|
+
- '**/*.rb'
|
|
111
|
+
Exclude:
|
|
112
|
+
- '**/bin/*'
|
|
113
|
+
- 'bin/*'
|
|
114
|
+
- 'config/**/*'
|
|
115
|
+
- 'lib/*.rb'
|
|
116
|
+
- 'packs/**/{db/seeds,lib,config/initializers}/**/*'
|
|
117
|
+
- 'script/**/*'
|
|
118
|
+
- 'spec/rails_helper.rb'
|
|
119
|
+
- '**/spec/support/**/*'
|
|
120
|
+
- '**/*/spec_helper.rb'
|
|
121
|
+
- 'spec/support/**/*.rb'
|
|
122
|
+
|
|
123
|
+
Gusto/UnreferencedLet:
|
|
124
|
+
Description: 'Removes a lazy let whose name is never referenced (its block never runs).'
|
|
125
|
+
Enabled: true
|
|
126
|
+
Include:
|
|
127
|
+
- '**/spec/**/*_spec.rb'
|
|
128
|
+
SafeAutoCorrect: false
|
|
129
|
+
|
|
130
|
+
Gusto/UsePaintNotColorize:
|
|
131
|
+
Description: 'Use Paint instead of colorize for terminal colors.'
|
|
132
|
+
SafeAutoCorrect: false
|
|
133
|
+
|
|
134
|
+
Gusto/VcrRecordings:
|
|
135
|
+
Description: 'VCR should be set to not record in tests. Use vcr: {record: :none}.'
|
data/config/sidekiq.yml
ADDED
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module Gusto
|
|
6
|
-
#
|
|
6
|
+
# Prefer `YAML.load_file`/`JSON.load_file` over reading a file and then
|
|
7
|
+
# parsing its contents. Bootsnap caches the parsed result of `load_file`,
|
|
8
|
+
# so this improves load time.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# YAML.load(File.read("config.yml"))
|
|
13
|
+
# File.open("config.yml") { |f| YAML.load(f) }
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# YAML.load_file("config.yml")
|
|
7
17
|
class BootsnapLoadFile < Base
|
|
8
18
|
PROHIBITED_CONSTANTS = Set[:YAML, :JSON].freeze
|
|
9
19
|
RESTRICT_ON_SEND = %i(load).freeze
|
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module Gusto
|
|
6
|
+
# Disallow referencing the `Datadog` constant directly. Calls should go
|
|
7
|
+
# through an approved wrapper library so instrumentation stays consistent
|
|
8
|
+
# and swappable.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# Datadog::Tracing.active_trace
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# Observability.active_trace
|
|
6
16
|
class DatadogConstant < Base
|
|
7
17
|
MSG = "Do not call Datadog directly, use an appropriate wrapper library."
|
|
8
18
|
NAMESPACE = "Datadog"
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Gusto
|
|
6
|
+
# Flags constants that are scoped through `described_class`, e.g.
|
|
7
|
+
# `described_class::Worker`.
|
|
8
|
+
#
|
|
9
|
+
# `described_class` is an RSpec helper method resolved at runtime, so
|
|
10
|
+
# Sorbet's static analysis treats `described_class::Worker` as a dynamic
|
|
11
|
+
# constant reference and cannot resolve it (`Dynamic constant references
|
|
12
|
+
# are unsupported`, https://srb.help/5001). Reference the constant by its
|
|
13
|
+
# fully-qualified name instead. A bare `described_class` (with no `::`
|
|
14
|
+
# constant lookup) is an ordinary method call and is left alone.
|
|
15
|
+
#
|
|
16
|
+
# Autocorrection replaces `described_class` with the constant that the
|
|
17
|
+
# enclosing example group describes. It is marked unsafe
|
|
18
|
+
# (`SafeAutoCorrect: false`) because the rewrite relies on the described
|
|
19
|
+
# constant being a statically-written name; review the result before
|
|
20
|
+
# committing. In particular, a constant defined on an *ancestor* of the
|
|
21
|
+
# described class is qualified against the described class itself, which
|
|
22
|
+
# is correct at runtime but which Sorbet cannot resolve through the
|
|
23
|
+
# inheritance chain -- re-point those to the defining ancestor by hand.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# # bad
|
|
27
|
+
# RSpec.describe Payments::Processor do
|
|
28
|
+
# describe described_class::Worker do
|
|
29
|
+
# end
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# # good
|
|
33
|
+
# RSpec.describe Payments::Processor do
|
|
34
|
+
# describe Payments::Processor::Worker do
|
|
35
|
+
# end
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# # good - `RSpec.describe self` resolves to the enclosing namespace
|
|
39
|
+
# module Payments
|
|
40
|
+
# RSpec.describe self do
|
|
41
|
+
# it { expect(Payments::TIMEOUT).to eq(5) }
|
|
42
|
+
# end
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
# # good - a bare `described_class` is not a constant reference
|
|
46
|
+
# RSpec.describe Payments::Processor do
|
|
47
|
+
# subject { described_class.new }
|
|
48
|
+
# end
|
|
49
|
+
class DescribedClassConstantReference < Base
|
|
50
|
+
extend AutoCorrector
|
|
51
|
+
|
|
52
|
+
MSG = "Use the fully-qualified constant name instead of scoping it through " \
|
|
53
|
+
"`described_class`, which Sorbet cannot resolve statically."
|
|
54
|
+
|
|
55
|
+
# A constant whose scope is a no-receiver `described_class`, e.g.
|
|
56
|
+
# `described_class::Worker`.
|
|
57
|
+
# @!method const_scoped_on_described_class?(node)
|
|
58
|
+
def_node_matcher :const_scoped_on_described_class?, <<~PATTERN
|
|
59
|
+
(const (send nil? :described_class) _)
|
|
60
|
+
PATTERN
|
|
61
|
+
|
|
62
|
+
# An example group, capturing its first argument: a constant
|
|
63
|
+
# (`RSpec.describe Foo do`, `context Foo do`), `self`
|
|
64
|
+
# (`RSpec.describe self do`), and so on.
|
|
65
|
+
# @!method example_group_described_argument(node)
|
|
66
|
+
def_node_matcher :example_group_described_argument, <<~PATTERN
|
|
67
|
+
(block
|
|
68
|
+
(send {(const nil? :RSpec) nil?}
|
|
69
|
+
{:describe :xdescribe :fdescribe :context :xcontext :fcontext :feature :example_group}
|
|
70
|
+
$_ ...)
|
|
71
|
+
...)
|
|
72
|
+
PATTERN
|
|
73
|
+
|
|
74
|
+
# Whether a node routes through a no-receiver `described_class`.
|
|
75
|
+
# @!method scoped_through_described_class?(node)
|
|
76
|
+
def_node_search :scoped_through_described_class?, <<~PATTERN
|
|
77
|
+
(send nil? :described_class)
|
|
78
|
+
PATTERN
|
|
79
|
+
|
|
80
|
+
def on_const(node)
|
|
81
|
+
return unless const_scoped_on_described_class?(node)
|
|
82
|
+
|
|
83
|
+
scope = node.children[0]
|
|
84
|
+
add_offense(scope) do |corrector|
|
|
85
|
+
replacement = described_class_replacement(node)
|
|
86
|
+
corrector.replace(scope, replacement) if replacement
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
# The fully-qualified name (as a String) that `described_class` resolves
|
|
93
|
+
# to lexically, from the nearest enclosing example group, or nil if it
|
|
94
|
+
# cannot be determined statically.
|
|
95
|
+
#
|
|
96
|
+
# - `describe SomeClass` resolves to that constant's written name.
|
|
97
|
+
# - `describe self` resolves to the enclosing module/class namespace.
|
|
98
|
+
# - `describe described_class::X` qualifies the describe argument itself
|
|
99
|
+
# against the outer group; a reference in such a group's *body* resolves
|
|
100
|
+
# at runtime to the scoped (statically unknown) class, so we decline to
|
|
101
|
+
# autocorrect it. Once the enclosing `described_class::X` is rewritten,
|
|
102
|
+
# a later pass resolves the body reference correctly.
|
|
103
|
+
# - Any other describe argument (e.g. a string) is skipped, and the
|
|
104
|
+
# search continues at the next enclosing example group.
|
|
105
|
+
def described_class_replacement(node)
|
|
106
|
+
node.each_ancestor(:block) do |block_node|
|
|
107
|
+
described_argument = example_group_described_argument(block_node)
|
|
108
|
+
next if described_argument.nil?
|
|
109
|
+
|
|
110
|
+
if described_argument.self_type?
|
|
111
|
+
namespace = enclosing_namespace(block_node)
|
|
112
|
+
return namespace if namespace
|
|
113
|
+
elsif described_argument.const_type?
|
|
114
|
+
return described_argument.source unless scoped_through_described_class?(described_argument)
|
|
115
|
+
return nil unless reference_within_described_constant?(described_argument, node)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The fully-qualified name of the module/class lexically enclosing the
|
|
122
|
+
# example group, which is what `self` refers to in `RSpec.describe self`.
|
|
123
|
+
def enclosing_namespace(block_node)
|
|
124
|
+
names = block_node.each_ancestor(:class, :module).map { |mod| mod.children.first.source }
|
|
125
|
+
return if names.empty?
|
|
126
|
+
|
|
127
|
+
names.reverse.join("::")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Whether the offending constant is the described constant itself (the
|
|
131
|
+
# describe argument) rather than a reference inside the group's body.
|
|
132
|
+
def reference_within_described_constant?(described_constant, node)
|
|
133
|
+
node.equal?(described_constant) ||
|
|
134
|
+
described_constant.each_descendant(:const).any? { |const_node| const_node.equal?(node) }
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module Gusto
|
|
6
|
-
#
|
|
6
|
+
# Flag installation of discouraged gems (e.g. timecop) in Gemfiles and
|
|
7
|
+
# gemspecs. The discouraged gems an advice about alternatives are configured under
|
|
8
|
+
# `Gems:`; intended to be enabled in Rails projects via config/rails.yml.
|
|
7
9
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
10
|
+
# @example Gems: { timecop: "Use Rails' time helpers instead of Timecop." }
|
|
11
|
+
# # bad
|
|
12
|
+
# gem "timecop"
|
|
11
13
|
#
|
|
12
|
-
#
|
|
14
|
+
# # good
|
|
15
|
+
# # Use Rails' time helpers (freeze_time, travel_to) instead.
|
|
13
16
|
class DiscouragedGem < Base
|
|
14
17
|
MSG = "Avoid using the '%{gem}' gem. %{advice}"
|
|
15
18
|
|
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module Gusto
|
|
6
|
+
# Disallow `execute` (raw SQL) in migrations. Run raw SQL from a backfill
|
|
7
|
+
# rake task, or pass SQL options to `add_column`/`change_column` instead,
|
|
8
|
+
# so migrations stay reversible and schema-focused.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# def up
|
|
13
|
+
# execute("UPDATE users SET active = true")
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# # good
|
|
17
|
+
# def up
|
|
18
|
+
# add_column :users, :active, :boolean, default: true
|
|
19
|
+
# end
|
|
6
20
|
class ExecuteMigration < Base
|
|
7
21
|
MSG = "Do not use `execute` to run raw SQL in a migration. Run the query from a backfill rake task or pass the SQL options to the `add_column`/`change_column` method."
|
|
8
22
|
RESTRICT_ON_SEND = [:execute].freeze
|
|
@@ -3,6 +3,24 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module Gusto
|
|
6
|
+
# Disallow defining classes or modules in factory directories. They break
|
|
7
|
+
# Rails autoloading/reloading; define shared helpers outside the factories.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# # bad
|
|
11
|
+
# # spec/factories/users.rb
|
|
12
|
+
# class UserHelper
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# FactoryBot.define do
|
|
16
|
+
# factory :user
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# # good
|
|
20
|
+
# # spec/factories/users.rb
|
|
21
|
+
# FactoryBot.define do
|
|
22
|
+
# factory :user
|
|
23
|
+
# end
|
|
6
24
|
class FactoryClassesOrModules < Base
|
|
7
25
|
MSG = "Do not define modules or classes in factory directories - they break reloading"
|
|
8
26
|
|
|
@@ -4,6 +4,15 @@
|
|
|
4
4
|
module RuboCop
|
|
5
5
|
module Cop
|
|
6
6
|
module Gusto
|
|
7
|
+
# Disallow new Paperclip / Attachable attachments. New attachments should
|
|
8
|
+
# use ActiveStorage instead.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# has_attached_file :avatar
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# has_one_attached :avatar
|
|
7
16
|
class PaperclipOrAttachable < Base
|
|
8
17
|
MSG = "No more new paperclip or Attachable are allowed. New attachments should use ActiveStorage instead"
|
|
9
18
|
RESTRICT_ON_SEND = %i(has_attached_file has_pdf_attachment has_attachment).freeze
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# This cop enforces that polymorphic relations have a corresponding validation
|
|
4
|
-
# for their type field with an inclusion validation. This is required in order for Tapioca
|
|
5
|
-
# to generate correct Sorbet types
|
|
6
3
|
module RuboCop
|
|
7
4
|
module Cop
|
|
8
5
|
module Gusto
|
|
6
|
+
# Require polymorphic relations to validate their `*_type` field with an
|
|
7
|
+
# inclusion validation (or `polymorphic_methods_for`). This is needed for
|
|
8
|
+
# Tapioca to generate correct Sorbet types.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# belongs_to :subscription_detail, polymorphic: true
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# VALID_TYPES = T.let([Foo.polymorphic_name, Bar.polymorphic_name].freeze, T::Array[String])
|
|
16
|
+
# belongs_to :subscription_detail, polymorphic: true
|
|
17
|
+
# validates :subscription_detail_type, presence: true, inclusion: { in: VALID_TYPES }
|
|
18
|
+
#
|
|
19
|
+
# # also good
|
|
20
|
+
# include PolymorphicCallable
|
|
21
|
+
# VALID_TYPES = T.let([Foo.polymorphic_name, Bar.polymorphic_name].freeze, T::Array[String])
|
|
22
|
+
# belongs_to :subscription_detail, polymorphic: true
|
|
23
|
+
# polymorphic_methods_for :subscription_detail, VALID_TYPES
|
|
9
24
|
class PolymorphicTypeValidation < Base
|
|
10
25
|
RESTRICT_ON_SEND = %i(belongs_to validates polymorphic_methods_for).freeze
|
|
11
26
|
|
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
module RuboCop
|
|
4
4
|
module Cop
|
|
5
5
|
module Gusto
|
|
6
|
+
# Disallow keyword arguments on Sidekiq `perform` methods. Sidekiq
|
|
7
|
+
# serializes job arguments as JSON and replays them positionally, so
|
|
8
|
+
# keyword arguments are not preserved.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# # bad
|
|
12
|
+
# def perform(user_id:, force: false)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# # good
|
|
16
|
+
# def perform(user_id, force = false)
|
|
17
|
+
# end
|
|
6
18
|
class SidekiqParams < Base
|
|
7
19
|
MSG = "Sidekiq perform methods cannot take keyword arguments"
|
|
8
20
|
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "rubocop-rspec"
|
|
5
|
+
|
|
6
|
+
module RuboCop
|
|
7
|
+
module Cop
|
|
8
|
+
module Gusto
|
|
9
|
+
# Flags lazy `let` declarations whose name is never referenced. A lazy `let(:name) { ... }`
|
|
10
|
+
# is only evaluated when `name` is called, so an unreferenced one is dead code -- its block
|
|
11
|
+
# never runs -- and is deleted.
|
|
12
|
+
#
|
|
13
|
+
# Eager `let!` is intentionally out of scope: it runs its block before every example for its
|
|
14
|
+
# side effect even when unreferenced, so it cannot simply be deleted. Only plain `let` is
|
|
15
|
+
# handled here.
|
|
16
|
+
#
|
|
17
|
+
# Detection is file-scoped: a `let` referenced only from another file (through a shared
|
|
18
|
+
# example or an included test harness) cannot be seen, so the cop stays conservative and
|
|
19
|
+
# prefers false negatives over false positives:
|
|
20
|
+
# - a name defined more than once in the file by `let`/`let!`/`subject` (an override /
|
|
21
|
+
# `super` chain, including a `subject` that overrides a `let` of the same name) is never
|
|
22
|
+
# flagged;
|
|
23
|
+
# - a `let` declared lexically inside a `shared_examples` / `shared_examples_for` /
|
|
24
|
+
# `shared_context` block is skipped (its consumers live in other files);
|
|
25
|
+
# - every `let` in a file that uses `it_behaves_like` / `it_should_behave_like` /
|
|
26
|
+
# `include_examples` / `include_context` is skipped, because an included shared block may
|
|
27
|
+
# reference the binding by a name we cannot follow statically;
|
|
28
|
+
# - any `let` whose name is also defined as a `let`/`subject` in a `spec/support/**` helper is
|
|
29
|
+
# skipped, because it is almost certainly overriding a contract an included harness consumes;
|
|
30
|
+
# - `let(:cop_config)` is skipped: it is a rubocop-rspec contract consumed by the `:config`
|
|
31
|
+
# shared context, not by a reference in the spec file; and
|
|
32
|
+
# - every `let` in a file that reflectively dispatches through a name we cannot resolve
|
|
33
|
+
# statically (e.g. `send("expected_#{type}")`) is skipped, since any `let` could be the
|
|
34
|
+
# target.
|
|
35
|
+
# A name counts as referenced if it is called bare (`foo`), appears as a symbol (`:foo`)
|
|
36
|
+
# anywhere but the let's own name argument, or appears as an identifier-shaped token inside
|
|
37
|
+
# any string/heredoc literal -- covering dynamic dispatch, `:foo` entries in data tables the
|
|
38
|
+
# spec later dispatches on, and bindings named only inside raw SQL/GraphQL text.
|
|
39
|
+
#
|
|
40
|
+
# Because a bare `:foo` symbol anywhere counts as a reference, commonly-named lets
|
|
41
|
+
# (`let(:user)`, `let(:company)`, `let(:id)`) are essentially never flagged -- `create(:user)`,
|
|
42
|
+
# `:name` hash keys, and the like saturate the file. This conservative bias means the cop
|
|
43
|
+
# realistically only deletes distinctively-named dead lets; it is not a complete dead-`let`
|
|
44
|
+
# finder.
|
|
45
|
+
#
|
|
46
|
+
# @example
|
|
47
|
+
# # bad (name never referenced -- deleted, the block never runs)
|
|
48
|
+
# let(:unused) { create(:thing) }
|
|
49
|
+
#
|
|
50
|
+
# # good
|
|
51
|
+
# let(:thing) { create(:thing) }
|
|
52
|
+
# it { expect(thing).to be_present }
|
|
53
|
+
#
|
|
54
|
+
class UnreferencedLet < ::RuboCop::Cop::RSpec::Base
|
|
55
|
+
extend AutoCorrector
|
|
56
|
+
include RangeHelp
|
|
57
|
+
|
|
58
|
+
DEFINITION_METHODS = Set[:let, :let!, :subject].freeze
|
|
59
|
+
# `let`s consumed by a test framework rather than by a reference in the spec file. The
|
|
60
|
+
# rubocop-rspec `:config` shared context reads `cop_config`, so it is live even though the
|
|
61
|
+
# spec never names it.
|
|
62
|
+
FRAMEWORK_RESERVED_NAMES = %i(cop_config).freeze
|
|
63
|
+
# Reflective dispatch methods whose target is the first argument. When that argument is not
|
|
64
|
+
# a statically-resolvable name (a `sym` or plain `str`) -- e.g. `send("expected_#{type}")` --
|
|
65
|
+
# the called name cannot be known, so the whole file is left untouched.
|
|
66
|
+
DYNAMIC_DISPATCH_METHODS = %i(send public_send __send__ try try! method public_method respond_to?).freeze
|
|
67
|
+
FRAMEWORK_LET_PATTERN = /\b(?:let!?|subject)\s*\(?\s*:([A-Za-z_]\w*[!?]?)/
|
|
68
|
+
# Identifier-shaped tokens inside a string/heredoc literal. A `let` whose name appears only
|
|
69
|
+
# inside string text -- e.g. a binding or column referenced in raw SQL/GraphQL the spec
|
|
70
|
+
# later executes -- counts as referenced, so it is not deleted.
|
|
71
|
+
IDENTIFIER_IN_STRING = /[A-Za-z_]\w*[!?]?/
|
|
72
|
+
MSG = "Remove unreferenced `let(:%{name})` -- its name is never used, so the block never runs."
|
|
73
|
+
RESTRICT_ON_SEND = %i(let).freeze
|
|
74
|
+
# The glob and the pathspec encode the SAME set of files two ways: `Dir.glob` (fallback) and
|
|
75
|
+
# a regexp filter over `git ls-files` output. Keep them in sync if either changes.
|
|
76
|
+
SUPPORT_FILES_GLOB = "**/spec/support/**/*.rb"
|
|
77
|
+
SUPPORT_FILES_PATHSPEC = %r{(?:\A|/)spec/support/.+\.rb\z}
|
|
78
|
+
|
|
79
|
+
# The name symbol of any definition (`let`/`let!`/`subject`) in any block form -- used to
|
|
80
|
+
# count how many times a name is defined, so override / `super` chains (including a
|
|
81
|
+
# `subject` that overrides a `let` of the same name) are never flagged.
|
|
82
|
+
# @!method definition_name(node)
|
|
83
|
+
def_node_matcher :definition_name, <<~PATTERN
|
|
84
|
+
(any_block (send nil? %DEFINITION_METHODS (sym $_) ...) ...)
|
|
85
|
+
PATTERN
|
|
86
|
+
|
|
87
|
+
class << self
|
|
88
|
+
# Names defined as `let`/`subject` anywhere under `spec/support/**`. Computed once per
|
|
89
|
+
# process (lazily, after boot) and shared across every file the cop inspects.
|
|
90
|
+
def framework_let_names
|
|
91
|
+
@framework_let_names ||= scan_framework_let_names(support_file_paths)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Enumerate `spec/support/**/*.rb`. Prefer `git ls-files` (reads the git index, skipping
|
|
95
|
+
# untracked trees like `node_modules`): a leading-`**` `Dir.glob` walks the entire
|
|
96
|
+
# repository and costs seconds, while reading the index costs tens of milliseconds. Fall
|
|
97
|
+
# back to `Dir.glob` when not in a git work tree or `git` is unavailable.
|
|
98
|
+
#
|
|
99
|
+
# Tradeoff: an untracked (brand-new, uncommitted) `spec/support/*.rb` override is invisible
|
|
100
|
+
# to `git ls-files`. In that narrow window its contract names are not exempted; once
|
|
101
|
+
# committed it is seen like any other support file.
|
|
102
|
+
def support_file_paths
|
|
103
|
+
git_tracked_support_files || ::Dir.glob(SUPPORT_FILES_GLOB)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def git_tracked_support_files
|
|
107
|
+
output, status = ::Open3.capture2("git", "ls-files", "-z")
|
|
108
|
+
return nil unless status.success?
|
|
109
|
+
|
|
110
|
+
output.split("\x0").grep(SUPPORT_FILES_PATHSPEC)
|
|
111
|
+
rescue ::SystemCallError
|
|
112
|
+
nil
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def scan_framework_let_names(paths)
|
|
116
|
+
paths.each_with_object(Set.new) do |path, names|
|
|
117
|
+
extract_let_names(read_source(path), names)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def extract_let_names(source, names)
|
|
122
|
+
source.scan(FRAMEWORK_LET_PATTERN) { |(captured)| names << captured.to_sym }
|
|
123
|
+
names
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def read_source(path)
|
|
127
|
+
return "" unless ::File.file?(path)
|
|
128
|
+
|
|
129
|
+
::File.read(path)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def on_send(node)
|
|
134
|
+
return unless node.receiver.nil?
|
|
135
|
+
|
|
136
|
+
name_argument = node.first_argument
|
|
137
|
+
return unless name_argument&.sym_type?
|
|
138
|
+
|
|
139
|
+
block = node.block_node
|
|
140
|
+
return unless block
|
|
141
|
+
|
|
142
|
+
name = name_argument.value
|
|
143
|
+
return if exempt_from_deletion?(name, block)
|
|
144
|
+
|
|
145
|
+
add_offense(node.loc.selector, message: format(MSG, name:)) do |corrector|
|
|
146
|
+
corrector.remove(removal_range(block))
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
# A lazy `let` is exempt from deletion whenever file-scoped analysis cannot prove its name
|
|
153
|
+
# is dead: its name is a framework-reserved contract (e.g. `cop_config`), the file
|
|
154
|
+
# dispatches through a name we cannot resolve statically, it consumes shared examples, the
|
|
155
|
+
# `let` is lexically inside a shared-example definition, its name is a `spec/support/**`
|
|
156
|
+
# framework contract, it is overridden by another definition of the same name, or it is
|
|
157
|
+
# referenced somewhere in the file.
|
|
158
|
+
def exempt_from_deletion?(name, block)
|
|
159
|
+
FRAMEWORK_RESERVED_NAMES.include?(name) ||
|
|
160
|
+
dynamic_dispatch? ||
|
|
161
|
+
consumes_shared_examples? ||
|
|
162
|
+
within_shared_definition?(block) ||
|
|
163
|
+
self.class.framework_let_names.include?(name) ||
|
|
164
|
+
overridden?(name) ||
|
|
165
|
+
referenced?(name)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Delete the `let` block, plus:
|
|
169
|
+
# - an immediately-preceding `sig { ... }` (so a Sorbet signature is not left dangling),
|
|
170
|
+
# - explanatory comment lines attached directly above it (so they are not orphaned), and
|
|
171
|
+
# - a single trailing blank line where removal would otherwise leave a stray/duplicate
|
|
172
|
+
# blank -- unless the line above is a `let`/`subject`, where that blank is the required
|
|
173
|
+
# separator after the now-final let and must stay.
|
|
174
|
+
def removal_range(node)
|
|
175
|
+
lines = processed_source.lines
|
|
176
|
+
start_line = node.source_range.first_line
|
|
177
|
+
end_line = node.source_range.last_line
|
|
178
|
+
|
|
179
|
+
sig = preceding_sig(node)
|
|
180
|
+
start_line = sig.source_range.first_line if sig
|
|
181
|
+
|
|
182
|
+
start_line -= 1 while start_line > 1 && absorbable_comment?(lines[start_line - 2])
|
|
183
|
+
|
|
184
|
+
if end_line < lines.size && blank_line?(lines[end_line]) &&
|
|
185
|
+
!(start_line > 1 && let_or_subject_line?(lines[start_line - 2]))
|
|
186
|
+
end_line += 1
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
buffer = processed_source.buffer
|
|
190
|
+
range_by_whole_lines(buffer.line_range(start_line).join(buffer.line_range(end_line)), include_final_newline: true)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def absorbable_comment?(source_line)
|
|
194
|
+
stripped = source_line.strip
|
|
195
|
+
stripped.start_with?("#") && !stripped.start_with?("# rubocop:")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def blank_line?(source_line)
|
|
199
|
+
source_line.strip.empty?
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def let_or_subject_line?(source_line)
|
|
203
|
+
source_line.match?(/\A\s*(?:let!?|subject)\b/)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def preceding_sig(node)
|
|
207
|
+
sibling = node.left_sibling
|
|
208
|
+
return unless sibling.is_a?(::RuboCop::AST::BlockNode)
|
|
209
|
+
return unless sibling.method?(:sig)
|
|
210
|
+
|
|
211
|
+
sibling
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def within_shared_definition?(node)
|
|
215
|
+
node.each_ancestor(:any_block).any? { |ancestor| shared_group?(ancestor) }
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def consumes_shared_examples?
|
|
219
|
+
return @consumes_shared_examples unless @consumes_shared_examples.nil?
|
|
220
|
+
|
|
221
|
+
@consumes_shared_examples = processed_source.ast.each_node(:call).any? { |send_node| include?(send_node) }
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# True when the file reflectively dispatches through a name we cannot resolve statically --
|
|
225
|
+
# `send`/`public_send`/`method`/etc. called with anything other than a `sym` or plain `str`
|
|
226
|
+
# first argument (most commonly an interpolated string, `send("expected_#{type}")`). In
|
|
227
|
+
# that case any `let` in the file could be the dispatch target, so none are deleted.
|
|
228
|
+
def dynamic_dispatch?
|
|
229
|
+
return @dynamic_dispatch unless @dynamic_dispatch.nil?
|
|
230
|
+
|
|
231
|
+
@dynamic_dispatch = processed_source.ast.each_node(:call).any? do |send_node|
|
|
232
|
+
next false unless DYNAMIC_DISPATCH_METHODS.include?(send_node.method_name)
|
|
233
|
+
|
|
234
|
+
target = send_node.first_argument
|
|
235
|
+
target && !target.sym_type? && !target.str_type?
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def overridden?(name)
|
|
240
|
+
definitions_by_name.fetch(name, 0) > 1
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def definitions_by_name
|
|
244
|
+
@definitions_by_name ||= processed_source.ast.each_node(:any_block).each_with_object(Hash.new(0)) do |node, counts|
|
|
245
|
+
name = definition_name(node)
|
|
246
|
+
counts[name] += 1 if name
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def referenced?(name)
|
|
251
|
+
referenced_names.include?(name)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A name is "referenced" if it is called as a bare method (`foo`), appears as a symbol
|
|
255
|
+
# literal (`:foo`) other than the let/subject's own name argument, or appears as an
|
|
256
|
+
# identifier-shaped token inside any string/heredoc literal. The symbol and string cases
|
|
257
|
+
# cover indirect invocation -- `send(:foo)` / `send("foo")`, a `:foo`/`"foo"` listed in a
|
|
258
|
+
# data table the spec later dispatches on, or a binding named only inside raw SQL/GraphQL
|
|
259
|
+
# text the spec executes -- which file-scoped analysis cannot otherwise follow. (Tokenizing
|
|
260
|
+
# string bodies, rather than matching the whole string, keeps a `let` referenced only from
|
|
261
|
+
# inside a multi-word heredoc from being deleted.) Interpolated-string *dispatch* is handled
|
|
262
|
+
# separately by `dynamic_dispatch?`, which exempts the whole file.
|
|
263
|
+
def referenced_names
|
|
264
|
+
@referenced_names ||= processed_source.ast.each_node(:sym, :str, :call).each_with_object(Set.new) do |node, names|
|
|
265
|
+
if node.sym_type?
|
|
266
|
+
names << node.value unless definition_name_argument?(node)
|
|
267
|
+
elsif node.str_type?
|
|
268
|
+
# A string with invalid encoding (e.g. a deliberate bad-UTF-8 test fixture) cannot
|
|
269
|
+
# contain an identifier-shaped reference and would raise on `scan`, so skip it.
|
|
270
|
+
node.value.scan(IDENTIFIER_IN_STRING) { |token| names << token.to_sym } if node.value.valid_encoding?
|
|
271
|
+
elsif node.receiver.nil? && node.arguments.empty?
|
|
272
|
+
names << node.method_name
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def definition_name_argument?(sym_node)
|
|
278
|
+
parent = sym_node.parent
|
|
279
|
+
parent.send_type? && parent.receiver.nil? && DEFINITION_METHODS.include?(parent.method_name)
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks that `perform_async` calls to enqueue Sidekiq jobs are not stubbed
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# allow(Foo).to receive(:perform_async)
|
|
11
|
+
# expect(Foo).to receive(:perform_async)
|
|
12
|
+
# expect(Foo).not_to receive(:perform_async)
|
|
13
|
+
#
|
|
14
|
+
# # good (still invokes the real method)
|
|
15
|
+
# allow(Foo).to receive(:perform_async).and_call_original
|
|
16
|
+
# expect(Foo).to receive(:perform_async).with(arg).and_call_original
|
|
17
|
+
# allow(Foo).to receive(:perform_async).and_wrap_original { |m, *args| m.call(*args) }
|
|
18
|
+
#
|
|
19
|
+
# # good (checking enqueued jobs)
|
|
20
|
+
# expect { subject }.to change(Foo.jobs, :count).by(n)
|
|
21
|
+
# expect { subject }.not_to change(Foo.jobs, :count)
|
|
22
|
+
# expect(Foo.jobs.count).to eq(n)
|
|
23
|
+
#
|
|
24
|
+
# # good (only checks previously pre-stubbed objects)
|
|
25
|
+
# expect(Foo).to have_received(:perform_async)
|
|
26
|
+
#
|
|
27
|
+
# @safety
|
|
28
|
+
# Autocorrect is unsafe: it appends `.and_call_original` on positive `receive` only, which runs
|
|
29
|
+
# the real `perform_async` during the example (may enqueue jobs, hit external code, or
|
|
30
|
+
# change expectations vs a pure stub). There is no autocorrect for `not_to` / `to_not receive`,
|
|
31
|
+
# since `.and_call_original` would not apply to a negative expectation. Autocorrect is also
|
|
32
|
+
# suppressed when the expectation uses a block, since appending `.and_call_original` would
|
|
33
|
+
# rebind the block to the wrong method.
|
|
34
|
+
class PerformAsyncStub < Base
|
|
35
|
+
extend AutoCorrector
|
|
36
|
+
|
|
37
|
+
MSG = "Prefer checking enqueued jobs over stubbing `perform_async`."
|
|
38
|
+
MSG_RECEIVE = "Prefer checking enqueued jobs over stubbing `perform_async` or add `.and_call_original`."
|
|
39
|
+
RESTRICT_ON_SEND = %i(receive).freeze
|
|
40
|
+
|
|
41
|
+
# TODO: this should match on perform_async, not on receive, requires pattern update
|
|
42
|
+
# @!method stub_perform_async?(node)
|
|
43
|
+
def_node_matcher :stub_perform_async?, <<~PATTERN
|
|
44
|
+
(send nil? :receive (sym :perform_async))
|
|
45
|
+
PATTERN
|
|
46
|
+
|
|
47
|
+
def on_send(node)
|
|
48
|
+
return unless stub_perform_async?(node)
|
|
49
|
+
|
|
50
|
+
negative_expectation = false
|
|
51
|
+
calls_original = false
|
|
52
|
+
|
|
53
|
+
current = node.parent
|
|
54
|
+
while current&.call_type?
|
|
55
|
+
negative_expectation = true if current.method?(:not_to) || current.method?(:to_not)
|
|
56
|
+
calls_original = true if current.method?(:and_call_original) || current.method?(:and_wrap_original)
|
|
57
|
+
|
|
58
|
+
current = current.parent
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return add_offense(node) if negative_expectation
|
|
62
|
+
return if calls_original # already have .and_call_original, not an offense
|
|
63
|
+
|
|
64
|
+
tail = message_expectation_chain_tail(node)
|
|
65
|
+
return add_offense(node, message: MSG_RECEIVE) if tail.parent&.block_type?
|
|
66
|
+
|
|
67
|
+
add_offense(node, message: MSG_RECEIVE) do |corrector|
|
|
68
|
+
corrector.insert_after(tail, ".and_call_original")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
alias_method :on_csend, :on_send
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def message_expectation_chain_tail(node)
|
|
77
|
+
tail = node
|
|
78
|
+
loop do
|
|
79
|
+
parent = tail.parent
|
|
80
|
+
break unless parent&.call_type?
|
|
81
|
+
break unless parent.receiver.equal?(tail)
|
|
82
|
+
|
|
83
|
+
tail = parent
|
|
84
|
+
end
|
|
85
|
+
tail
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/rubocop/gusto/init.rb
CHANGED
|
@@ -10,6 +10,8 @@ module RuboCop
|
|
|
10
10
|
include Thor::Actions
|
|
11
11
|
|
|
12
12
|
PLUGINS = %w(rubocop-gusto rubocop-rspec rubocop-performance rubocop-rake rubocop-rails).freeze
|
|
13
|
+
SIDEKIQ_GEM_PATTERN = /\A\s*gem\s+['"]sidekiq['"]/
|
|
14
|
+
SIDEKIQ_LOCKFILE_PATTERN = /\A\s+sidekiq\s+\(/
|
|
13
15
|
|
|
14
16
|
class_option :rubocop_yml, type: :string, default: ".rubocop.yml"
|
|
15
17
|
|
|
@@ -34,13 +36,8 @@ module RuboCop
|
|
|
34
36
|
config = ConfigYml.load_file(options[:rubocop_yml])
|
|
35
37
|
end
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
config.add_plugin(PLUGINS)
|
|
40
|
-
else
|
|
41
|
-
config.add_inherit_gem("rubocop-gusto", "config/default.yml")
|
|
42
|
-
config.add_plugin(PLUGINS - %w(rubocop-rails))
|
|
43
|
-
end
|
|
39
|
+
config.add_inherit_gem("rubocop-gusto", *inherit_gem_configs)
|
|
40
|
+
config.add_plugin(rails? ? PLUGINS : PLUGINS - %w(rubocop-rails))
|
|
44
41
|
|
|
45
42
|
config.sort!
|
|
46
43
|
config.write(options[:rubocop_yml])
|
|
@@ -51,9 +48,25 @@ module RuboCop
|
|
|
51
48
|
|
|
52
49
|
private
|
|
53
50
|
|
|
51
|
+
def inherit_gem_configs
|
|
52
|
+
configs = ["config/default.yml"]
|
|
53
|
+
configs << "config/rails.yml" if rails?
|
|
54
|
+
configs << "config/sidekiq.yml" if sidekiq?
|
|
55
|
+
configs
|
|
56
|
+
end
|
|
57
|
+
|
|
54
58
|
def rails?
|
|
55
59
|
File.exist?("config/application.rb")
|
|
56
60
|
end
|
|
61
|
+
|
|
62
|
+
def sidekiq?
|
|
63
|
+
gem_referenced?("Gemfile", SIDEKIQ_GEM_PATTERN) ||
|
|
64
|
+
gem_referenced?("Gemfile.lock", SIDEKIQ_LOCKFILE_PATTERN)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def gem_referenced?(path, pattern)
|
|
68
|
+
File.exist?(path) && File.foreach(path).any? { |line| line.match?(pattern) }
|
|
69
|
+
end
|
|
57
70
|
end
|
|
58
71
|
end
|
|
59
72
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubocop-gusto
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 11.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Gusto Engineering
|
|
@@ -119,12 +119,16 @@ files:
|
|
|
119
119
|
- LICENSE
|
|
120
120
|
- README.md
|
|
121
121
|
- config/default.yml
|
|
122
|
+
- config/gusto_cops.yml
|
|
122
123
|
- config/rails.yml
|
|
124
|
+
- config/sidekiq.yml
|
|
123
125
|
- exe/gusto-rubocop
|
|
124
126
|
- exe/rubocop-gusto
|
|
125
127
|
- lib/rubocop-gusto.rb
|
|
128
|
+
- lib/rubocop/cop/gusto/all.rb
|
|
126
129
|
- lib/rubocop/cop/gusto/bootsnap_load_file.rb
|
|
127
130
|
- lib/rubocop/cop/gusto/datadog_constant.rb
|
|
131
|
+
- lib/rubocop/cop/gusto/described_class_constant_reference.rb
|
|
128
132
|
- lib/rubocop/cop/gusto/discouraged_gem.rb
|
|
129
133
|
- lib/rubocop/cop/gusto/execute_migration.rb
|
|
130
134
|
- lib/rubocop/cop/gusto/factory_classes_or_modules.rb
|
|
@@ -143,12 +147,14 @@ files:
|
|
|
143
147
|
- lib/rubocop/cop/gusto/rspec_date_time_mock.rb
|
|
144
148
|
- lib/rubocop/cop/gusto/sidekiq_params.rb
|
|
145
149
|
- lib/rubocop/cop/gusto/toplevel_constants.rb
|
|
150
|
+
- lib/rubocop/cop/gusto/unreferenced_let.rb
|
|
146
151
|
- lib/rubocop/cop/gusto/use_paint_not_colorize.rb
|
|
147
152
|
- lib/rubocop/cop/gusto/vcr_recordings.rb
|
|
148
153
|
- lib/rubocop/cop/internal_affairs/assignment_first.rb
|
|
149
154
|
- lib/rubocop/cop/internal_affairs/require_restrict_on_send.rb
|
|
150
155
|
- lib/rubocop/cop/rack/lowercase_header_keys.rb
|
|
151
156
|
- lib/rubocop/cop/rspec/scattered_let.rb
|
|
157
|
+
- lib/rubocop/cop/sidekiq/perform_async_stub.rb
|
|
152
158
|
- lib/rubocop/gusto.rb
|
|
153
159
|
- lib/rubocop/gusto/cli.rb
|
|
154
160
|
- lib/rubocop/gusto/config_yml.rb
|
|
@@ -169,7 +175,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
169
175
|
requirements:
|
|
170
176
|
- - ">="
|
|
171
177
|
- !ruby/object:Gem::Version
|
|
172
|
-
version: '3.
|
|
178
|
+
version: '3.3'
|
|
173
179
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
174
180
|
requirements:
|
|
175
181
|
- - ">="
|