eager_eye 1.0.2 → 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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +27 -24
- data/lib/eager_eye/detectors/callback_query.rb +30 -4
- data/lib/eager_eye/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecc47488d2632329805448f55dfafc1f6670efd02b04a3dd0464c22ef9a3a4ba
|
|
4
|
+
data.tar.gz: b37b1be91854abeea6a11d33f28df1ada2a3fb0a427b28a892cb1249c22a7e67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
[](https://github.com/hamzagedikkaya/eager_eye/actions/workflows/main.yml)
|
|
4
|
-
[](https://rubygems.org/gems/eager_eye)
|
|
5
5
|
[](https://github.com/hamzagedikkaya/eager_eye)
|
|
6
6
|
[](https://www.ruby-lang.org/)
|
|
7
7
|
[](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
|
|
212
|
+
Detects N+1 patterns inside ActiveRecord callbacks - specifically iterations that execute queries on each loop.
|
|
213
213
|
|
|
214
214
|
```ruby
|
|
215
|
-
# Bad -
|
|
216
|
-
class
|
|
217
|
-
|
|
215
|
+
# Bad - N+1 in callback (DETECTED)
|
|
216
|
+
class Order < ApplicationRecord
|
|
217
|
+
after_create :notify_subscribers
|
|
218
218
|
|
|
219
|
-
def
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
#
|
|
226
|
-
Article
|
|
227
|
-
|
|
226
|
+
# OK - Single query in callback (NOT flagged - not N+1)
|
|
227
|
+
class Article < ApplicationRecord
|
|
228
|
+
after_save :update_stats
|
|
228
229
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
230
|
+
def update_stats
|
|
231
|
+
author.articles.count # Single query, acceptable
|
|
232
|
+
end
|
|
233
|
+
end
|
|
232
234
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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 -
|
|
241
|
-
|
|
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
|
|
247
|
-
|
|
249
|
+
def schedule_notifications
|
|
250
|
+
NotifySubscribersJob.perform_later(id)
|
|
248
251
|
end
|
|
249
252
|
```
|
|
250
253
|
|
|
@@ -106,8 +106,9 @@ module EagerEye
|
|
|
106
106
|
return unless node.is_a?(Parser::AST::Node)
|
|
107
107
|
|
|
108
108
|
if iteration_block?(node)
|
|
109
|
+
block_var = extract_block_variable(node)
|
|
109
110
|
add_iteration_issue(node, method_name, callback_type)
|
|
110
|
-
find_query_calls_in_block(node, method_name, callback_type)
|
|
111
|
+
find_query_calls_in_block(node, method_name, callback_type, block_var) if block_var
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
node.children.each do |child|
|
|
@@ -115,13 +116,15 @@ module EagerEye
|
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
|
|
118
|
-
def find_query_calls_in_block(node, method_name, callback_type)
|
|
119
|
+
def find_query_calls_in_block(node, method_name, callback_type, block_var)
|
|
119
120
|
return unless node.is_a?(Parser::AST::Node)
|
|
120
121
|
|
|
121
|
-
|
|
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
|
|
122
125
|
|
|
123
126
|
node.children.each do |child|
|
|
124
|
-
find_query_calls_in_block(child, method_name, callback_type)
|
|
127
|
+
find_query_calls_in_block(child, method_name, callback_type, block_var)
|
|
125
128
|
end
|
|
126
129
|
end
|
|
127
130
|
|
|
@@ -163,6 +166,29 @@ module EagerEye
|
|
|
163
166
|
suggestion: "Avoid iterations in callbacks. Use background jobs for bulk operations"
|
|
164
167
|
)
|
|
165
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
|
|
166
192
|
end
|
|
167
193
|
end
|
|
168
194
|
end
|
data/lib/eager_eye/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2025-12-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|