rubocop-callback_checker 0.1.0 → 0.1.1

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: 27a8d5e0a0b8d6c92ccabe6a928f106fc80a3d438ed07002686f07bf5c3c07c9
4
- data.tar.gz: 1bad8a7bb9dcf33f5758655bbdfc208f9849dc75cde2b534dbaedef18e7e31ad
3
+ metadata.gz: 0e884e5e21fa07e119f9f4b418e405be6611d2d5c0b696da5bdded094dd31455
4
+ data.tar.gz: a86502267af368a6d0922a46984190fb68b9f1e7a6523516b9695ce63c23f5b8
5
5
  SHA512:
6
- metadata.gz: 222120c9a2df85dc84fd57709c17b52b494f9e05b21a9ae64fc7f94d058595f6aebb3a9a821daad78707de021ff796c7039c73710760fd1c410f43e04a07e04f
7
- data.tar.gz: 29893d82a5f907f2d7cf88781864963e169f6c0e22b1bbc0f5615c0101773aa12f6a53a7e6dc5363c61a6849680090aa4edecd33e3edbe01af3130b47fc63bf6
6
+ metadata.gz: 6b893a0fe683cee9c9d8d8e636a10198b6fcd16c23358598c6983cb800cc7d68424a40dc334e9947853db463f8ceca043c6786b42b46a172ef1379802055a7de
7
+ data.tar.gz: 33aea6ae0240e55f7dfb902079679d704059ae2e00fa04f2a7b0968e1a73fa3a8ea3e51ef0bfa5d7d550d38bac804113aec35cc0254fac8a7737291a0ee73a13
data/.rubocop.yml CHANGED
@@ -1,15 +1,2 @@
1
1
  require:
2
- - rubocop-rspec
3
-
4
- plugins:
5
- - rubocop-rake
6
-
7
- AllCops:
8
- TargetRubyVersion: 3.1
9
- NewCops: enable
10
-
11
- Style/StringLiterals:
12
- EnforcedStyle: double_quotes
13
-
14
- Style/StringLiteralsInInterpolation:
15
- EnforcedStyle: double_quotes
2
+ - rubocop-callback_checker
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
@@ -5,7 +5,7 @@ require_relative '../rubocop/callback_checker/version'
5
5
  require 'optparse'
6
6
 
7
7
  module CallbackChecker
8
- VERSION = Rubocop::CallbackChecker::VERSION
8
+ VERSION = RuboCop::CallbackChecker::VERSION
9
9
 
10
10
  class CLI
11
11
  def self.run(argv)
@@ -22,15 +22,15 @@ module CallbackChecker
22
22
  parse_options
23
23
 
24
24
  if @paths.empty?
25
- puts "Usage: rubocop-callback-checker [options] FILE..."
25
+ puts 'Usage: rubocop-callback-checker [options] FILE...'
26
26
  puts "Try 'rubocop-callback-checker --help' for more information."
27
27
  return 1
28
28
  end
29
29
 
30
30
  files = collect_files(@paths)
31
-
31
+
32
32
  if files.empty?
33
- puts "No Ruby files found to analyze."
33
+ puts 'No Ruby files found to analyze.'
34
34
  return 1
35
35
  end
36
36
 
@@ -38,7 +38,7 @@ module CallbackChecker
38
38
 
39
39
  files.each do |file|
40
40
  offenses = PrismAnalyzer.analyze_file(file)
41
-
41
+
42
42
  if offenses.any?
43
43
  total_offenses += offenses.size
44
44
  print_offenses(file, offenses)
@@ -47,21 +47,21 @@ module CallbackChecker
47
47
 
48
48
  print_summary(files.size, total_offenses)
49
49
 
50
- total_offenses > 0 ? 1 : 0
50
+ total_offenses.positive? ? 1 : 0
51
51
  end
52
52
 
53
53
  private
54
54
 
55
55
  def parse_options
56
56
  OptionParser.new do |opts|
57
- opts.banner = "Usage: rubocop-callback-checker [options] FILE..."
57
+ opts.banner = 'Usage: rubocop-callback-checker [options] FILE...'
58
58
 
59
- opts.on("-h", "--help", "Print this help") do
59
+ opts.on('-h', '--help', 'Print this help') do
60
60
  puts opts
61
61
  exit 0
62
62
  end
63
63
 
64
- opts.on("-v", "--version", "Print version") do
64
+ opts.on('-v', '--version', 'Print version') do
65
65
  puts "rubocop-callback-checker version #{VERSION}"
66
66
  exit 0
67
67
  end
@@ -88,7 +88,7 @@ module CallbackChecker
88
88
 
89
89
  def print_offenses(file, offenses)
90
90
  puts "\n#{file}"
91
-
91
+
92
92
  offenses.each do |offense|
93
93
  location = offense[:location]
94
94
  puts " #{location[:start_line]}:#{location[:start_column]}: #{offense[:message]}"
@@ -97,7 +97,7 @@ module CallbackChecker
97
97
  end
98
98
 
99
99
  def print_summary(file_count, offense_count)
100
- puts "\n" + "=" * 80
100
+ puts "\n#{'=' * 80}"
101
101
  puts "#{file_count} file(s) inspected, #{offense_count} offense(s) detected"
102
102
  end
103
103
  end
@@ -78,9 +78,7 @@ module CallbackChecker
78
78
 
79
79
  def collect_methods(class_node)
80
80
  class_node.body&.body&.each do |statement|
81
- if statement.is_a?(Prism::DefNode)
82
- @callback_methods[statement.name] = statement
83
- end
81
+ @callback_methods[statement.name] = statement if statement.is_a?(Prism::DefNode)
84
82
  end
85
83
  end
86
84
 
@@ -148,7 +146,7 @@ module CallbackChecker
148
146
  if node.receiver.is_a?(Prism::ConstantReadNode)
149
147
  constant_name = node.receiver.name.to_s
150
148
  return true if SUSPICIOUS_CONSTANTS.include?(constant_name)
151
-
149
+
152
150
  # Check for any constant that isn't a known safe pattern
153
151
  # This catches things like NewsletterSDK, CustomAPI, etc.
154
152
  return true if constant_appears_to_be_external_service?(constant_name)
@@ -165,18 +163,14 @@ module CallbackChecker
165
163
  end
166
164
 
167
165
  # Check for method chains that end with deliver_now
168
- if method_name == :deliver_now && node.receiver.is_a?(Prism::CallNode)
169
- return true
170
- end
166
+ return true if method_name == :deliver_now && node.receiver.is_a?(Prism::CallNode)
171
167
 
172
168
  # Check for calls on associations or other objects (not self)
173
- if node.receiver && !self_reference?(node.receiver)
174
- return true if persistence_method?(method_name)
175
- end
169
+ return true if node.receiver && !self_reference?(node.receiver) && persistence_method?(method_name)
176
170
 
177
171
  # Check for save/update on self or implicit self
178
- if node.receiver.nil? || self_reference?(node.receiver)
179
- return true if %i[save save! update update!].include?(method_name)
172
+ if (node.receiver.nil? || self_reference?(node.receiver)) && %i[save save! update update!].include?(method_name)
173
+ return true
180
174
  end
181
175
 
182
176
  false
@@ -187,7 +181,7 @@ module CallbackChecker
187
181
  # it's probably an external service
188
182
  return true if constant_name.end_with?('SDK', 'API', 'Client', 'Service')
189
183
  return true if constant_name == constant_name.upcase && constant_name.length > 1
190
-
184
+
191
185
  false
192
186
  end
193
187
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Rubocop
3
+ module RuboCop
4
4
  module CallbackChecker
5
- VERSION = "0.1.0"
5
+ VERSION = '0.1.1'
6
6
  end
7
7
  end
@@ -1,21 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "callback_checker/version"
4
- require "pathname"
5
- require "yaml"
3
+ require_relative 'callback_checker/version'
4
+ require 'pathname'
6
5
 
7
- # Load all the cops
8
- Dir[Pathname.new(__dir__).join("cop", "callback_checker", "**", "*.rb")].each { |file| require file }
9
-
10
- module Rubocop
6
+ module RuboCop
11
7
  module CallbackChecker
12
8
  class Error < StandardError; end
13
9
 
14
10
  PROJECT_ROOT = Pathname.new(__dir__).parent.parent.freeze
15
- CONFIG_DEFAULT = PROJECT_ROOT.join("config", "default.yml").freeze
11
+ CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
16
12
 
17
- def self.version
18
- VERSION
19
- end
13
+ # Inject the plugin's default configuration into RuboCop
14
+ ::RuboCop::ConfigLoader.inject_defaults!(CONFIG_DEFAULT.to_s)
20
15
  end
21
16
  end
@@ -69,13 +69,13 @@ module RuboCop
69
69
 
70
70
  def on_class(node)
71
71
  @current_callbacks = {}
72
-
72
+
73
73
  node.each_descendant(:send) do |send_node|
74
74
  next unless callback_method?(send_node)
75
-
75
+
76
76
  callback_name = send_node.method_name
77
77
  callback_args = send_node.arguments
78
-
78
+
79
79
  callback_args.each do |arg|
80
80
  if arg.sym_type?
81
81
  method_name = arg.value
@@ -84,18 +84,16 @@ module RuboCop
84
84
  check_block_for_persistence(arg, callback_name)
85
85
  end
86
86
  end
87
-
88
- if send_node.block_node
89
- check_block_for_persistence(send_node.block_node, callback_name)
90
- end
87
+
88
+ check_block_for_persistence(send_node.block_node, callback_name) if send_node.block_node
91
89
  end
92
-
90
+
93
91
  node.each_descendant(:def) do |def_node|
94
92
  method_name = def_node.method_name
95
93
  callback_name = @current_callbacks[method_name]
96
-
94
+
97
95
  next unless callback_name
98
-
96
+
99
97
  check_method_for_persistence(def_node, callback_name)
100
98
  end
101
99
  end
@@ -105,7 +103,7 @@ module RuboCop
105
103
  def check_block_for_persistence(block_node, callback_name)
106
104
  block_node.each_descendant(:send) do |send_node|
107
105
  next unless persistence_call?(send_node)
108
-
106
+
109
107
  add_offense_for_node(send_node, callback_name)
110
108
  end
111
109
  end
@@ -113,7 +111,7 @@ module RuboCop
113
111
  def check_method_for_persistence(method_node, callback_name)
114
112
  method_node.each_descendant(:send) do |send_node|
115
113
  next unless persistence_call?(send_node)
116
-
114
+
117
115
  add_offense_for_node(send_node, callback_name)
118
116
  end
119
117
  end
@@ -121,14 +119,14 @@ module RuboCop
121
119
  def add_offense_for_node(node, callback_name)
122
120
  method_name = node.method_name
123
121
  attribute = extract_attribute_name(node)
124
-
122
+
125
123
  message = format(
126
124
  MSG,
127
125
  attribute: attribute,
128
126
  method: method_name,
129
127
  callback: callback_name
130
128
  )
131
-
129
+
132
130
  add_offense(node, message: message)
133
131
  end
134
132
 
@@ -138,11 +136,11 @@ module RuboCop
138
136
  key = first_hash_key(node)
139
137
  return key.to_s if key
140
138
  end
141
-
139
+
142
140
  # Try to extract from symbol argument (e.g., update_column(:name, 'foo'))
143
141
  symbol_arg = first_symbol_arg(node)
144
142
  return symbol_arg.to_s if symbol_arg
145
-
143
+
146
144
  'attribute'
147
145
  end
148
146
  end
@@ -23,9 +23,9 @@ module RuboCop
23
23
  # self.status = 'active'
24
24
  # end
25
25
  class AvoidSelfPersistence < Base
26
- MSG = "Avoid calling `%<method>s` on self within `%<callback>s`. " \
27
- "This can trigger infinite loops or run callbacks multiple times. " \
28
- "Assign attributes directly instead: `self.attribute = value`."
26
+ MSG = 'Avoid calling `%<method>s` on self within `%<callback>s`. ' \
27
+ 'This can trigger infinite loops or run callbacks multiple times. ' \
28
+ 'Assign attributes directly instead: `self.attribute = value`.'
29
29
 
30
30
  CALLBACK_METHODS = %i[
31
31
  before_validation after_validation
@@ -84,7 +84,7 @@ module RuboCop
84
84
  elsif arg.block_pass_type?
85
85
  # Handle block pass like: after_save &:method_name
86
86
  # We can't easily analyze these, so skip
87
- return
87
+ nil
88
88
  elsif arg.lambda_or_proc?
89
89
  check_proc_callback(arg, node.method_name)
90
90
  end
@@ -38,8 +38,8 @@ module RuboCop
38
38
  # # No callback, call UserRegistrationService.new(user).call from controller
39
39
  # end
40
40
  class CallbackMethodLength < Base
41
- MSG = "Callback method `%<method>s` is too long (%<length>d lines). " \
42
- "Max allowed: %<max>d lines. Extract complex logic to a service object."
41
+ MSG = 'Callback method `%<method>s` is too long (%<length>d lines). ' \
42
+ 'Max allowed: %<max>d lines. Extract complex logic to a service object.'
43
43
 
44
44
  CALLBACK_METHODS = %i[
45
45
  before_validation after_validation
@@ -54,7 +54,7 @@ module RuboCop
54
54
 
55
55
  def on_send(node)
56
56
  return unless callback_method?(node)
57
-
57
+
58
58
  # Only check symbol arguments (method name references)
59
59
  node.arguments.each do |arg|
60
60
  check_callback_argument(node, arg) if arg.sym_type?
@@ -100,11 +100,11 @@ module RuboCop
100
100
  return 0 unless method_node.body
101
101
 
102
102
  body = method_node.body
103
-
103
+
104
104
  # Calculate line count
105
105
  first_line = body.first_line
106
106
  last_line = body.last_line
107
-
107
+
108
108
  # Count non-empty lines
109
109
  (first_line..last_line).count do |line_number|
110
110
  line = processed_source.lines[line_number - 1]
@@ -26,8 +26,8 @@ module RuboCop
26
26
  # status == 'active' && !deleted?
27
27
  # end
28
28
  class ConditionalStyle < Base
29
- MSG = "Use a named method instead of a %<type>s for callback conditionals. " \
30
- "Extract the logic to a private method with a descriptive name."
29
+ MSG = 'Use a named method instead of a %<type>s for callback conditionals. ' \
30
+ 'Extract the logic to a private method with a descriptive name.'
31
31
 
32
32
  CALLBACK_METHODS = %i[
33
33
  before_validation after_validation
@@ -44,7 +44,7 @@ module RuboCop
44
44
 
45
45
  def on_send(node)
46
46
  return unless callback_method?(node)
47
-
47
+
48
48
  check_callback_conditionals(node)
49
49
  end
50
50
 
@@ -79,7 +79,7 @@ module RuboCop
79
79
 
80
80
  def conditional_key?(key)
81
81
  return false unless key.sym_type?
82
-
82
+
83
83
  CONDITIONAL_KEYS.include?(key.value)
84
84
  end
85
85
  end
@@ -14,8 +14,8 @@ module RuboCop
14
14
  # # good
15
15
  # after_commit :notify_external_api, on: :create
16
16
  class NoSideEffectsInCallbacks < Base
17
- MSG = "Avoid side effects (API calls, mailers, background jobs, or modifying other records) " \
18
- "in %<callback>s. Use `after_commit` instead."
17
+ MSG = 'Avoid side effects (API calls, mailers, background jobs, or modifying other records) ' \
18
+ 'in %<callback>s. Use `after_commit` instead.'
19
19
 
20
20
  SIDE_EFFECT_SENSITIVE_CALLBACKS = %i[
21
21
  before_validation before_save after_save
@@ -123,12 +123,12 @@ module RuboCop
123
123
  def part_of_reported_chain?(send_node)
124
124
  parent = send_node.parent
125
125
  return false unless parent&.send_type?
126
-
126
+
127
127
  # If parent is also a side effect, we'll report the parent instead
128
128
  # This handles cases like UserMailer.welcome(self).deliver_now
129
129
  # We want to report the .deliver_now, not the .welcome
130
130
  return false if async_delivery?(send_node) # Always report delivery methods
131
-
131
+
132
132
  # If this is a receiver of a delivery method, don't report it
133
133
  parent.receiver == send_node && async_delivery?(parent)
134
134
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'callback_checker/no_side_effects_in_callbacks'
4
+ require_relative 'callback_checker/avoid_self_persistence'
5
+ require_relative 'callback_checker/attribute_assignment_only'
6
+ require_relative 'callback_checker/callback_method_length'
7
+ require_relative 'callback_checker/conditional_style'
@@ -1,5 +1,5 @@
1
- require_relative "callback_checker/no_side_effects_in_callbacks"
2
- require_relative "callback_checker/avoid_self_persistence"
3
- require_relative "callback_checker/attribute_assignment_only"
4
- require_relative "callback_checker/callback_method_length"
5
- require_relative "callback_checker/conditional_style"
1
+ require_relative 'callback_checker/no_side_effects_in_callbacks'
2
+ require_relative 'callback_checker/avoid_self_persistence'
3
+ require_relative 'callback_checker/attribute_assignment_only'
4
+ require_relative 'callback_checker/callback_method_length'
5
+ require_relative 'callback_checker/conditional_style'
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ require_relative 'rubocop/callback_checker'
6
+ require_relative 'rubocop/cop/callback_checker_cops'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubocop-callback_checker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - rahsheen
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-03-13 00:00:00.000000000 Z
11
+ date: 2026-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rubocop
@@ -81,33 +81,33 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '2.0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: rubocop-rspec
84
+ name: rubocop-rake
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - "~>"
88
88
  - !ruby/object:Gem::Version
89
- version: '2.0'
89
+ version: '0.7'
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
94
  - - "~>"
95
95
  - !ruby/object:Gem::Version
96
- version: '2.0'
96
+ version: '0.7'
97
97
  - !ruby/object:Gem::Dependency
98
- name: rubocop-rake
98
+ name: rubocop-rspec
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.7'
103
+ version: '2.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.7'
110
+ version: '2.0'
111
111
  description: A RuboCop extension focused on avoiding callback hell in Rails.
112
112
  email:
113
113
  - rahsheen.porter@gmail.com
@@ -127,6 +127,7 @@ files:
127
127
  - exe/rubocop-callback-checker
128
128
  - lib/callback_checker/cli.rb
129
129
  - lib/callback_checker/prism_analyzer.rb
130
+ - lib/rubocop-callback_checker.rb
130
131
  - lib/rubocop/callback_checker.rb
131
132
  - lib/rubocop/callback_checker/version.rb
132
133
  - lib/rubocop/cop/callback_checker/attribute_assignment_only.rb
@@ -134,6 +135,7 @@ files:
134
135
  - lib/rubocop/cop/callback_checker/callback_method_length.rb
135
136
  - lib/rubocop/cop/callback_checker/conditional_style.rb
136
137
  - lib/rubocop/cop/callback_checker/no_side_effects_in_callbacks.rb
138
+ - lib/rubocop/cop/callback_checker_cops.rb
137
139
  - lib/rubocop/cop/cops.rb
138
140
  - sig/rubocop/callback_checker.rbs
139
141
  homepage: https://github.com/rahsheen/rubocop-callback_checker