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,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for network calls without explicit timeouts in Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# Net::HTTP.get(URI(url))
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# http.open_timeout = 5
|
|
14
|
+
# http.read_timeout = 10
|
|
15
|
+
#
|
|
16
|
+
class MissingTimeout < Base
|
|
17
|
+
MSG = 'Configure explicit timeouts for network calls in Sidekiq jobs.'
|
|
18
|
+
|
|
19
|
+
HTTP_METHODS = %i[get post put delete].freeze
|
|
20
|
+
TIMEOUT_METHODS = %i[timeout= open_timeout= read_timeout=].freeze
|
|
21
|
+
|
|
22
|
+
def on_def(node)
|
|
23
|
+
return unless node.method?(:perform)
|
|
24
|
+
return unless in_sidekiq_job?(node)
|
|
25
|
+
return if timeout_configured?(node)
|
|
26
|
+
|
|
27
|
+
node.each_descendant(:send) do |send|
|
|
28
|
+
next unless network_call?(send)
|
|
29
|
+
|
|
30
|
+
add_offense(send)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def timeout_configured?(def_node)
|
|
37
|
+
def_node.each_descendant(:send).any? do |send|
|
|
38
|
+
TIMEOUT_METHODS.include?(send.method_name)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def network_call?(send)
|
|
43
|
+
receiver = send.receiver
|
|
44
|
+
const_name = receiver&.const_name
|
|
45
|
+
return false unless const_name
|
|
46
|
+
|
|
47
|
+
return %i[get get_response start].include?(send.method_name) if const_name == 'Net::HTTP'
|
|
48
|
+
|
|
49
|
+
library_http_methods?(const_name, send.method_name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def library_http_methods?(const_name, method_name)
|
|
53
|
+
return method_name == :new if const_name == 'Faraday'
|
|
54
|
+
return HTTP_METHODS.include?(method_name) if http_libraries.include?(const_name)
|
|
55
|
+
|
|
56
|
+
false
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def http_libraries
|
|
60
|
+
%w[HTTParty RestClient HTTP Typhoeus].freeze
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for mixed ActiveJob and Sidekiq retry strategies in the same job.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# class MyJob < ApplicationJob
|
|
11
|
+
# retry_on SomeError
|
|
12
|
+
# sidekiq_options retry: 5
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
class MixedRetryStrategies < Base
|
|
16
|
+
MSG = 'Avoid mixing ActiveJob retry_on with Sidekiq retry options.'
|
|
17
|
+
|
|
18
|
+
def on_class(node)
|
|
19
|
+
return unless active_job_class?(node)
|
|
20
|
+
return unless retry_on_call?(node)
|
|
21
|
+
|
|
22
|
+
sidekiq_retry_option_nodes(node).each do |send|
|
|
23
|
+
add_offense(send)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def retry_on_call?(class_node)
|
|
30
|
+
class_node.each_descendant(:send).any? { |send| send.method?(:retry_on) }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def sidekiq_retry_option_nodes(class_node)
|
|
34
|
+
class_node.each_descendant(:send).select do |send|
|
|
35
|
+
sidekiq_options_call?(send) { |args| retry_option?(args) }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def retry_option?(args)
|
|
40
|
+
args.any? do |arg|
|
|
41
|
+
next false unless arg.hash_type?
|
|
42
|
+
|
|
43
|
+
arg.pairs.any? do |pair|
|
|
44
|
+
key = pair.key
|
|
45
|
+
next false unless key&.sym_type? && key.value == :retry
|
|
46
|
+
|
|
47
|
+
value = pair.value
|
|
48
|
+
!value.false_type?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Shared helpers for traversing Sidekiq job arguments.
|
|
7
|
+
module ArgumentTraversal
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def check_arguments(args, **kwargs)
|
|
11
|
+
args.each { |arg| check_argument(arg, **kwargs) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check_hash_values(hash_node, **kwargs)
|
|
15
|
+
hash_node.each_pair do |_key, value|
|
|
16
|
+
check_argument(value, **kwargs)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check_array_elements(array_node, **kwargs)
|
|
21
|
+
array_node.each_child_node do |element|
|
|
22
|
+
check_argument(element, **kwargs)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Shared helpers for extracting class names and file-friendly names.
|
|
7
|
+
module ClassNameHelper
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def class_name(node)
|
|
11
|
+
identifier = node&.identifier
|
|
12
|
+
return unless identifier&.const_type?
|
|
13
|
+
|
|
14
|
+
identifier.const_name.split('::').last
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def underscore(name)
|
|
18
|
+
name.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Shared helpers for working with the current file path.
|
|
7
|
+
module ProcessedSourcePath
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def processed_file_path
|
|
11
|
+
file_path = processed_source.file_path
|
|
12
|
+
return if file_path.nil? || file_path == '(string)'
|
|
13
|
+
|
|
14
|
+
file_path
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for bare `rescue` or `rescue Exception` in Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# Rescuing all exceptions can hide bugs and prevent Sidekiq's retry
|
|
9
|
+
# mechanism from working properly. If you need to handle errors,
|
|
10
|
+
# rescue specific exception classes and consider re-raising.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# class MyJob
|
|
15
|
+
# include Sidekiq::Job
|
|
16
|
+
#
|
|
17
|
+
# def perform
|
|
18
|
+
# do_work
|
|
19
|
+
# rescue
|
|
20
|
+
# log_error
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # bad
|
|
25
|
+
# class MyJob
|
|
26
|
+
# include Sidekiq::Job
|
|
27
|
+
#
|
|
28
|
+
# def perform
|
|
29
|
+
# do_work
|
|
30
|
+
# rescue Exception
|
|
31
|
+
# log_error
|
|
32
|
+
# end
|
|
33
|
+
# end
|
|
34
|
+
#
|
|
35
|
+
# # good
|
|
36
|
+
# class MyJob
|
|
37
|
+
# include Sidekiq::Job
|
|
38
|
+
#
|
|
39
|
+
# def perform
|
|
40
|
+
# do_work
|
|
41
|
+
# rescue NetworkError => e
|
|
42
|
+
# log_error(e)
|
|
43
|
+
# raise
|
|
44
|
+
# end
|
|
45
|
+
# end
|
|
46
|
+
#
|
|
47
|
+
class NoRescueAll < Base
|
|
48
|
+
MSG = 'Avoid rescuing all exceptions in Sidekiq jobs. ' \
|
|
49
|
+
'Rescue specific exceptions and consider re-raising.'
|
|
50
|
+
|
|
51
|
+
# @!method bare_rescue?(node)
|
|
52
|
+
def_node_matcher :bare_rescue?, <<~PATTERN
|
|
53
|
+
(resbody nil? ...)
|
|
54
|
+
PATTERN
|
|
55
|
+
|
|
56
|
+
# @!method rescue_exception?(node)
|
|
57
|
+
def_node_matcher :rescue_exception?, <<~PATTERN
|
|
58
|
+
(resbody (array (const {nil? cbase} :Exception)) ...)
|
|
59
|
+
PATTERN
|
|
60
|
+
|
|
61
|
+
def on_resbody(node)
|
|
62
|
+
return unless in_sidekiq_job?(node)
|
|
63
|
+
return unless bare_rescue?(node) || rescue_exception?(node)
|
|
64
|
+
|
|
65
|
+
add_offense(node)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for usage of `perform_inline` in production code.
|
|
7
|
+
#
|
|
8
|
+
# `perform_inline` executes the job synchronously, bypassing the
|
|
9
|
+
# job queue. This can be useful in tests but should generally be
|
|
10
|
+
# avoided in production code.
|
|
11
|
+
#
|
|
12
|
+
# @example AllowedInTests: true (default)
|
|
13
|
+
# # bad (in app/services/user_service.rb)
|
|
14
|
+
# MyJob.perform_inline(user.id)
|
|
15
|
+
#
|
|
16
|
+
# # good (in spec/jobs/my_job_spec.rb)
|
|
17
|
+
# MyJob.perform_inline(user.id)
|
|
18
|
+
#
|
|
19
|
+
# @example AllowedInTests: false
|
|
20
|
+
# # bad (in any file)
|
|
21
|
+
# MyJob.perform_inline(user.id)
|
|
22
|
+
#
|
|
23
|
+
class PerformInlineUsage < Base
|
|
24
|
+
MSG = 'Avoid using `perform_inline` in production code. ' \
|
|
25
|
+
'Use `perform_async` instead.'
|
|
26
|
+
|
|
27
|
+
RESTRICT_ON_SEND = %i[perform_inline].freeze
|
|
28
|
+
|
|
29
|
+
def on_send(node)
|
|
30
|
+
return if allowed_in_tests? && in_test_file?
|
|
31
|
+
|
|
32
|
+
add_offense(node)
|
|
33
|
+
end
|
|
34
|
+
alias on_csend on_send
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def allowed_in_tests?
|
|
39
|
+
cop_config.fetch('AllowedInTests', true)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def in_test_file?
|
|
43
|
+
file_path = processed_source.file_path
|
|
44
|
+
test_file_pattern.any? { |pattern| file_path.include?(pattern) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_file_pattern
|
|
48
|
+
%w[_spec.rb _test.rb /spec/ /test/]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks that the perform method does not use keyword arguments.
|
|
7
|
+
#
|
|
8
|
+
# Sidekiq serializes job arguments to JSON, which does not support
|
|
9
|
+
# Ruby keyword arguments. Using keyword arguments will cause errors
|
|
10
|
+
# or unexpected behavior.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# def perform(user_id:, status:)
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # bad
|
|
18
|
+
# def perform(id, status: 'pending')
|
|
19
|
+
# end
|
|
20
|
+
#
|
|
21
|
+
# # good
|
|
22
|
+
# def perform(user_id, status)
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class PerformMethodParameters < Base
|
|
26
|
+
MSG = 'Do not use keyword arguments in the `perform` method. ' \
|
|
27
|
+
'Sidekiq cannot serialize keyword arguments to JSON.'
|
|
28
|
+
|
|
29
|
+
# @!method perform_method?(node)
|
|
30
|
+
def_node_matcher :perform_method?, <<~PATTERN
|
|
31
|
+
(def :perform ...)
|
|
32
|
+
PATTERN
|
|
33
|
+
|
|
34
|
+
def on_def(node)
|
|
35
|
+
return unless perform_method?(node)
|
|
36
|
+
return unless in_sidekiq_job?(node)
|
|
37
|
+
|
|
38
|
+
node.arguments.each do |arg|
|
|
39
|
+
next unless keyword_argument?(arg)
|
|
40
|
+
|
|
41
|
+
add_offense(arg)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def keyword_argument?(arg)
|
|
48
|
+
arg.type?(:kwarg, :kwoptarg, :kwrestarg)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for PII passed as Sidekiq job arguments.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# NotifyJob.perform_async(email: 'user@example.com')
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# NotifyJob.perform_async(user_id)
|
|
14
|
+
#
|
|
15
|
+
class PiiInArguments < Base
|
|
16
|
+
include ArgumentTraversal
|
|
17
|
+
|
|
18
|
+
MSG = 'Avoid passing PII in Sidekiq job arguments.'
|
|
19
|
+
|
|
20
|
+
DEFAULT_PATTERNS = %w[email phone address].freeze
|
|
21
|
+
|
|
22
|
+
RESTRICT_ON_SEND = PerformMethods.all
|
|
23
|
+
|
|
24
|
+
def on_send(node)
|
|
25
|
+
perform_call?(node) do
|
|
26
|
+
check_arguments(node.arguments, allow_literal: true)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
alias on_csend on_send
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def check_argument(arg, allow_literal:)
|
|
34
|
+
return check_literal(arg, allow_literal) if literal_node?(arg)
|
|
35
|
+
return check_variable(arg) if var_node?(arg)
|
|
36
|
+
return check_send(arg) if arg.send_type?
|
|
37
|
+
return check_hash(arg) if arg.hash_type?
|
|
38
|
+
|
|
39
|
+
check_array_elements(arg, allow_literal: allow_literal) if arg.array_type?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def literal_node?(arg)
|
|
43
|
+
arg.type?(:sym, :str)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def var_node?(arg)
|
|
47
|
+
%i[lvar ivar gvar cvar].include?(arg.type)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def check_literal(arg, allow_literal)
|
|
51
|
+
return unless allow_literal
|
|
52
|
+
return unless pii_name?(arg.value.to_s)
|
|
53
|
+
|
|
54
|
+
add_offense(arg)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def check_variable(arg)
|
|
58
|
+
add_offense(arg) if pii_name?(arg.name.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def check_send(arg)
|
|
62
|
+
return unless arg.receiver.nil? && arg.arguments.empty?
|
|
63
|
+
return unless pii_name?(arg.method_name.to_s)
|
|
64
|
+
|
|
65
|
+
add_offense(arg)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def check_hash(arg)
|
|
69
|
+
arg.pairs.each do |pair|
|
|
70
|
+
check_hash_pair(pair)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def check_hash_pair(pair)
|
|
75
|
+
key = pair.key
|
|
76
|
+
add_offense(key) if key&.type?(:sym, :str) && pii_name?(key.value.to_s)
|
|
77
|
+
check_argument(pair.value, allow_literal: false) if pair.value
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def pii_name?(name)
|
|
81
|
+
patterns.any? { |pattern| name.downcase.include?(pattern) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def patterns
|
|
85
|
+
Array(cop_config.fetch('PiiPatterns', DEFAULT_PATTERNS))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for puts/print usage inside Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# puts 'Processing...'
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# logger.info 'Processing...'
|
|
14
|
+
#
|
|
15
|
+
class PutsOrPrintUsage < Base
|
|
16
|
+
MSG = 'Use logger instead of puts/print in Sidekiq jobs.'
|
|
17
|
+
|
|
18
|
+
RESTRICT_ON_SEND = %i[puts print].freeze
|
|
19
|
+
|
|
20
|
+
def on_send(node)
|
|
21
|
+
return unless node.receiver.nil?
|
|
22
|
+
return unless in_perform_in_sidekiq_job?(node)
|
|
23
|
+
|
|
24
|
+
add_offense(node)
|
|
25
|
+
end
|
|
26
|
+
alias on_csend on_send
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def in_perform_in_sidekiq_job?(node)
|
|
31
|
+
node.each_ancestor(:def).any? { |def_node| def_node.method?(:perform) } &&
|
|
32
|
+
in_sidekiq_job?(node)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks that Sidekiq jobs have an explicit queue specified.
|
|
7
|
+
#
|
|
8
|
+
# Without an explicit queue, jobs go to the `default` queue.
|
|
9
|
+
# Specifying queues helps with job organization and prioritization.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# # bad
|
|
13
|
+
# class MyJob
|
|
14
|
+
# include Sidekiq::Job
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # good
|
|
18
|
+
# class MyJob
|
|
19
|
+
# include Sidekiq::Job
|
|
20
|
+
# sidekiq_options queue: :critical
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
class QueueSpecified < Base
|
|
24
|
+
MSG = 'Specify a queue for this Sidekiq job using `sidekiq_options queue: :queue_name`.'
|
|
25
|
+
|
|
26
|
+
# @!method sidekiq_include?(node)
|
|
27
|
+
def_node_matcher :sidekiq_include?, <<~PATTERN
|
|
28
|
+
(send nil? :include (const (const {nil? cbase} :Sidekiq) {:Job :Worker}))
|
|
29
|
+
PATTERN
|
|
30
|
+
|
|
31
|
+
# @!method sidekiq_options_with_queue?(node)
|
|
32
|
+
def_node_matcher :sidekiq_options_with_queue?, <<~PATTERN
|
|
33
|
+
(send nil? :sidekiq_options (hash <(pair (sym :queue) _) ...>))
|
|
34
|
+
PATTERN
|
|
35
|
+
|
|
36
|
+
def on_class(node)
|
|
37
|
+
return unless sidekiq_job_class?(node)
|
|
38
|
+
return if queue_option?(node)
|
|
39
|
+
|
|
40
|
+
include_node = find_sidekiq_include(node)
|
|
41
|
+
add_offense(include_node) if include_node
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def sidekiq_job_class?(class_node)
|
|
47
|
+
return false unless class_node.body
|
|
48
|
+
|
|
49
|
+
find_sidekiq_include(class_node)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def find_sidekiq_include(class_node)
|
|
53
|
+
return nil unless class_node.body
|
|
54
|
+
|
|
55
|
+
if class_node.body.begin_type?
|
|
56
|
+
class_node.body.each_child_node.find { |n| sidekiq_include?(n) }
|
|
57
|
+
elsif sidekiq_include?(class_node.body)
|
|
58
|
+
class_node.body
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def queue_option?(class_node)
|
|
63
|
+
return false unless class_node.body
|
|
64
|
+
|
|
65
|
+
if class_node.body.begin_type?
|
|
66
|
+
class_node.body.each_child_node.any? { |n| sidekiq_options_with_queue?(n) }
|
|
67
|
+
else
|
|
68
|
+
sidekiq_options_with_queue?(class_node.body)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks for direct Redis connections inside Sidekiq jobs.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# # bad
|
|
10
|
+
# redis = Redis.new
|
|
11
|
+
#
|
|
12
|
+
# # good
|
|
13
|
+
# Sidekiq.redis { |conn| conn.get('key') }
|
|
14
|
+
#
|
|
15
|
+
class RedisInJob < Base
|
|
16
|
+
MSG = 'Use Sidekiq.redis instead of creating a new Redis connection in jobs.'
|
|
17
|
+
|
|
18
|
+
def on_def(node)
|
|
19
|
+
return unless node.method?(:perform)
|
|
20
|
+
return unless in_sidekiq_job?(node)
|
|
21
|
+
|
|
22
|
+
node.each_descendant(:send) do |send|
|
|
23
|
+
receiver = send.receiver
|
|
24
|
+
next unless receiver&.const_name == 'Redis' && send.method?(:new)
|
|
25
|
+
|
|
26
|
+
add_offense(send)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Sidekiq
|
|
6
|
+
# Checks that Sidekiq jobs have explicit retry configuration.
|
|
7
|
+
#
|
|
8
|
+
# Without explicit retry configuration, jobs use the default retry
|
|
9
|
+
# setting (25 retries). Being explicit about retry behavior makes
|
|
10
|
+
# the job's error handling clearer.
|
|
11
|
+
#
|
|
12
|
+
# @example
|
|
13
|
+
# # bad
|
|
14
|
+
# class MyJob
|
|
15
|
+
# include Sidekiq::Job
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# # good
|
|
19
|
+
# class MyJob
|
|
20
|
+
# include Sidekiq::Job
|
|
21
|
+
# sidekiq_options retry: 5
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# # good
|
|
25
|
+
# class MyJob
|
|
26
|
+
# include Sidekiq::Job
|
|
27
|
+
# sidekiq_options retry: false
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
class RetrySpecified < Base
|
|
31
|
+
MSG = 'Specify retry configuration for this Sidekiq job using `sidekiq_options retry: ...`.'
|
|
32
|
+
|
|
33
|
+
# @!method sidekiq_include?(node)
|
|
34
|
+
def_node_matcher :sidekiq_include?, <<~PATTERN
|
|
35
|
+
(send nil? :include (const (const {nil? cbase} :Sidekiq) {:Job :Worker}))
|
|
36
|
+
PATTERN
|
|
37
|
+
|
|
38
|
+
# @!method sidekiq_options_with_retry?(node)
|
|
39
|
+
def_node_matcher :sidekiq_options_with_retry?, <<~PATTERN
|
|
40
|
+
(send nil? :sidekiq_options (hash <(pair (sym :retry) _) ...>))
|
|
41
|
+
PATTERN
|
|
42
|
+
|
|
43
|
+
def on_class(node)
|
|
44
|
+
return unless sidekiq_job_class?(node)
|
|
45
|
+
return if retry_option?(node)
|
|
46
|
+
|
|
47
|
+
include_node = find_sidekiq_include(node)
|
|
48
|
+
add_offense(include_node) if include_node
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def sidekiq_job_class?(class_node)
|
|
54
|
+
return false unless class_node.body
|
|
55
|
+
|
|
56
|
+
find_sidekiq_include(class_node)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def find_sidekiq_include(class_node)
|
|
60
|
+
return nil unless class_node.body
|
|
61
|
+
|
|
62
|
+
if class_node.body.begin_type?
|
|
63
|
+
class_node.body.each_child_node.find { |n| sidekiq_include?(n) }
|
|
64
|
+
elsif sidekiq_include?(class_node.body)
|
|
65
|
+
class_node.body
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def retry_option?(class_node)
|
|
70
|
+
return false unless class_node.body
|
|
71
|
+
|
|
72
|
+
if class_node.body.begin_type?
|
|
73
|
+
class_node.body.each_child_node.any? { |n| sidekiq_options_with_retry?(n) }
|
|
74
|
+
else
|
|
75
|
+
sidekiq_options_with_retry?(class_node.body)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|