eager_eye 1.0.3 → 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 +18 -0
- data/README.md +18 -2
- data/lib/eager_eye/detectors/loop_association.rb +74 -30
- 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: 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,24 @@ 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
|
+
|
|
19
|
+
## [1.0.4] - 2025-12-21
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed false positive N+1 warnings when associations are preloaded via `includes`, `preload`, or `eager_load` on a separate line
|
|
24
|
+
- Now correctly tracks variable assignments with preload methods (e.g., `posts = Post.includes(:author)`)
|
|
25
|
+
- Supports both local variables and instance variables
|
|
26
|
+
- Works with all three preload methods: `includes`, `preload`, `eager_load`
|
|
27
|
+
|
|
10
28
|
## [1.0.3] - 2025-12-20
|
|
11
29
|
|
|
12
30
|
### 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)
|
|
@@ -99,11 +99,27 @@ posts.each do |post|
|
|
|
99
99
|
post.comments.count # Another query for each post!
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# Good - Eager load associations
|
|
102
|
+
# Good - Eager load associations (chained)
|
|
103
103
|
posts.includes(:author, :comments).each do |post|
|
|
104
104
|
post.author.name # No additional query
|
|
105
105
|
post.comments.count # No additional query
|
|
106
106
|
end
|
|
107
|
+
|
|
108
|
+
# Good - Eager load on separate line (also detected correctly!)
|
|
109
|
+
@posts = Post.includes(:author)
|
|
110
|
+
@posts.each do |post|
|
|
111
|
+
post.author.name # No warning - EagerEye tracks the preload
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Also works with preload and eager_load
|
|
115
|
+
posts = Post.preload(:comments)
|
|
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
|
|
107
123
|
```
|
|
108
124
|
|
|
109
125
|
### 2. Serializer Nesting (N+1 in serializers)
|
|
@@ -4,31 +4,27 @@ module EagerEye
|
|
|
4
4
|
module Detectors
|
|
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
|
-
|
|
8
|
-
#
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
images avatars photos attachments documents
|
|
22
|
-
].freeze
|
|
7
|
+
PRELOAD_METHODS = %i[includes preload eager_load].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
|
|
23
21
|
|
|
24
22
|
# Methods that should NOT be treated as associations
|
|
25
23
|
EXCLUDED_METHODS = %i[
|
|
26
|
-
id to_s to_h to_a to_json to_xml inspect class object_id
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
name title body content text description value key type status state
|
|
31
|
-
created_at updated_at deleted_at
|
|
24
|
+
id to_s to_h to_a to_json to_xml inspect class object_id nil? blank? present? empty?
|
|
25
|
+
any? none? size count length save save! update update! destroy destroy! delete delete!
|
|
26
|
+
valid? invalid? errors new? persisted? changed? frozen? name title body content text
|
|
27
|
+
description value key type status state created_at updated_at deleted_at
|
|
32
28
|
].freeze
|
|
33
29
|
|
|
34
30
|
def self.detector_name
|
|
@@ -39,6 +35,7 @@ module EagerEye
|
|
|
39
35
|
return [] unless ast
|
|
40
36
|
|
|
41
37
|
issues = []
|
|
38
|
+
build_variable_maps(ast)
|
|
42
39
|
|
|
43
40
|
traverse_ast(ast) do |node|
|
|
44
41
|
next unless iteration_block?(node)
|
|
@@ -49,11 +46,13 @@ module EagerEye
|
|
|
49
46
|
block_body = node.children[2]
|
|
50
47
|
next unless block_body
|
|
51
48
|
|
|
52
|
-
# Check if the collection already has includes
|
|
53
49
|
collection_node = node.children[0]
|
|
54
|
-
|
|
50
|
+
next if single_record_iteration?(collection_node)
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
included = extract_included_associations(collection_node)
|
|
53
|
+
included.merge(extract_variable_preloads(collection_node))
|
|
54
|
+
|
|
55
|
+
find_association_calls(block_body, block_var, file_path, issues, included)
|
|
57
56
|
end
|
|
58
57
|
|
|
59
58
|
issues
|
|
@@ -86,11 +85,11 @@ module EagerEye
|
|
|
86
85
|
included = Set.new
|
|
87
86
|
return included unless collection_node&.type == :send
|
|
88
87
|
|
|
89
|
-
# Traverse through chained method calls to find includes
|
|
88
|
+
# Traverse through chained method calls to find includes/preload/eager_load
|
|
90
89
|
current = collection_node
|
|
91
90
|
while current&.type == :send
|
|
92
91
|
method_name = current.children[1]
|
|
93
|
-
extract_includes_from_method(current, included) if method_name
|
|
92
|
+
extract_includes_from_method(current, included) if PRELOAD_METHODS.include?(method_name)
|
|
94
93
|
|
|
95
94
|
current = current.children[0]
|
|
96
95
|
end
|
|
@@ -98,6 +97,53 @@ module EagerEye
|
|
|
98
97
|
included
|
|
99
98
|
end
|
|
100
99
|
|
|
100
|
+
def build_variable_maps(ast)
|
|
101
|
+
@variable_preloads = {}
|
|
102
|
+
@single_record_variables = Set.new
|
|
103
|
+
|
|
104
|
+
traverse_ast(ast) do |node|
|
|
105
|
+
next unless %i[lvasgn ivasgn].include?(node.type)
|
|
106
|
+
|
|
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
|
|
111
|
+
|
|
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
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def extract_variable_preloads(node)
|
|
120
|
+
key = variable_key_for_node(node)
|
|
121
|
+
(key && @variable_preloads&.[](key)) || Set.new
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def variable_key_for_node(node)
|
|
125
|
+
case node&.type
|
|
126
|
+
when :lvar then [:lvar, node.children[0]]
|
|
127
|
+
when :ivar then [:ivar, node.children[0]]
|
|
128
|
+
when :send then variable_key_for_node(node.children[0])
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
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)
|
|
145
|
+
end
|
|
146
|
+
|
|
101
147
|
def extract_includes_from_method(method_node, included_set)
|
|
102
148
|
args = method_node.children[2..]
|
|
103
149
|
args&.each do |arg|
|
|
@@ -159,9 +205,7 @@ module EagerEye
|
|
|
159
205
|
def likely_association?(method_name)
|
|
160
206
|
return false if EXCLUDED_METHODS.include?(method_name)
|
|
161
207
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
SINGULAR_ASSOCIATIONS.include?(name) || PLURAL_ASSOCIATIONS.include?(name)
|
|
208
|
+
ASSOCIATION_NAMES.include?(method_name.to_s)
|
|
165
209
|
end
|
|
166
210
|
end
|
|
167
211
|
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.5
|
|
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-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|