eager_eye 1.0.3 → 1.0.4

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: ecc47488d2632329805448f55dfafc1f6670efd02b04a3dd0464c22ef9a3a4ba
4
- data.tar.gz: b37b1be91854abeea6a11d33f28df1ada2a3fb0a427b28a892cb1249c22a7e67
3
+ metadata.gz: f7ebed3daa9046b0b6bbda1cbb7a9cce182af014ac31334973b4d98a5a1059bc
4
+ data.tar.gz: 61e220f6737089f52ab495d422cac8b91bf4720d6551c6a42abad7801c7d3c79
5
5
  SHA512:
6
- metadata.gz: 19adae5e3c000cfa658f5d2615da31c9a702ff031766240b86968d69165445bbbc7b630b5a1596822eaa9683c23db84379127d0c83ed4a59366475b3585972b7
7
- data.tar.gz: a87d19f36dcaf9bd7a360b31051ed32da241e7c0b867815e8e17962f4b29bdd43d6983d84981e54a350ba4d415a076a84a8b1abc6852311277f25398c51d6a1a
6
+ metadata.gz: 198f7d4c064f940a73e2ac41f401869274482f1388f9031f405c89cdeddd3e8e1cf829f8d107c7c3fa1a88fa1a5de07fe44a3d08acf4cb269aa063ebfafae729
7
+ data.tar.gz: 814ce257bfc024ca1d83b61cddd3df9b56ed877dfb40d9eaf5564c951bebf11f4351d0c4965ff1510509f62aad3f97af8f0e4933b5a23b4daf048b186d5c748b
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.4] - 2025-12-21
11
+
12
+ ### Fixed
13
+
14
+ - Fixed false positive N+1 warnings when associations are preloaded via `includes`, `preload`, or `eager_load` on a separate line
15
+ - Now correctly tracks variable assignments with preload methods (e.g., `posts = Post.includes(:author)`)
16
+ - Supports both local variables and instance variables
17
+ - Works with all three preload methods: `includes`, `preload`, `eager_load`
18
+
10
19
  ## [1.0.3] - 2025-12-20
11
20
 
12
21
  ### Fixed
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.3-red.svg)](https://rubygems.org/gems/eager_eye)
4
+ [![Gem Version](https://img.shields.io/badge/gem-v1.0.4-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)
@@ -99,11 +99,21 @@ 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
107
117
  ```
108
118
 
109
119
  ### 2. Serializer Nesting (N+1 in serializers)
@@ -4,6 +4,7 @@ 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
+ PRELOAD_METHODS = %i[includes preload eager_load].freeze
7
8
 
8
9
  # Common singular association names (belongs_to pattern)
9
10
  SINGULAR_ASSOCIATIONS = %w[
@@ -23,12 +24,10 @@ module EagerEye
23
24
 
24
25
  # Methods that should NOT be treated as associations
25
26
  EXCLUDED_METHODS = %i[
26
- id to_s to_h to_a to_json to_xml inspect class object_id
27
- nil? blank? present? empty? any? none? size count length
28
- save save! update update! destroy destroy! delete delete!
29
- valid? invalid? errors new? persisted? changed? frozen?
30
- name title body content text description value key type status state
31
- created_at updated_at deleted_at
27
+ id to_s to_h to_a to_json to_xml inspect class object_id nil? blank? present? empty?
28
+ any? none? size count length save save! update update! destroy destroy! delete delete!
29
+ valid? invalid? errors new? persisted? changed? frozen? name title body content text
30
+ description value key type status state created_at updated_at deleted_at
32
31
  ].freeze
33
32
 
34
33
  def self.detector_name
@@ -40,6 +39,9 @@ module EagerEye
40
39
 
41
40
  issues = []
42
41
 
42
+ # Build a map of variable names to their preloaded associations
43
+ @variable_preloads = build_variable_preloads_map(ast)
44
+
43
45
  traverse_ast(ast) do |node|
44
46
  next unless iteration_block?(node)
45
47
 
@@ -49,10 +51,14 @@ module EagerEye
49
51
  block_body = node.children[2]
50
52
  next unless block_body
51
53
 
52
- # Check if the collection already has includes
54
+ # Check if the collection already has includes (both chained and from variable assignment)
53
55
  collection_node = node.children[0]
54
56
  included_associations = extract_included_associations(collection_node)
55
57
 
58
+ # Also check if the collection comes from a variable that was assigned with preloads
59
+ variable_preloads = extract_variable_preloads(collection_node)
60
+ included_associations.merge(variable_preloads)
61
+
56
62
  find_association_calls(block_body, block_var, file_path, issues, included_associations)
57
63
  end
58
64
 
@@ -86,11 +92,11 @@ module EagerEye
86
92
  included = Set.new
87
93
  return included unless collection_node&.type == :send
88
94
 
89
- # Traverse through chained method calls to find includes()
95
+ # Traverse through chained method calls to find includes/preload/eager_load
90
96
  current = collection_node
91
97
  while current&.type == :send
92
98
  method_name = current.children[1]
93
- extract_includes_from_method(current, included) if method_name == :includes
99
+ extract_includes_from_method(current, included) if PRELOAD_METHODS.include?(method_name)
94
100
 
95
101
  current = current.children[0]
96
102
  end
@@ -98,6 +104,52 @@ module EagerEye
98
104
  included
99
105
  end
100
106
 
107
+ def build_variable_preloads_map(ast)
108
+ preloads_map = {}
109
+
110
+ traverse_ast(ast) do |node|
111
+ case node.type
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
121
+
122
+ def record_variable_preloads(preloads_map, var_type, node)
123
+ var_name = node.children[0]
124
+ value_node = node.children[1]
125
+ return unless value_node
126
+
127
+ preloaded = extract_included_associations(value_node)
128
+ preloads_map[[var_type, var_name]] = preloaded unless preloaded.empty?
129
+ end
130
+
131
+ def extract_variable_preloads(collection_node)
132
+ preloads = Set.new
133
+ return preloads unless @variable_preloads
134
+
135
+ key = variable_key_for_node(collection_node)
136
+ merge_preloads_for_key(preloads, key) if key
137
+
138
+ preloads
139
+ end
140
+
141
+ def variable_key_for_node(node)
142
+ case node&.type
143
+ when :lvar then [:lvar, node.children[0]]
144
+ when :ivar then [:ivar, node.children[0]]
145
+ when :send then variable_key_for_node(node.children[0])
146
+ end
147
+ end
148
+
149
+ def merge_preloads_for_key(preloads, key)
150
+ preloads.merge(@variable_preloads[key]) if @variable_preloads[key]
151
+ end
152
+
101
153
  def extract_includes_from_method(method_node, included_set)
102
154
  args = method_node.children[2..]
103
155
  args&.each do |arg|
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.0.3"
4
+ VERSION = "1.0.4"
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.3
4
+ version: 1.0.4
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-20 00:00:00.000000000 Z
11
+ date: 2025-12-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast