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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +322 -0
  4. data/config/default.yml +396 -0
  5. data/lib/rubocop/cop/sidekiq/active_record_argument.rb +77 -0
  6. data/lib/rubocop/cop/sidekiq/async_in_test.rb +42 -0
  7. data/lib/rubocop/cop/sidekiq/base.rb +14 -0
  8. data/lib/rubocop/cop/sidekiq/consistent_job_suffix.rb +44 -0
  9. data/lib/rubocop/cop/sidekiq/constant_job_class_name.rb +53 -0
  10. data/lib/rubocop/cop/sidekiq/database_connection_leak.rb +33 -0
  11. data/lib/rubocop/cop/sidekiq/date_time_argument.rb +80 -0
  12. data/lib/rubocop/cop/sidekiq/deprecated_default_worker_options.rb +44 -0
  13. data/lib/rubocop/cop/sidekiq/deprecated_delay_extension.rb +29 -0
  14. data/lib/rubocop/cop/sidekiq/deprecated_worker_module.rb +40 -0
  15. data/lib/rubocop/cop/sidekiq/enqueue_inefficiency.rb +75 -0
  16. data/lib/rubocop/cop/sidekiq/excessive_retry.rb +52 -0
  17. data/lib/rubocop/cop/sidekiq/find_each_in_job.rb +58 -0
  18. data/lib/rubocop/cop/sidekiq/huge_job_arguments.rb +73 -0
  19. data/lib/rubocop/cop/sidekiq/job_dependency.rb +30 -0
  20. data/lib/rubocop/cop/sidekiq/job_file_location.rb +52 -0
  21. data/lib/rubocop/cop/sidekiq/job_file_naming.rb +38 -0
  22. data/lib/rubocop/cop/sidekiq/job_include.rb +67 -0
  23. data/lib/rubocop/cop/sidekiq/missing_logging.rb +49 -0
  24. data/lib/rubocop/cop/sidekiq/missing_timeout.rb +65 -0
  25. data/lib/rubocop/cop/sidekiq/mixed_retry_strategies.rb +55 -0
  26. data/lib/rubocop/cop/sidekiq/mixin/argument_traversal.rb +28 -0
  27. data/lib/rubocop/cop/sidekiq/mixin/class_name_helper.rb +23 -0
  28. data/lib/rubocop/cop/sidekiq/mixin/processed_source_path.rb +19 -0
  29. data/lib/rubocop/cop/sidekiq/no_rescue_all.rb +70 -0
  30. data/lib/rubocop/cop/sidekiq/perform_inline_usage.rb +53 -0
  31. data/lib/rubocop/cop/sidekiq/perform_method_parameters.rb +53 -0
  32. data/lib/rubocop/cop/sidekiq/pii_in_arguments.rb +90 -0
  33. data/lib/rubocop/cop/sidekiq/puts_or_print_usage.rb +37 -0
  34. data/lib/rubocop/cop/sidekiq/queue_specified.rb +74 -0
  35. data/lib/rubocop/cop/sidekiq/redis_in_job.rb +32 -0
  36. data/lib/rubocop/cop/sidekiq/retry_specified.rb +81 -0
  37. data/lib/rubocop/cop/sidekiq/retry_zero.rb +48 -0
  38. data/lib/rubocop/cop/sidekiq/self_scheduling_job.rb +54 -0
  39. data/lib/rubocop/cop/sidekiq/sensitive_data_in_arguments.rb +92 -0
  40. data/lib/rubocop/cop/sidekiq/sidekiq_over_active_job.rb +30 -0
  41. data/lib/rubocop/cop/sidekiq/silent_rescue.rb +63 -0
  42. data/lib/rubocop/cop/sidekiq/sleep_in_jobs.rb +46 -0
  43. data/lib/rubocop/cop/sidekiq/symbol_argument.rb +69 -0
  44. data/lib/rubocop/cop/sidekiq/thread_in_job.rb +53 -0
  45. data/lib/rubocop/cop/sidekiq/transaction_leak.rb +72 -0
  46. data/lib/rubocop/cop/sidekiq/unknown_sidekiq_option.rb +52 -0
  47. data/lib/rubocop/cop/sidekiq_cops.rb +71 -0
  48. data/lib/rubocop/cop/sidekiq_ent/base.rb +43 -0
  49. data/lib/rubocop/cop/sidekiq_ent/encryption_with_many_arguments.rb +71 -0
  50. data/lib/rubocop/cop/sidekiq_ent/encryption_without_secret_bag.rb +83 -0
  51. data/lib/rubocop/cop/sidekiq_ent/leader_election_without_block.rb +75 -0
  52. data/lib/rubocop/cop/sidekiq_ent/limiter_not_reused.rb +79 -0
  53. data/lib/rubocop/cop/sidekiq_ent/limiter_without_lock_timeout.rb +46 -0
  54. data/lib/rubocop/cop/sidekiq_ent/limiter_without_wait_timeout.rb +49 -0
  55. data/lib/rubocop/cop/sidekiq_ent/periodic_job_invalid_cron.rb +108 -0
  56. data/lib/rubocop/cop/sidekiq_ent/periodic_job_with_arguments.rb +94 -0
  57. data/lib/rubocop/cop/sidekiq_ent/unique_job_too_short_ttl.rb +80 -0
  58. data/lib/rubocop/cop/sidekiq_ent/unique_job_without_ttl.rb +52 -0
  59. data/lib/rubocop/cop/sidekiq_ent/unique_until_mismatch.rb +59 -0
  60. data/lib/rubocop/cop/sidekiq_pro/base.rb +39 -0
  61. data/lib/rubocop/cop/sidekiq_pro/batch_callback_method.rb +66 -0
  62. data/lib/rubocop/cop/sidekiq_pro/batch_retry_in_callback.rb +54 -0
  63. data/lib/rubocop/cop/sidekiq_pro/batch_status_polling.rb +68 -0
  64. data/lib/rubocop/cop/sidekiq_pro/batch_without_callback.rb +92 -0
  65. data/lib/rubocop/cop/sidekiq_pro/empty_batch.rb +108 -0
  66. data/lib/rubocop/cop/sidekiq_pro/expiring_job_without_ttl.rb +101 -0
  67. data/lib/rubocop/cop/sidekiq_pro/large_argument_in_batch.rb +93 -0
  68. data/lib/rubocop/cop/sidekiq_pro/nested_batch_without_parent.rb +55 -0
  69. data/lib/rubocop/cop/sidekiq_pro/reliability_not_enabled.rb +67 -0
  70. data/lib/rubocop/sidekiq/config_formatter.rb +60 -0
  71. data/lib/rubocop/sidekiq/description_extractor.rb +70 -0
  72. data/lib/rubocop/sidekiq/language.rb +79 -0
  73. data/lib/rubocop/sidekiq/plugin.rb +30 -0
  74. data/lib/rubocop/sidekiq/version.rb +7 -0
  75. data/lib/rubocop/sidekiq.rb +10 -0
  76. data/lib/rubocop-sidekiq.rb +5 -0
  77. data/lib/rubocop-sidekiq_plus.rb +9 -0
  78. 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