eager_eye 1.0.4 → 1.0.5
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 +9 -0
- data/README.md +7 -1
- data/lib/eager_eye/detectors/loop_association.rb +48 -56
- data/lib/eager_eye/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e198460506f7e19906eeed320e9f9f11987ca1f4894b4b205a56d58799f3781
|
|
4
|
+
data.tar.gz: e61be4439b88d9731678fae64cb530390c7d2ab06dede3bbef570f8923c5542c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b32e0853b58fd412511418aedf8ce2d452d0f5a20b20e54eb3c2bb260c7233af7f7bbbe465f7efd7b511114152ffb72b381f54b88bd12c67463a63b547f6ff91
|
|
7
|
+
data.tar.gz: bf371dcf0600ec9f57614eae0d2d52fee4a904f3be37bd5d2f8a0e8d7066135d78b86d6a914e90f61fbc26afcee09003f886c8415ce2d84ce3d35799825975ee
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.5] - 2025-12-21
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed false positive N+1 warnings when iterating over a single record's associations
|
|
15
|
+
- Now correctly skips warnings for patterns like `User.find(id).posts.each { |p| p.comments }`
|
|
16
|
+
- Supports `find`, `find_by`, `find_by!`, `first`, `last`, `take`, `second`, `third`, etc.
|
|
17
|
+
- Works with both inline chains and variable assignments
|
|
18
|
+
|
|
10
19
|
## [1.0.4] - 2025-12-21
|
|
11
20
|
|
|
12
21
|
### Fixed
|
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)
|
|
@@ -114,6 +114,12 @@ end
|
|
|
114
114
|
# Also works with preload and eager_load
|
|
115
115
|
posts = Post.preload(:comments)
|
|
116
116
|
posts.each { |post| post.comments.size } # No warning
|
|
117
|
+
|
|
118
|
+
# Single record context - no N+1 possible (also detected correctly!)
|
|
119
|
+
@user = User.find(params[:id])
|
|
120
|
+
@user.posts.each do |post|
|
|
121
|
+
post.comments # No warning - single user, no N+1
|
|
122
|
+
end
|
|
117
123
|
```
|
|
118
124
|
|
|
119
125
|
### 2. Serializer Nesting (N+1 in serializers)
|
|
@@ -5,22 +5,19 @@ module EagerEye
|
|
|
5
5
|
class LoopAssociation < Base
|
|
6
6
|
ITERATION_METHODS = %i[each map collect select find find_all reject filter filter_map flat_map].freeze
|
|
7
7
|
PRELOAD_METHODS = %i[includes preload eager_load].freeze
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
projects tasks items orders products accounts profiles settings
|
|
22
|
-
images avatars photos attachments documents
|
|
23
|
-
].freeze
|
|
8
|
+
# Methods that return a single record (not a collection)
|
|
9
|
+
SINGLE_RECORD_METHODS = %i[find find_by find_by! first first! last last! take take! second third fourth fifth
|
|
10
|
+
forty_two sole find_sole_by].freeze
|
|
11
|
+
|
|
12
|
+
# Common association names (belongs_to = singular, has_many = plural)
|
|
13
|
+
ASSOCIATION_NAMES = Set.new(%w[
|
|
14
|
+
author user owner creator admin member customer client post article comment category tag
|
|
15
|
+
parent company organization project task item order product account profile setting image
|
|
16
|
+
avatar photo attachment document authors users owners creators admins members customers
|
|
17
|
+
clients posts articles comments categories tags children companies organizations projects
|
|
18
|
+
tasks items orders products accounts profiles settings images avatars photos attachments
|
|
19
|
+
documents
|
|
20
|
+
]).freeze
|
|
24
21
|
|
|
25
22
|
# Methods that should NOT be treated as associations
|
|
26
23
|
EXCLUDED_METHODS = %i[
|
|
@@ -38,9 +35,7 @@ module EagerEye
|
|
|
38
35
|
return [] unless ast
|
|
39
36
|
|
|
40
37
|
issues = []
|
|
41
|
-
|
|
42
|
-
# Build a map of variable names to their preloaded associations
|
|
43
|
-
@variable_preloads = build_variable_preloads_map(ast)
|
|
38
|
+
build_variable_maps(ast)
|
|
44
39
|
|
|
45
40
|
traverse_ast(ast) do |node|
|
|
46
41
|
next unless iteration_block?(node)
|
|
@@ -51,15 +46,13 @@ module EagerEye
|
|
|
51
46
|
block_body = node.children[2]
|
|
52
47
|
next unless block_body
|
|
53
48
|
|
|
54
|
-
# Check if the collection already has includes (both chained and from variable assignment)
|
|
55
49
|
collection_node = node.children[0]
|
|
56
|
-
|
|
50
|
+
next if single_record_iteration?(collection_node)
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
included_associations.merge(variable_preloads)
|
|
52
|
+
included = extract_included_associations(collection_node)
|
|
53
|
+
included.merge(extract_variable_preloads(collection_node))
|
|
61
54
|
|
|
62
|
-
find_association_calls(block_body, block_var, file_path, issues,
|
|
55
|
+
find_association_calls(block_body, block_var, file_path, issues, included)
|
|
63
56
|
end
|
|
64
57
|
|
|
65
58
|
issues
|
|
@@ -104,38 +97,28 @@ module EagerEye
|
|
|
104
97
|
included
|
|
105
98
|
end
|
|
106
99
|
|
|
107
|
-
def
|
|
108
|
-
|
|
100
|
+
def build_variable_maps(ast)
|
|
101
|
+
@variable_preloads = {}
|
|
102
|
+
@single_record_variables = Set.new
|
|
109
103
|
|
|
110
104
|
traverse_ast(ast) do |node|
|
|
111
|
-
|
|
112
|
-
when :lvasgn
|
|
113
|
-
record_variable_preloads(preloads_map, :lvar, node)
|
|
114
|
-
when :ivasgn
|
|
115
|
-
record_variable_preloads(preloads_map, :ivar, node)
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
preloads_map
|
|
120
|
-
end
|
|
105
|
+
next unless %i[lvasgn ivasgn].include?(node.type)
|
|
121
106
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
107
|
+
var_type = node.type == :lvasgn ? :lvar : :ivar
|
|
108
|
+
var_name = node.children[0]
|
|
109
|
+
value_node = node.children[1]
|
|
110
|
+
next unless value_node
|
|
126
111
|
|
|
127
|
-
|
|
128
|
-
|
|
112
|
+
key = [var_type, var_name]
|
|
113
|
+
preloaded = extract_included_associations(value_node)
|
|
114
|
+
@variable_preloads[key] = preloaded unless preloaded.empty?
|
|
115
|
+
@single_record_variables.add(key) if single_record_query?(value_node)
|
|
116
|
+
end
|
|
129
117
|
end
|
|
130
118
|
|
|
131
|
-
def extract_variable_preloads(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
key = variable_key_for_node(collection_node)
|
|
136
|
-
merge_preloads_for_key(preloads, key) if key
|
|
137
|
-
|
|
138
|
-
preloads
|
|
119
|
+
def extract_variable_preloads(node)
|
|
120
|
+
key = variable_key_for_node(node)
|
|
121
|
+
(key && @variable_preloads&.[](key)) || Set.new
|
|
139
122
|
end
|
|
140
123
|
|
|
141
124
|
def variable_key_for_node(node)
|
|
@@ -146,8 +129,19 @@ module EagerEye
|
|
|
146
129
|
end
|
|
147
130
|
end
|
|
148
131
|
|
|
149
|
-
def
|
|
150
|
-
|
|
132
|
+
def single_record_query?(node)
|
|
133
|
+
current = node
|
|
134
|
+
while current&.type == :send && !SINGLE_RECORD_METHODS.include?(current.children[1])
|
|
135
|
+
current = current.children[0]
|
|
136
|
+
end
|
|
137
|
+
current&.type == :send && SINGLE_RECORD_METHODS.include?(current.children[1])
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def single_record_iteration?(node)
|
|
141
|
+
return false unless node&.type == :send && (receiver = node.children[0])
|
|
142
|
+
|
|
143
|
+
key = variable_key_for_node(receiver)
|
|
144
|
+
(key && @single_record_variables&.include?(key)) || single_record_query?(receiver)
|
|
151
145
|
end
|
|
152
146
|
|
|
153
147
|
def extract_includes_from_method(method_node, included_set)
|
|
@@ -211,9 +205,7 @@ module EagerEye
|
|
|
211
205
|
def likely_association?(method_name)
|
|
212
206
|
return false if EXCLUDED_METHODS.include?(method_name)
|
|
213
207
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
SINGULAR_ASSOCIATIONS.include?(name) || PLURAL_ASSOCIATIONS.include?(name)
|
|
208
|
+
ASSOCIATION_NAMES.include?(method_name.to_s)
|
|
217
209
|
end
|
|
218
210
|
end
|
|
219
211
|
end
|
data/lib/eager_eye/version.rb
CHANGED