rubocop-sidekiq_plus 0.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +322 -0
- data/config/default.yml +396 -0
- data/lib/rubocop/cop/sidekiq/active_record_argument.rb +77 -0
- data/lib/rubocop/cop/sidekiq/async_in_test.rb +42 -0
- data/lib/rubocop/cop/sidekiq/base.rb +14 -0
- data/lib/rubocop/cop/sidekiq/consistent_job_suffix.rb +44 -0
- data/lib/rubocop/cop/sidekiq/constant_job_class_name.rb +53 -0
- data/lib/rubocop/cop/sidekiq/database_connection_leak.rb +33 -0
- data/lib/rubocop/cop/sidekiq/date_time_argument.rb +80 -0
- data/lib/rubocop/cop/sidekiq/deprecated_default_worker_options.rb +44 -0
- data/lib/rubocop/cop/sidekiq/deprecated_delay_extension.rb +29 -0
- data/lib/rubocop/cop/sidekiq/deprecated_worker_module.rb +40 -0
- data/lib/rubocop/cop/sidekiq/enqueue_inefficiency.rb +75 -0
- data/lib/rubocop/cop/sidekiq/excessive_retry.rb +52 -0
- data/lib/rubocop/cop/sidekiq/find_each_in_job.rb +58 -0
- data/lib/rubocop/cop/sidekiq/huge_job_arguments.rb +73 -0
- data/lib/rubocop/cop/sidekiq/job_dependency.rb +30 -0
- data/lib/rubocop/cop/sidekiq/job_file_location.rb +52 -0
- data/lib/rubocop/cop/sidekiq/job_file_naming.rb +38 -0
- data/lib/rubocop/cop/sidekiq/job_include.rb +67 -0
- data/lib/rubocop/cop/sidekiq/missing_logging.rb +49 -0
- data/lib/rubocop/cop/sidekiq/missing_timeout.rb +65 -0
- data/lib/rubocop/cop/sidekiq/mixed_retry_strategies.rb +55 -0
- data/lib/rubocop/cop/sidekiq/mixin/argument_traversal.rb +28 -0
- data/lib/rubocop/cop/sidekiq/mixin/class_name_helper.rb +23 -0
- data/lib/rubocop/cop/sidekiq/mixin/processed_source_path.rb +19 -0
- data/lib/rubocop/cop/sidekiq/no_rescue_all.rb +70 -0
- data/lib/rubocop/cop/sidekiq/perform_inline_usage.rb +53 -0
- data/lib/rubocop/cop/sidekiq/perform_method_parameters.rb +53 -0
- data/lib/rubocop/cop/sidekiq/pii_in_arguments.rb +90 -0
- data/lib/rubocop/cop/sidekiq/puts_or_print_usage.rb +37 -0
- data/lib/rubocop/cop/sidekiq/queue_specified.rb +74 -0
- data/lib/rubocop/cop/sidekiq/redis_in_job.rb +32 -0
- data/lib/rubocop/cop/sidekiq/retry_specified.rb +81 -0
- data/lib/rubocop/cop/sidekiq/retry_zero.rb +48 -0
- data/lib/rubocop/cop/sidekiq/self_scheduling_job.rb +54 -0
- data/lib/rubocop/cop/sidekiq/sensitive_data_in_arguments.rb +92 -0
- data/lib/rubocop/cop/sidekiq/sidekiq_over_active_job.rb +30 -0
- data/lib/rubocop/cop/sidekiq/silent_rescue.rb +63 -0
- data/lib/rubocop/cop/sidekiq/sleep_in_jobs.rb +46 -0
- data/lib/rubocop/cop/sidekiq/symbol_argument.rb +69 -0
- data/lib/rubocop/cop/sidekiq/thread_in_job.rb +53 -0
- data/lib/rubocop/cop/sidekiq/transaction_leak.rb +72 -0
- data/lib/rubocop/cop/sidekiq/unknown_sidekiq_option.rb +52 -0
- data/lib/rubocop/cop/sidekiq_cops.rb +71 -0
- data/lib/rubocop/cop/sidekiq_ent/base.rb +43 -0
- data/lib/rubocop/cop/sidekiq_ent/encryption_with_many_arguments.rb +71 -0
- data/lib/rubocop/cop/sidekiq_ent/encryption_without_secret_bag.rb +83 -0
- data/lib/rubocop/cop/sidekiq_ent/leader_election_without_block.rb +75 -0
- data/lib/rubocop/cop/sidekiq_ent/limiter_not_reused.rb +79 -0
- data/lib/rubocop/cop/sidekiq_ent/limiter_without_lock_timeout.rb +46 -0
- data/lib/rubocop/cop/sidekiq_ent/limiter_without_wait_timeout.rb +49 -0
- data/lib/rubocop/cop/sidekiq_ent/periodic_job_invalid_cron.rb +108 -0
- data/lib/rubocop/cop/sidekiq_ent/periodic_job_with_arguments.rb +94 -0
- data/lib/rubocop/cop/sidekiq_ent/unique_job_too_short_ttl.rb +80 -0
- data/lib/rubocop/cop/sidekiq_ent/unique_job_without_ttl.rb +52 -0
- data/lib/rubocop/cop/sidekiq_ent/unique_until_mismatch.rb +59 -0
- data/lib/rubocop/cop/sidekiq_pro/base.rb +39 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_callback_method.rb +66 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_retry_in_callback.rb +54 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_status_polling.rb +68 -0
- data/lib/rubocop/cop/sidekiq_pro/batch_without_callback.rb +92 -0
- data/lib/rubocop/cop/sidekiq_pro/empty_batch.rb +108 -0
- data/lib/rubocop/cop/sidekiq_pro/expiring_job_without_ttl.rb +101 -0
- data/lib/rubocop/cop/sidekiq_pro/large_argument_in_batch.rb +93 -0
- data/lib/rubocop/cop/sidekiq_pro/nested_batch_without_parent.rb +55 -0
- data/lib/rubocop/cop/sidekiq_pro/reliability_not_enabled.rb +67 -0
- data/lib/rubocop/sidekiq/config_formatter.rb +60 -0
- data/lib/rubocop/sidekiq/description_extractor.rb +70 -0
- data/lib/rubocop/sidekiq/language.rb +79 -0
- data/lib/rubocop/sidekiq/plugin.rb +30 -0
- data/lib/rubocop/sidekiq/version.rb +7 -0
- data/lib/rubocop/sidekiq.rb +10 -0
- data/lib/rubocop-sidekiq.rb +5 -0
- data/lib/rubocop-sidekiq_plus.rb +9 -0
- metadata +150 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for Date/Time/DateTime objects passed to Sidekiq job methods.
|
|
7
|
+
#
|
|
8
|
+
# Date and Time objects should be converted to strings or timestamps
|
|
9
|
+
# before being passed to Sidekiq jobs to ensure proper serialization.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# MyJob.perform_async(Time.current)
|
|
14
|
+
# MyJob.perform_async(Date.today)
|
|
15
|
+
# MyJob.perform_async(DateTime.now)
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# MyJob.perform_async(Time.current.iso8601)
|
|
19
|
+
# MyJob.perform_async(Date.today.to_s)
|
|
20
|
+
# MyJob.perform_async(Time.current.to_i)
|
|
21
|
+
#
|
|
22
|
+
class DateTimeArgument < Base
|
|
23
|
+
include ArgumentTraversal
|
|
24
|
+
|
|
25
|
+
MSG = 'Do not pass Date/Time objects to Sidekiq jobs. ' \
|
|
26
|
+
'Convert to a string or timestamp first.'
|
|
27
|
+
|
|
28
|
+
RESTRICT_ON_SEND = PerformMethods.all
|
|
29
|
+
|
|
30
|
+
TIME_METHODS = %i[now current zone].freeze
|
|
31
|
+
DATE_METHODS = %i[today yesterday tomorrow current].freeze
|
|
32
|
+
|
|
33
|
+
# @!method sidekiq_perform_call?(node)
|
|
34
|
+
def_node_matcher :sidekiq_perform_call?, <<~PATTERN
|
|
35
|
+
(send _ {#{RESTRICT_ON_SEND.map(&:inspect).join(' ')}} $...)
|
|
36
|
+
PATTERN
|
|
37
|
+
|
|
38
|
+
# @!method time_constructor?(node)
|
|
39
|
+
def_node_matcher :time_constructor?, <<~PATTERN
|
|
40
|
+
(send (const {nil? cbase} :Time) {#{TIME_METHODS.map(&:inspect).join(' ')}} ...)
|
|
41
|
+
PATTERN
|
|
42
|
+
|
|
43
|
+
# @!method date_constructor?(node)
|
|
44
|
+
def_node_matcher :date_constructor?, <<~PATTERN
|
|
45
|
+
(send (const {nil? cbase} :Date) {#{DATE_METHODS.map(&:inspect).join(' ')}} ...)
|
|
46
|
+
PATTERN
|
|
47
|
+
|
|
48
|
+
# @!method datetime_constructor?(node)
|
|
49
|
+
def_node_matcher :datetime_constructor?, <<~PATTERN
|
|
50
|
+
(send (const {nil? cbase} :DateTime) {:now :current :parse} ...)
|
|
51
|
+
PATTERN
|
|
52
|
+
|
|
53
|
+
def on_send(node)
|
|
54
|
+
sidekiq_perform_call?(node) do |args|
|
|
55
|
+
check_arguments(args)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
alias on_csend on_send
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def check_argument(arg)
|
|
63
|
+
if datetime_object?(arg)
|
|
64
|
+
add_offense(arg)
|
|
65
|
+
elsif arg.hash_type?
|
|
66
|
+
check_hash_values(arg)
|
|
67
|
+
elsif arg.array_type?
|
|
68
|
+
check_array_elements(arg)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def datetime_object?(node)
|
|
73
|
+
return false unless node.send_type?
|
|
74
|
+
|
|
75
|
+
time_constructor?(node) || date_constructor?(node) || datetime_constructor?(node)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for deprecated Sidekiq.default_worker_options usage.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# Sidekiq.default_worker_options = { retry: 5 }
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# Sidekiq.default_job_options = { retry: 5 }
|
|
14
|
+
#
|
|
15
|
+
class DeprecatedDefaultWorkerOptions < Base
|
|
16
|
+
extend AutoCorrector
|
|
17
|
+
|
|
18
|
+
MSG = 'Sidekiq.default_worker_options is deprecated. Use Sidekiq.default_job_options instead.'
|
|
19
|
+
|
|
20
|
+
RESTRICT_ON_SEND = %i[default_worker_options default_worker_options=].freeze
|
|
21
|
+
|
|
22
|
+
def on_send(node)
|
|
23
|
+
return unless sidekiq_receiver?(node)
|
|
24
|
+
|
|
25
|
+
add_offense(node.loc.selector) do |corrector|
|
|
26
|
+
corrector.replace(node.loc.selector, replacement_selector(node))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
alias on_csend on_send
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def sidekiq_receiver?(node)
|
|
34
|
+
receiver = node.receiver
|
|
35
|
+
receiver&.const_type? && receiver.const_name == 'Sidekiq'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def replacement_selector(_node)
|
|
39
|
+
'default_job_options'
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for deprecated delay extension usage.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# UserMailer.delay.welcome_email(user)
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# UserMailer.welcome_email(user).deliver_later
|
|
14
|
+
#
|
|
15
|
+
class DeprecatedDelayExtension < Base
|
|
16
|
+
MSG = 'Avoid using the delay extension. Use `deliver_later` or enqueue a Sidekiq job instead.'
|
|
17
|
+
|
|
18
|
+
RESTRICT_ON_SEND = %i[delay].freeze
|
|
19
|
+
|
|
20
|
+
def on_send(node)
|
|
21
|
+
return unless node.arguments.empty?
|
|
22
|
+
|
|
23
|
+
add_offense(node.loc.selector)
|
|
24
|
+
end
|
|
25
|
+
alias on_csend on_send
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for Sidekiq::Worker usage and recommends Sidekiq::Job.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class MyWorker
|
|
11
|
+
# include Sidekiq::Worker
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# class MyJob
|
|
16
|
+
# include Sidekiq::Job
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
class DeprecatedWorkerModule < Base
|
|
20
|
+
extend AutoCorrector
|
|
21
|
+
|
|
22
|
+
MSG = 'Sidekiq::Worker is deprecated. Use Sidekiq::Job instead.'
|
|
23
|
+
|
|
24
|
+
# @!method sidekiq_worker_include?(node)
|
|
25
|
+
def_node_matcher :sidekiq_worker_include?, <<~PATTERN
|
|
26
|
+
(send nil? :include (const (const {nil? cbase} :Sidekiq) :Worker))
|
|
27
|
+
PATTERN
|
|
28
|
+
|
|
29
|
+
def on_send(node)
|
|
30
|
+
sidekiq_worker_include?(node) do
|
|
31
|
+
add_offense(node) do |corrector|
|
|
32
|
+
corrector.replace(node, 'include Sidekiq::Job')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
alias on_csend on_send
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for perform_async calls inside loops and recommends perform_bulk.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# users.each do |user|
|
|
11
|
+
# NotifyJob.perform_async(user.id)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# user_ids = users.map { |user| [user.id] }
|
|
16
|
+
# NotifyJob.perform_bulk(user_ids)
|
|
17
|
+
#
|
|
18
|
+
class EnqueueInefficiency < Base
|
|
19
|
+
MSG = 'Prefer `perform_bulk` over `perform_async` inside loops to reduce Redis round trips.'
|
|
20
|
+
|
|
21
|
+
DEFAULT_ALLOWED_METHODS = %w[each find_each find_in_batches].freeze
|
|
22
|
+
|
|
23
|
+
def on_block(node)
|
|
24
|
+
send_node = node.send_node
|
|
25
|
+
return unless allowed_method?(send_node)
|
|
26
|
+
return unless meets_minimum_iterations?(send_node)
|
|
27
|
+
|
|
28
|
+
check_send(node.body)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def on_numblock(node)
|
|
32
|
+
on_block(node)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def check_send(body_node)
|
|
38
|
+
return unless body_node
|
|
39
|
+
|
|
40
|
+
add_offense(body_node) if body_node.send_type? && body_node.method?(:perform_async)
|
|
41
|
+
|
|
42
|
+
body_node.each_descendant(:send) do |send|
|
|
43
|
+
next unless send.method?(:perform_async)
|
|
44
|
+
|
|
45
|
+
add_offense(send)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def allowed_method?(send_node)
|
|
50
|
+
allowed_methods = cop_config.fetch('AllowedMethods', DEFAULT_ALLOWED_METHODS)
|
|
51
|
+
allowed_methods.include?(send_node.method_name.to_s)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def meets_minimum_iterations?(send_node)
|
|
55
|
+
minimum = cop_config['MinimumIterations']
|
|
56
|
+
return true unless minimum
|
|
57
|
+
|
|
58
|
+
iteration_count = iteration_count(send_node)
|
|
59
|
+
return true unless iteration_count
|
|
60
|
+
|
|
61
|
+
iteration_count >= minimum
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def iteration_count(send_node)
|
|
65
|
+
return unless send_node.method?(:times)
|
|
66
|
+
|
|
67
|
+
receiver = send_node.receiver
|
|
68
|
+
return unless receiver&.int_type?
|
|
69
|
+
|
|
70
|
+
receiver.value
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for excessive retry counts in sidekiq_options.
|
|
7
|
+
#
|
|
8
|
+
# @example MaxRetries: 25 (default)
|
|
9
|
+
# # bad
|
|
10
|
+
# sidekiq_options retry: 100
|
|
11
|
+
#
|
|
12
|
+
class ExcessiveRetry < Base
|
|
13
|
+
MSG = 'Retry count exceeds the maximum allowed (%<max>d).'
|
|
14
|
+
|
|
15
|
+
def on_send(node)
|
|
16
|
+
sidekiq_options_call?(node) do |args|
|
|
17
|
+
args.each { |arg| check_hash(arg) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
alias on_csend on_send
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def check_hash(arg)
|
|
25
|
+
return unless arg.hash_type?
|
|
26
|
+
|
|
27
|
+
arg.pairs.each do |pair|
|
|
28
|
+
check_pair(pair)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def check_pair(pair)
|
|
33
|
+
return unless retry_key?(pair.key)
|
|
34
|
+
|
|
35
|
+
value = pair.value
|
|
36
|
+
return unless value&.int_type?
|
|
37
|
+
return unless value.value > max_retries
|
|
38
|
+
|
|
39
|
+
add_offense(value, message: format(MSG, max: max_retries))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def retry_key?(node)
|
|
43
|
+
node&.sym_type? && node.value == :retry
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def max_retries
|
|
47
|
+
cop_config.fetch('MaxRetries', 25)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for find_each/find_in_batches usage inside Sidekiq job perform methods.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class ProcessAllUsersJob
|
|
11
|
+
# include Sidekiq::Job
|
|
12
|
+
#
|
|
13
|
+
# def perform
|
|
14
|
+
# User.find_each { |user| process(user) }
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# class ProcessUserJob
|
|
20
|
+
# include Sidekiq::Job
|
|
21
|
+
#
|
|
22
|
+
# def perform(user_id)
|
|
23
|
+
# user = User.find(user_id)
|
|
24
|
+
# process(user)
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
class FindEachInJob < Base
|
|
29
|
+
MSG = 'Avoid processing large datasets in a single Sidekiq job. Split into smaller jobs instead.'
|
|
30
|
+
|
|
31
|
+
DEFAULT_ALLOWED_METHODS = [].freeze
|
|
32
|
+
RESTRICT_ON_SEND = %i[find_each find_in_batches].freeze
|
|
33
|
+
|
|
34
|
+
def on_send(node)
|
|
35
|
+
return unless in_sidekiq_job?(node)
|
|
36
|
+
return unless in_perform_method?(node)
|
|
37
|
+
return if allowed_method?(node)
|
|
38
|
+
|
|
39
|
+
add_offense(node)
|
|
40
|
+
end
|
|
41
|
+
alias on_csend on_send
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def in_perform_method?(node)
|
|
46
|
+
node.each_ancestor(:any_def).any? do |def_node|
|
|
47
|
+
def_node.method?(:perform)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def allowed_method?(node)
|
|
52
|
+
allowed_methods = cop_config.fetch('AllowedMethods', DEFAULT_ALLOWED_METHODS)
|
|
53
|
+
allowed_methods.include?(node.method_name.to_s)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for potentially huge arguments passed to Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# MyJob.perform_async(users.pluck(:id, :name, :email))
|
|
11
|
+
# MyJob.perform_async([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
|
|
12
|
+
#
|
|
13
|
+
# # good
|
|
14
|
+
# MyJob.perform_async(user_ids)
|
|
15
|
+
#
|
|
16
|
+
class HugeJobArguments < Base
|
|
17
|
+
MSG = 'Avoid passing large arguments to Sidekiq jobs. Pass IDs and load records in the job instead.'
|
|
18
|
+
|
|
19
|
+
DEFAULT_MAX_ARRAY_SIZE = 10
|
|
20
|
+
DEFAULT_MAX_HASH_SIZE = 10
|
|
21
|
+
DEFAULT_MAX_PLUCK_COLUMNS = 3
|
|
22
|
+
|
|
23
|
+
def on_send(node)
|
|
24
|
+
perform_call?(node) do
|
|
25
|
+
node.arguments.each do |arg|
|
|
26
|
+
check_argument(arg)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
alias on_csend on_send
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def check_argument(arg)
|
|
35
|
+
return unless large_pluck?(arg) || large_array?(arg) || large_hash?(arg)
|
|
36
|
+
|
|
37
|
+
add_offense(arg)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def large_pluck?(arg)
|
|
41
|
+
return false unless arg.send_type?
|
|
42
|
+
return false unless arg.method?(:pluck)
|
|
43
|
+
|
|
44
|
+
arg.arguments.size >= max_pluck_columns
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def large_array?(arg)
|
|
48
|
+
return false unless arg.array_type?
|
|
49
|
+
|
|
50
|
+
arg.values.size > max_array_size
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def large_hash?(arg)
|
|
54
|
+
return false unless arg.hash_type?
|
|
55
|
+
|
|
56
|
+
arg.pairs.size > max_hash_size
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def max_array_size
|
|
60
|
+
cop_config.fetch('MaxArraySize', DEFAULT_MAX_ARRAY_SIZE)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def max_hash_size
|
|
64
|
+
cop_config.fetch('MaxHashSize', DEFAULT_MAX_HASH_SIZE)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def max_pluck_columns
|
|
68
|
+
cop_config.fetch('MaxPluckColumns', DEFAULT_MAX_PLUCK_COLUMNS)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for implicit job dependencies (enqueuing other jobs inside jobs).
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# def perform
|
|
11
|
+
# SecondJob.perform_async
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
class JobDependency < Base
|
|
15
|
+
MSG = 'Avoid implicit job dependencies. Use Sidekiq Batches instead.'
|
|
16
|
+
|
|
17
|
+
def on_def(node)
|
|
18
|
+
return unless node.method?(:perform)
|
|
19
|
+
return unless in_sidekiq_job?(node)
|
|
20
|
+
|
|
21
|
+
node.each_descendant(:send) do |send|
|
|
22
|
+
next unless perform_call?(send)
|
|
23
|
+
|
|
24
|
+
add_offense(send)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks that Sidekiq job classes are located in allowed directories.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad - app/models/send_email_job.rb
|
|
10
|
+
# class SendEmailJob
|
|
11
|
+
# include Sidekiq::Job
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # good - app/jobs/send_email_job.rb
|
|
15
|
+
# class SendEmailJob
|
|
16
|
+
# include Sidekiq::Job
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
class JobFileLocation < Base
|
|
20
|
+
include ProcessedSourcePath
|
|
21
|
+
|
|
22
|
+
MSG = 'Place Sidekiq job classes under %<dirs>s.'
|
|
23
|
+
|
|
24
|
+
DEFAULT_DIRECTORIES = ['app/jobs', 'app/workers'].freeze
|
|
25
|
+
|
|
26
|
+
def on_class(node)
|
|
27
|
+
return unless sidekiq_job_class?(node)
|
|
28
|
+
|
|
29
|
+
file_path = processed_file_path
|
|
30
|
+
return unless file_path
|
|
31
|
+
|
|
32
|
+
return if in_allowed_directory?(file_path)
|
|
33
|
+
|
|
34
|
+
add_offense(node.loc.keyword, message: format(MSG, dirs: allowed_directories.join(' or ')))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def allowed_directories
|
|
40
|
+
cop_config.fetch('AllowedDirectories', DEFAULT_DIRECTORIES)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def in_allowed_directory?(file_path)
|
|
44
|
+
allowed_directories.any? do |dir|
|
|
45
|
+
file_path.include?("/#{dir}/") || file_path.include?("#{dir}/") ||
|
|
46
|
+
file_path.end_with?("/#{dir}") || file_path.end_with?(dir)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks that job file names match the class name.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad - file: send_email_worker.rb
|
|
10
|
+
# class SendEmailJob
|
|
11
|
+
# include Sidekiq::Job
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
class JobFileNaming < Base
|
|
15
|
+
include ClassNameHelper
|
|
16
|
+
include ProcessedSourcePath
|
|
17
|
+
|
|
18
|
+
MSG = 'Job file name should match the class name.'
|
|
19
|
+
|
|
20
|
+
def on_class(node)
|
|
21
|
+
return unless sidekiq_job_class?(node)
|
|
22
|
+
|
|
23
|
+
file_path = processed_file_path
|
|
24
|
+
return unless file_path
|
|
25
|
+
|
|
26
|
+
class_name = class_name(node)
|
|
27
|
+
return unless class_name
|
|
28
|
+
|
|
29
|
+
expected = underscore(class_name)
|
|
30
|
+
actual = File.basename(file_path, '.rb')
|
|
31
|
+
return if expected == actual
|
|
32
|
+
|
|
33
|
+
add_offense(node.loc.keyword)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks that Sidekiq jobs include the preferred module.
|
|
7
|
+
#
|
|
8
|
+
# Sidekiq::Worker was renamed to Sidekiq::Job in Sidekiq 6.3.
|
|
9
|
+
# Sidekiq::Job is the preferred module name.
|
|
10
|
+
#
|
|
11
|
+
# @example PreferredModule: Job (default)
|
|
12
|
+
# # bad
|
|
13
|
+
# class MyJob
|
|
14
|
+
# include Sidekiq::Worker
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# class MyJob
|
|
19
|
+
# include Sidekiq::Job
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# @example PreferredModule: Worker
|
|
23
|
+
# # bad
|
|
24
|
+
# class MyJob
|
|
25
|
+
# include Sidekiq::Job
|
|
26
|
+
# end
|
|
27
|
+
#
|
|
28
|
+
# # good
|
|
29
|
+
# class MyJob
|
|
30
|
+
# include Sidekiq::Worker
|
|
31
|
+
# end
|
|
32
|
+
#
|
|
33
|
+
class JobInclude < Base
|
|
34
|
+
extend AutoCorrector
|
|
35
|
+
|
|
36
|
+
MSG = 'Use `Sidekiq::%<preferred>s` instead of `Sidekiq::%<deprecated>s`.'
|
|
37
|
+
|
|
38
|
+
MODULES = %w[Job Worker].freeze
|
|
39
|
+
|
|
40
|
+
# @!method sidekiq_include(node)
|
|
41
|
+
def_node_matcher :sidekiq_include, <<~PATTERN
|
|
42
|
+
(send nil? :include (const (const {nil? cbase} :Sidekiq) ${:Job :Worker}))
|
|
43
|
+
PATTERN
|
|
44
|
+
|
|
45
|
+
def on_send(node)
|
|
46
|
+
sidekiq_include(node) do |module_name|
|
|
47
|
+
return if module_name.to_s == preferred_module
|
|
48
|
+
|
|
49
|
+
deprecated = module_name.to_s
|
|
50
|
+
message = format(MSG, preferred: preferred_module, deprecated: deprecated)
|
|
51
|
+
|
|
52
|
+
add_offense(node, message: message) do |corrector|
|
|
53
|
+
corrector.replace(node, "include Sidekiq::#{preferred_module}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
alias on_csend on_send
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def preferred_module
|
|
62
|
+
cop_config.fetch('PreferredModule', 'Job')
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for missing logging in Sidekiq job perform methods.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# def perform(user_id)
|
|
11
|
+
# do_work(user_id)
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# # good
|
|
15
|
+
# def perform(user_id)
|
|
16
|
+
# logger.info "Starting job for #{user_id}"
|
|
17
|
+
# do_work(user_id)
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
class MissingLogging < Base
|
|
21
|
+
MSG = 'Add logging to Sidekiq job perform methods.'
|
|
22
|
+
|
|
23
|
+
def on_def(node)
|
|
24
|
+
return unless node.method?(:perform)
|
|
25
|
+
return unless in_sidekiq_job?(node)
|
|
26
|
+
return if logger_call?(node)
|
|
27
|
+
|
|
28
|
+
add_offense(node.loc.keyword)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def logger_call?(def_node)
|
|
34
|
+
def_node.each_descendant(:send).any? { |send| logger_call_send?(send) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def logger_call_send?(send)
|
|
38
|
+
receiver = send.receiver
|
|
39
|
+
return false unless receiver&.send_type?
|
|
40
|
+
|
|
41
|
+
receiver_name = receiver.method_name
|
|
42
|
+
return false unless %i[logger].include?(receiver_name)
|
|
43
|
+
|
|
44
|
+
%i[debug info warn error fatal].include?(send.method_name)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|