eager_eye 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 037dbef0e4dc82641ae8c56bb8110f822900dd7afe76ddac6051b375bb547208
4
- data.tar.gz: 1d0878ea0046406e264ddfa92d68ac0c9f1748aea4803d9a638554bb13ea77c9
3
+ metadata.gz: a971d7fbafebc02b51a9767987541f67fdd216025ac04aa4bdf4f304cc3abad9
4
+ data.tar.gz: db6eeeb3e1f511856144c70031f63c5216c73b917c00a17a31dbcd44444757a8
5
5
  SHA512:
6
- metadata.gz: b97082ff4f9176c773a1007e9045305ad4c1d9ae46898bc1969eb66e6dd9bb42605bf567875ba7ba429693b1cd61385274587c51d6d0dc0397fc15f304d49af7
7
- data.tar.gz: ca8e47200e8d140361dfb57f37fff56616d1c75c284b19de43afeb2c1f179cdf506731866ccaf4b531e867877fd2815de2e6082134b8f6754bfe861068647ff4
6
+ metadata.gz: b847b72aff2e758f0a32effdf68057c499503f97251802263d5a1109e5eac3afd8841044d5d144a48b351c4c0909789e5528574f911101f3949e8f78d015dd0a
7
+ data.tar.gz: 3938004802489b63234cc75ec7cfc79c580a0cf263c004762a63fb2d4d1a09350f49f158ab9f4e99cbe4a11b9a8772766be61531c1909d958bed5b82141b407c
data/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2025-12-15
11
+
12
+ ### Added
13
+
14
+ - **New Detector: `CallbackQuery`** - Detects database queries and iterations inside ActiveRecord callbacks
15
+ - Identifies potential bulk operation performance issues
16
+ - Detects query methods (`.count`, `.sum`, `.update!`, etc.) in callbacks
17
+ - Detects iterations (`.each`, `.map`, etc.) in callbacks as errors
18
+ - Severity: `:error` for iterations in callbacks, `:warning` for query methods
19
+ - Suggests moving to background jobs or using conditional callbacks
20
+
21
+ ### Changed
22
+
23
+ - Updated default `enabled_detectors` to include `:callback_query`
24
+ - Updated README with new detector documentation
25
+
10
26
  ## [0.3.0] - 2025-12-15
11
27
 
12
28
  ### Added
data/README.md CHANGED
@@ -205,6 +205,47 @@ user.posts_count # Just reads the column
205
205
  | `.size` | Array#size | COUNT query |
206
206
  | `.length` | Array#length | Loads all, then counts |
207
207
 
208
+ ### 6. Callback Query Detection
209
+
210
+ Detects database queries and iterations inside ActiveRecord callbacks. These become performance disasters during bulk operations.
211
+
212
+ ```ruby
213
+ # Bad - Queries in callbacks
214
+ class Article < ApplicationRecord
215
+ after_save :recalculate_stats
216
+
217
+ def recalculate_stats
218
+ author.articles.published.count # Runs on EVERY save
219
+ category.update_article_count! # Another query on EVERY save
220
+ end
221
+ end
222
+
223
+ # Disaster scenario
224
+ Article.import(1000.times.map { |i| { title: "Post #{i}" } })
225
+ # = 2000+ queries from callbacks!
226
+
227
+ # Bad - N+1 in callback
228
+ class Order < ApplicationRecord
229
+ after_create :notify_subscribers
230
+
231
+ def notify_subscribers
232
+ customer.followers.each do |follower| # N+1!
233
+ NotificationMailer.new_order(follower).deliver_later
234
+ end
235
+ end
236
+ end
237
+
238
+ # Good - Use conditional callbacks
239
+ after_save :recalculate_stats, if: :should_recalculate?
240
+
241
+ # Good - Move to background job
242
+ after_commit :schedule_stats_update, on: :create
243
+
244
+ def schedule_stats_update
245
+ RecalculateStatsJob.perform_later(id)
246
+ end
247
+ ```
248
+
208
249
  ## Configuration
209
250
 
210
251
  ### Config File (.eager_eye.yml)
@@ -222,6 +263,7 @@ enabled_detectors:
222
263
  - missing_counter_cache
223
264
  - custom_method_query
224
265
  - count_in_iteration
266
+ - callback_query
225
267
 
226
268
  # Base path to analyze (default: app)
227
269
  app_path: app
@@ -9,7 +9,8 @@ module EagerEye
9
9
  serializer_nesting: Detectors::SerializerNesting,
10
10
  missing_counter_cache: Detectors::MissingCounterCache,
11
11
  custom_method_query: Detectors::CustomMethodQuery,
12
- count_in_iteration: Detectors::CountInIteration
12
+ count_in_iteration: Detectors::CountInIteration,
13
+ callback_query: Detectors::CallbackQuery
13
14
  }.freeze
14
15
 
15
16
  attr_reader :paths, :issues
@@ -6,7 +6,7 @@ module EagerEye
6
6
 
7
7
  DEFAULT_DETECTORS = %i[
8
8
  loop_association serializer_nesting missing_counter_cache
9
- custom_method_query count_in_iteration
9
+ custom_method_query count_in_iteration callback_query
10
10
  ].freeze
11
11
 
12
12
  def initialize
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ module Detectors
5
+ class CallbackQuery < Base
6
+ CALLBACK_METHODS = %i[
7
+ before_validation after_validation
8
+ before_save after_save around_save
9
+ before_create after_create around_create
10
+ before_update after_update around_update
11
+ before_destroy after_destroy around_destroy
12
+ after_commit after_rollback
13
+ after_create_commit after_update_commit after_destroy_commit
14
+ after_save_commit
15
+ ].freeze
16
+
17
+ QUERY_INDICATORS = %i[
18
+ where find find_by find_by! first last take
19
+ exists? any? none? many? one?
20
+ count sum average minimum maximum
21
+ pluck ids select
22
+ update update_all update! update_attribute update_column update_columns
23
+ destroy destroy_all destroy! delete delete_all
24
+ create create! save save! insert insert_all insert! upsert upsert_all
25
+ increment! decrement! toggle!
26
+ reload
27
+ ].freeze
28
+
29
+ ITERATION_METHODS = %i[each map select find_all reject collect].freeze
30
+
31
+ def self.detector_name
32
+ :callback_query
33
+ end
34
+
35
+ def detect(ast, file_path)
36
+ @issues = []
37
+ @file_path = file_path
38
+ @callback_methods = {}
39
+
40
+ return @issues unless ast
41
+
42
+ find_callback_definitions(ast)
43
+ check_callback_methods(ast)
44
+
45
+ @issues
46
+ end
47
+
48
+ private
49
+
50
+ def find_callback_definitions(node)
51
+ return unless node.is_a?(Parser::AST::Node)
52
+
53
+ extract_callback_method_name(node) if callback_definition?(node)
54
+
55
+ node.children.each do |child|
56
+ find_callback_definitions(child)
57
+ end
58
+ end
59
+
60
+ def callback_definition?(node)
61
+ return false unless node.type == :send
62
+ return false unless node.children[0].nil?
63
+
64
+ method_name = node.children[1]
65
+ CALLBACK_METHODS.include?(method_name)
66
+ end
67
+
68
+ def extract_callback_method_name(node)
69
+ node.children[2..].each do |arg|
70
+ next unless arg.is_a?(Parser::AST::Node) && arg.type == :sym
71
+
72
+ method_name = arg.children[0]
73
+ callback_type = node.children[1]
74
+ @callback_methods[method_name] = callback_type
75
+ end
76
+ end
77
+
78
+ def check_callback_methods(node)
79
+ return unless node.is_a?(Parser::AST::Node)
80
+
81
+ if method_definition?(node)
82
+ method_name = node.children[0]
83
+ if @callback_methods.key?(method_name)
84
+ callback_type = @callback_methods[method_name]
85
+ check_method_body_for_queries(node, method_name, callback_type)
86
+ end
87
+ end
88
+
89
+ node.children.each do |child|
90
+ check_callback_methods(child)
91
+ end
92
+ end
93
+
94
+ def method_definition?(node)
95
+ node.type == :def
96
+ end
97
+
98
+ def check_method_body_for_queries(method_node, method_name, callback_type)
99
+ method_body = method_node.children[2]
100
+ return unless method_body
101
+
102
+ find_query_calls(method_body, method_name, callback_type)
103
+ find_iteration_with_queries(method_body, method_name, callback_type)
104
+ end
105
+
106
+ def find_query_calls(node, method_name, callback_type)
107
+ return unless node.is_a?(Parser::AST::Node)
108
+
109
+ add_query_issue(node, method_name, callback_type) if query_call?(node)
110
+
111
+ node.children.each do |child|
112
+ find_query_calls(child, method_name, callback_type)
113
+ end
114
+ end
115
+
116
+ def find_iteration_with_queries(node, method_name, callback_type)
117
+ return unless node.is_a?(Parser::AST::Node)
118
+
119
+ add_iteration_issue(node, method_name, callback_type) if iteration_block?(node)
120
+
121
+ node.children.each do |child|
122
+ find_iteration_with_queries(child, method_name, callback_type)
123
+ end
124
+ end
125
+
126
+ def query_call?(node)
127
+ return false unless node.type == :send
128
+
129
+ method = node.children[1]
130
+ QUERY_INDICATORS.include?(method)
131
+ end
132
+
133
+ def iteration_block?(node)
134
+ return false unless node.type == :block
135
+
136
+ send_node = node.children[0]
137
+ return false unless send_node&.type == :send
138
+
139
+ method_name = send_node.children[1]
140
+ ITERATION_METHODS.include?(method_name)
141
+ end
142
+
143
+ def add_query_issue(node, method_name, callback_type)
144
+ query_method = node.children[1]
145
+
146
+ @issues << create_issue(
147
+ file_path: @file_path,
148
+ line_number: node.loc.line,
149
+ message: "Query method `.#{query_method}` found in `#{callback_type}` callback `:#{method_name}`",
150
+ severity: :warning,
151
+ suggestion: "Callbacks run on every save/create/update. Consider moving to a background job"
152
+ )
153
+ end
154
+
155
+ def add_iteration_issue(node, method_name, callback_type)
156
+ @issues << create_issue(
157
+ file_path: @file_path,
158
+ line_number: node.loc.line,
159
+ message: "Iteration found in `#{callback_type}` callback `:#{method_name}` - potential N+1",
160
+ severity: :error,
161
+ suggestion: "Avoid iterations in callbacks. Use background jobs for bulk operations"
162
+ )
163
+ end
164
+ end
165
+ end
166
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/eager_eye.rb CHANGED
@@ -9,6 +9,7 @@ require_relative "eager_eye/detectors/serializer_nesting"
9
9
  require_relative "eager_eye/detectors/missing_counter_cache"
10
10
  require_relative "eager_eye/detectors/custom_method_query"
11
11
  require_relative "eager_eye/detectors/count_in_iteration"
12
+ require_relative "eager_eye/detectors/callback_query"
12
13
  require_relative "eager_eye/analyzer"
13
14
  require_relative "eager_eye/reporters/base"
14
15
  require_relative "eager_eye/reporters/console"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eager_eye
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
@@ -61,6 +61,7 @@ files:
61
61
  - lib/eager_eye/cli.rb
62
62
  - lib/eager_eye/configuration.rb
63
63
  - lib/eager_eye/detectors/base.rb
64
+ - lib/eager_eye/detectors/callback_query.rb
64
65
  - lib/eager_eye/detectors/count_in_iteration.rb
65
66
  - lib/eager_eye/detectors/custom_method_query.rb
66
67
  - lib/eager_eye/detectors/loop_association.rb