eager_eye 1.0.1 → 1.0.3

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: 6600c4275476bffa0b4db2a3d245bc889a39a113827ae5e9ee023fd4cdc108b2
4
- data.tar.gz: d295dbec99f78fe9c0c0eead0b7d863408eefa7e9e461f8681736963d243b6b7
3
+ metadata.gz: ecc47488d2632329805448f55dfafc1f6670efd02b04a3dd0464c22ef9a3a4ba
4
+ data.tar.gz: b37b1be91854abeea6a11d33f28df1ada2a3fb0a427b28a892cb1249c22a7e67
5
5
  SHA512:
6
- metadata.gz: c35374cd1c854cb377095ca7c973fdc9cc93ed3686275be51c0b6a87ff7fa696bbe4e582e8bc45d9df1b98da0b8781d9141ff9744af448efcdd0727af324d3a0
7
- data.tar.gz: 45d8b73b921239751f733147738e47018a967f88df1dfbb7b66a72a5cffa1deceb9ec480b9680a34e3d22532a38b142ddf9b2964a2ee43953f640b1cbf332e32
6
+ metadata.gz: 19adae5e3c000cfa658f5d2615da31c9a702ff031766240b86968d69165445bbbc7b630b5a1596822eaa9683c23db84379127d0c83ed4a59366475b3585972b7
7
+ data.tar.gz: a87d19f36dcaf9bd7a360b31051ed32da241e7c0b867815e8e17962f4b29bdd43d6983d84981e54a350ba4d415a076a84a8b1abc6852311277f25398c51d6a1a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.3] - 2025-12-20
11
+
12
+ ### Fixed
13
+
14
+ - Only flag queries in callbacks where the iteration variable is the receiver (reduces false positives)
15
+ - Updated README callback query documentation to accurately reflect current behavior
16
+
17
+ ## [1.0.2] - 2025-12-19
18
+
19
+ ### Fixed
20
+
21
+ - Only detect queries inside iterations in callback detector
22
+
10
23
  ## [1.0.1] - 2025-12-16
11
24
 
12
25
  ### Changed
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # EagerEye
2
2
 
3
3
  [![CI](https://github.com/hamzagedikkaya/eager_eye/actions/workflows/main.yml/badge.svg)](https://github.com/hamzagedikkaya/eager_eye/actions/workflows/main.yml)
4
- [![Gem Version](https://img.shields.io/badge/gem-v1.0.1-red.svg)](https://rubygems.org/gems/eager_eye)
4
+ [![Gem Version](https://img.shields.io/badge/gem-v1.0.3-red.svg)](https://rubygems.org/gems/eager_eye)
5
5
  [![Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen.svg)](https://github.com/hamzagedikkaya/eager_eye)
6
6
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.1-ruby.svg)](https://www.ruby-lang.org/)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -209,42 +209,45 @@ user.posts_count # Just reads the column
209
209
 
210
210
  ### 6. Callback Query Detection
211
211
 
212
- Detects database queries and iterations inside ActiveRecord callbacks. These become performance disasters during bulk operations.
212
+ Detects N+1 patterns inside ActiveRecord callbacks - specifically iterations that execute queries on each loop.
213
213
 
214
214
  ```ruby
215
- # Bad - Queries in callbacks
216
- class Article < ApplicationRecord
217
- after_save :recalculate_stats
215
+ # Bad - N+1 in callback (DETECTED)
216
+ class Order < ApplicationRecord
217
+ after_create :notify_subscribers
218
218
 
219
- def recalculate_stats
220
- author.articles.published.count # Runs on EVERY save
221
- category.update_article_count! # Another query on EVERY save
219
+ def notify_subscribers
220
+ customer.followers.each do |follower| # Error: Iteration in callback
221
+ follower.notifications.create!(...) # Warning: Query on iteration variable
222
+ end
222
223
  end
223
224
  end
224
225
 
225
- # Disaster scenario
226
- Article.import(1000.times.map { |i| { title: "Post #{i}" } })
227
- # = 2000+ queries from callbacks!
226
+ # OK - Single query in callback (NOT flagged - not N+1)
227
+ class Article < ApplicationRecord
228
+ after_save :update_stats
228
229
 
229
- # Bad - N+1 in callback
230
- class Order < ApplicationRecord
231
- after_create :notify_subscribers
230
+ def update_stats
231
+ author.articles.count # Single query, acceptable
232
+ end
233
+ end
232
234
 
233
- def notify_subscribers
234
- customer.followers.each do |follower| # N+1!
235
- NotificationMailer.new_order(follower).deliver_later
235
+ # OK - Query not on iteration variable (NOT flagged)
236
+ class Post < ApplicationRecord
237
+ after_save :process_items
238
+
239
+ def process_items
240
+ items.each do |item|
241
+ OtherModel.where(name: item.name).first # OtherModel is receiver, not item
236
242
  end
237
243
  end
238
244
  end
239
245
 
240
- # Good - Use conditional callbacks
241
- after_save :recalculate_stats, if: :should_recalculate?
242
-
243
- # Good - Move to background job
244
- after_commit :schedule_stats_update, on: :create
246
+ # Good - Move iterations to background job
247
+ after_commit :schedule_notifications, on: :create
245
248
 
246
- def schedule_stats_update
247
- RecalculateStatsJob.perform_later(id)
249
+ def schedule_notifications
250
+ NotifySubscribersJob.perform_later(id)
248
251
  end
249
252
  ```
250
253
 
@@ -99,27 +99,32 @@ module EagerEye
99
99
  method_body = method_node.children[2]
100
100
  return unless method_body
101
101
 
102
- find_query_calls(method_body, method_name, callback_type)
103
- find_iteration_with_queries(method_body, method_name, callback_type)
102
+ find_iterations_with_queries(method_body, method_name, callback_type)
104
103
  end
105
104
 
106
- def find_query_calls(node, method_name, callback_type)
105
+ def find_iterations_with_queries(node, method_name, callback_type)
107
106
  return unless node.is_a?(Parser::AST::Node)
108
107
 
109
- add_query_issue(node, method_name, callback_type) if query_call?(node)
108
+ if iteration_block?(node)
109
+ block_var = extract_block_variable(node)
110
+ add_iteration_issue(node, method_name, callback_type)
111
+ find_query_calls_in_block(node, method_name, callback_type, block_var) if block_var
112
+ end
110
113
 
111
114
  node.children.each do |child|
112
- find_query_calls(child, method_name, callback_type)
115
+ find_iterations_with_queries(child, method_name, callback_type)
113
116
  end
114
117
  end
115
118
 
116
- def find_iteration_with_queries(node, method_name, callback_type)
119
+ def find_query_calls_in_block(node, method_name, callback_type, block_var)
117
120
  return unless node.is_a?(Parser::AST::Node)
118
121
 
119
- add_iteration_issue(node, method_name, callback_type) if iteration_block?(node)
122
+ if query_call?(node) && receiver_chain_starts_with?(node.children[0], block_var)
123
+ add_query_issue(node, method_name, callback_type)
124
+ end
120
125
 
121
126
  node.children.each do |child|
122
- find_iteration_with_queries(child, method_name, callback_type)
127
+ find_query_calls_in_block(child, method_name, callback_type, block_var)
123
128
  end
124
129
  end
125
130
 
@@ -161,6 +166,29 @@ module EagerEye
161
166
  suggestion: "Avoid iterations in callbacks. Use background jobs for bulk operations"
162
167
  )
163
168
  end
169
+
170
+ def extract_block_variable(block_node)
171
+ args_node = block_node.children[1]
172
+ return nil unless args_node&.type == :args
173
+
174
+ first_arg = args_node.children[0]
175
+ return nil unless first_arg&.type == :arg
176
+
177
+ first_arg.children[0]
178
+ end
179
+
180
+ def receiver_chain_starts_with?(node, block_var)
181
+ return false unless node.is_a?(Parser::AST::Node)
182
+
183
+ case node.type
184
+ when :lvar
185
+ node.children[0] == block_var
186
+ when :send
187
+ receiver_chain_starts_with?(node.children[0], block_var)
188
+ else
189
+ false
190
+ end
191
+ end
164
192
  end
165
193
  end
166
194
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.0.1"
4
+ VERSION = "1.0.3"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eager_eye
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-16 00:00:00.000000000 Z
11
+ date: 2025-12-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast