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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +27 -24
- data/lib/eager_eye/detectors/callback_query.rb +36 -8
- 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
|
|
|
@@ -99,27 +99,32 @@ module EagerEye
|
|
|
99
99
|
method_body = method_node.children[2]
|
|
100
100
|
return unless method_body
|
|
101
101
|
|
|
102
|
-
|
|
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
|
|
105
|
+
def find_iterations_with_queries(node, method_name, callback_type)
|
|
107
106
|
return unless node.is_a?(Parser::AST::Node)
|
|
108
107
|
|
|
109
|
-
|
|
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
|
-
|
|
115
|
+
find_iterations_with_queries(child, method_name, callback_type)
|
|
113
116
|
end
|
|
114
117
|
end
|
|
115
118
|
|
|
116
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
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
|
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
|