rubocop-gusto 11.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b642214b2077302ad9ab6e076c66534fe49cfcda33a411b87b9c4b34df8f8b43
4
- data.tar.gz: d9a653b4e27f33467921f5a2ffb520e2cc8b3a65f58e7cb4bb888611cea3e69d
3
+ metadata.gz: 5814e91aac6e402768ff64bd32f0c196838e20d90cbd927286238f68bb9d25b9
4
+ data.tar.gz: d4e988b9973101b7ea9a683900ae7489cbfe128b681d4137c7317f06a45cc247
5
5
  SHA512:
6
- metadata.gz: 876ba0b9f58929c28d77c71a370833670b27e0d55ecf8ab937f2882824db8b0da4ba2294a0649f2e54fdf7687b393d6c79be86da874a38d555b4190291c8a36b
7
- data.tar.gz: fe18184e9f77db188cbbdc77a5274fbe624d664e73ec97a2b5eec130b3753dd6323e5192e98441fe7832d166e2c8980e0d6231494f4d54b0317a9cb18aae9ee5
6
+ metadata.gz: 82385ed24fdf344897513f4f034e1f8ae662a5d41b62ccc8dc2c14e735d479deb31acf4c28047fbad1f59d828485798a4677148ca203989f15489d768cb88995
7
+ data.tar.gz: 403192785be424b372d709d2c7d9bac18491603c2a9449a2edeb5b35c981652e7ee87c307265a381b51d5285134fda5a0178adb04087bb6d2db7c10b51d41ae9
data/CHANGELOG.md CHANGED
@@ -3,6 +3,14 @@
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
+
6
14
  ## [11.0.0](https://github.com/Gusto/rubocop-gusto/compare/v10.10.0...v11.0.0) (2026-06-08)
7
15
 
8
16
 
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,135 +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/DescribedClassConstantReference:
46
- Description: 'Flags constants scoped through `described_class` (e.g. `described_class::Foo`), which Sorbet cannot resolve statically.'
47
- Enabled: true
48
- SafeAutoCorrect: false
49
- Include:
50
- - '**/spec/**/*'
51
-
52
- Gusto/DiscouragedGem:
53
- Description: 'Flags installation of discouraged gems in Gemfiles and gemspecs.'
54
- Enabled: false
55
- Gems: {}
56
-
57
- Gusto/ExecuteMigration:
58
- Description: "Don't use `execute` in migrations. Use a backfill rake task instead."
59
- Include:
60
- - 'db/migrate/*.rb'
61
-
62
- Gusto/FactoryClassesOrModules:
63
- Description: 'Do not define modules or classes in factory directories - they break reloading.'
64
- Include:
65
- - 'spec/**/factories/*.rb'
66
-
67
- Gusto/MinByMaxBy:
68
- Description: 'Checks for the use of `min` or `max` with a proc. Corrects to `min_by` or `max_by`.'
69
- Safe: false
70
- Severity: error
71
-
72
- Gusto/NoMetaprogramming:
73
- Description: 'Avoid using metaprogramming techniques like define_method and instance_eval which make code harder to understand and debug.'
74
-
75
- Gusto/NoRescueErrorMessageChecking:
76
- Description: 'Checks for the presence of error message checking within rescue blocks.'
77
-
78
- Gusto/NoSend:
79
- Description: 'Do not call a private method via `__send__`.'
80
- Exclude:
81
- - '**/spec/**/*'
82
-
83
- Gusto/PaperclipOrAttachable:
84
- Description: 'No more new paperclip or Attachable are allowed. Use ActiveStorage instead.'
85
-
86
- Gusto/PerformClassMethod:
87
- Description: 'Prevents accidental definition of `perform` class methods (should be instance methods instead).'
88
- # List of modules that include Sidekiq::Worker.
89
- # Add your other base modules here if they include Sidekiq::Worker too.
90
- WorkerModules:
91
- - Sidekiq::Worker
92
-
93
- Gusto/PolymorphicTypeValidation:
94
- Description: 'Ensures that polymorphic relations include a type validation, which is necessary for generating Sorbet types.'
95
- Include:
96
- - '**/models/**/*.rb'
97
-
98
- Gusto/PreferProcessLastStatus:
99
- Description: 'Prefer using `Process.last_status` instead of the global variables: `$?` and `$CHILD_STATUS`.'
100
-
101
- Gusto/RablExtends:
102
- Description: 'Disallows the use of `extends` in Rabl templates due to poor caching performance. Inline the templating to generate your JSON instead.'
103
- Include:
104
- - '**/*.json.rabl'
105
-
106
- Gusto/RailsEnv:
107
- Description: 'Use Feature Flags or config instead of `Rails.env`.'
108
-
109
- Gusto/RakeConstants:
110
- 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.'
111
- Include:
112
- - '**/*.rake'
113
- - 'Rakefile'
114
-
115
- Gusto/RegexpBypass:
116
- Description: 'Ensures regular expressions use \A and \z anchors instead of ^ and $ for security validation.'
117
- Exclude:
118
- - '**/spec/**/*'
119
- Safe: false
120
-
121
- Gusto/RspecDateTimeMock:
122
- Description: "Don't mock Date/Time/DateTime directly. Use Rails Testing Time Helpers (eg `freeze_time` and `travel_to`) instead."
123
- Include:
124
- - '**/spec/**/*'
125
- Safe: false
126
-
127
- Gusto/SidekiqParams:
128
- Description: 'Sidekiq perform methods cannot take keyword arguments.'
129
-
130
- Gusto/ToplevelConstants:
131
- Description: 'Prevents top-level constants from being defined outside of initializers.'
132
- Include:
133
- - '**/*.rb'
134
- Exclude:
135
- - '**/bin/*'
136
- - 'bin/*'
137
- - 'config/**/*'
138
- - 'lib/*.rb'
139
- - 'packs/**/{db/seeds,lib,config/initializers}/**/*'
140
- - 'script/**/*'
141
- - 'spec/rails_helper.rb'
142
- - '**/spec/support/**/*'
143
- - '**/*/spec_helper.rb'
144
- - 'spec/support/**/*.rb'
145
-
146
- Gusto/UnreferencedLet:
147
- Description: 'Removes a lazy let whose name is never referenced (its block never runs).'
148
- Enabled: true
149
- Include:
150
- - '**/spec/**/*_spec.rb'
151
- # Deletion is unsafe (explicit -A required, never applied on a plain run): the cop is heuristic
152
- # and cannot see references made across files via shared examples or included harnesses.
153
- SafeAutoCorrect: false
154
-
155
- Gusto/UsePaintNotColorize:
156
- Description: 'Use Paint instead of colorize for terminal colors.'
157
- SafeAutoCorrect: false
158
-
159
- Gusto/VcrRecordings:
160
- Description: 'VCR should be set to not record in tests. Use vcr: {record: :none}.'
161
-
162
35
  Layout/BlockAlignment:
163
36
  EnforcedStyleAlignWith: start_of_block
164
37
 
@@ -389,6 +262,13 @@ Rack/LowercaseHeaderKeys:
389
262
  Rake/ClassDefinitionInTask:
390
263
  Enabled: false
391
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
+
392
272
  Sorbet/ForbidTUnsafe:
393
273
  # T.unsafe completely disables typechecking. Prefer T.cast, shims, or
394
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}.'
@@ -0,0 +1,7 @@
1
+ #
2
+ # This file should be inherited alongside default.yml for projects using Sidekiq
3
+ #
4
+ # After you add a rule, sort this file with `bundle exec rubocop-gusto sort config/sidekiq.yml`
5
+
6
+ Sidekiq/PerformAsyncStub:
7
+ Enabled: true
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+ require_relative "../../gusto"
5
+ require_relative "../../gusto/version"
6
+
7
+ Dir.glob("#{__dir__}/*.rb").each { |f| require f unless f == __FILE__ }
@@ -3,7 +3,17 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Gusto
6
- # Do not use Bootsnap to load files. Use `require` instead.
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"
@@ -3,13 +3,16 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Gusto
6
- # Flags installation of discouraged gems (e.g., timecop) in Gemfiles and gemspecs.
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
- # Configuration:
9
- # Gems:
10
- # timecop: "Use Rails' time helpers (e.g., freeze_time, travel_to) instead of Timecop."
10
+ # @example Gems: { timecop: "Use Rails' time helpers instead of Timecop." }
11
+ # # bad
12
+ # gem "timecop"
11
13
  #
12
- # This cop is intended to be enabled in Rails projects via config/rails.yml.
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
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "open3"
4
+ require "rubocop-rspec"
4
5
 
5
6
  module RuboCop
6
7
  module Cop
@@ -54,7 +55,7 @@ module RuboCop
54
55
  extend AutoCorrector
55
56
  include RangeHelp
56
57
 
57
- DEFINITION_METHODS = %i(let let! subject).freeze
58
+ DEFINITION_METHODS = Set[:let, :let!, :subject].freeze
58
59
  # `let`s consumed by a test framework rather than by a reference in the spec file. The
59
60
  # rubocop-rspec `:config` shared context reads `cop_config`, so it is live even though the
60
61
  # spec never names it.
@@ -80,7 +81,7 @@ module RuboCop
80
81
  # `subject` that overrides a `let` of the same name) are never flagged.
81
82
  # @!method definition_name(node)
82
83
  def_node_matcher :definition_name, <<~PATTERN
83
- (any_block (send nil? {#{DEFINITION_METHODS.map { ":#{it}" }.join(' ')}} (sym $_) ...) ...)
84
+ (any_block (send nil? %DEFINITION_METHODS (sym $_) ...) ...)
84
85
  PATTERN
85
86
 
86
87
  class << self
@@ -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
@@ -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
- if rails?
38
- config.add_inherit_gem("rubocop-gusto", "config/default.yml", "config/rails.yml")
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RuboCop
4
4
  module Gusto
5
- VERSION = "11.0.0"
5
+ VERSION = "11.1.0"
6
6
  end
7
7
  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: 11.0.0
4
+ version: 11.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gusto Engineering
@@ -119,10 +119,13 @@ 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
128
131
  - lib/rubocop/cop/gusto/described_class_constant_reference.rb
@@ -151,6 +154,7 @@ files:
151
154
  - lib/rubocop/cop/internal_affairs/require_restrict_on_send.rb
152
155
  - lib/rubocop/cop/rack/lowercase_header_keys.rb
153
156
  - lib/rubocop/cop/rspec/scattered_let.rb
157
+ - lib/rubocop/cop/sidekiq/perform_async_stub.rb
154
158
  - lib/rubocop/gusto.rb
155
159
  - lib/rubocop/gusto/cli.rb
156
160
  - lib/rubocop/gusto/config_yml.rb
@@ -171,7 +175,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
171
175
  requirements:
172
176
  - - ">="
173
177
  - !ruby/object:Gem::Version
174
- version: '3.4'
178
+ version: '3.3'
175
179
  required_rubygems_version: !ruby/object:Gem::Requirement
176
180
  requirements:
177
181
  - - ">="