eager_eye 1.0.10 → 1.1.0

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: 27aed443ecc5e83aeb4fe0baa9aa9ccbcec462df45767c8294d867562c9066b5
4
- data.tar.gz: f2a9f245154a8fe043d9a9f0f68edaf940a5c3f8e2bd3f0255962603ef48d4d3
3
+ metadata.gz: 03025eeca47525318e9dbd1ffe0046bcd6647c16bc5696a1fd0b7906906ebc9d
4
+ data.tar.gz: c9fa809e3bcc95a6ea20d6f9ef18ca45241ad96e325fef76b0aa90e79b19e3a3
5
5
  SHA512:
6
- metadata.gz: f6f769f5d65827fd4059671674f8947f172b1ef9ff72a82375c25ac3e31c35c81099384e79de97036b8c867613343fd2e078f7c23f59fd68bf5daac8cd8a8a19
7
- data.tar.gz: f6c0743373d769ec6d147ccb1045b6a667d6015551a705b31eaefe6fc511568ecf06683cb99d7034f2d59cd83997732096d8e7c12b732b42c6ee460b9d152b5c
6
+ metadata.gz: c3faea6fb82cda3d118584970dba7bb88fa2fa1024dd6906d03f89699ff93b5f4d2a487b6c598eb9ee6bc18ae222b308f959d3cefbaed7d54c808aba93bc2fb2
7
+ data.tar.gz: 355d9b1c678b755ff679492183e7046b3edb511bff6e8fe3ff0990f091274917877cf8495984cb8315fe1096b0308a3528aff8c56fc6f1ed22270aa4257b0d03
data/.rubocop.yml CHANGED
@@ -19,6 +19,9 @@ Metrics/BlockLength:
19
19
  - "*.gemspec"
20
20
  - "lib/eager_eye/railtie.rb"
21
21
 
22
+ Metrics/ClassLength:
23
+ Max: 165
24
+
22
25
  Metrics/ParameterLists:
23
26
  Max: 6
24
27
 
@@ -28,8 +31,5 @@ Metrics/AbcSize:
28
31
  Metrics/MethodLength:
29
32
  Max: 30
30
33
 
31
- Metrics/ClassLength:
32
- Max: 150
33
-
34
34
  Layout/FirstArrayElementIndentation:
35
35
  EnforcedStyle: consistent
data/CHANGELOG.md CHANGED
@@ -7,21 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [1.0.10] - 2025-12-26
10
+ ## [1.1.0] - 2025-12-28
11
11
 
12
- ### Changed
12
+ ### Added
13
13
 
14
- - **PluckToArray Severity Levels** - Differentiate between scoped and unscoped pluck usage
15
- - Scoped `.pluck(:id)` (e.g., `User.active.pluck(:id)`) → **Warning** (default)
16
- - Unscoped `.all.pluck(:id)` **Error** (loads entire table into memory)
17
- - Small arrays with scoped pluck may be acceptable - users can suppress with comments
14
+ - **Association Scope Preloading Detection** - LoopAssociation now recognizes associations with built-in preloading
15
+ - Detects `has_many :posts, -> { includes(:comments) }` patterns
16
+ - Recognizes scope-defined preloads to reduce false positives
17
+ - Parses model files to extract association definitions and preload scopes
18
18
 
19
- ### Added
19
+ ## [1.0.10] - 2025-12-27
20
+
21
+ ### Changed
20
22
 
21
- - **Improved PluckToArray Detection** - Now detects and prioritizes `.all.pluck` patterns
22
- - Tracks `.all.pluck(:id)` as critical issues (error severity)
23
- - Detects both variable assignments and inline usage
24
- - Better suggestions for high-priority cases
23
+ - **PluckToArray Severity Levels** - Scoped pluck is warning, `.all.pluck` is error
24
+ - Scoped `.pluck(:id)` **Warning** (acceptable for small arrays)
25
+ - Unscoped `.all.pluck(:id)` **Error** (loads entire table)
26
+ - Improved detection and suggestions for critical patterns
25
27
 
26
28
  ## [1.0.9] - 2025-12-26
27
29
 
data/CONTRIBUTING.md CHANGED
@@ -74,7 +74,7 @@ end
74
74
  ## Code Style
75
75
 
76
76
  - Follow RuboCop rules (run `bundle exec rubocop`)
77
- - Maintain 90%+ test coverage
77
+ - Maintain 95%+ test coverage
78
78
  - Document public methods
79
79
  - Keep methods small and focused
80
80
 
data/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  <p align="center">
12
12
  <a href="https://github.com/hamzagedikkaya/eager_eye/actions/workflows/main.yml"><img src="https://github.com/hamzagedikkaya/eager_eye/actions/workflows/main.yml/badge.svg" alt="CI"></a>
13
- <a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.0.10-red.svg" alt="Gem Version"></a>
13
+ <a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.1.0-red.svg" alt="Gem Version"></a>
14
14
  <a href="https://github.com/hamzagedikkaya/eager_eye"><img src="https://img.shields.io/badge/coverage-95%25-brightgreen.svg" alt="Coverage"></a>
15
15
  <a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%3E%3D%203.1-ruby.svg" alt="Ruby"></a>
16
16
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
@@ -153,6 +153,17 @@ posts.each { |post| post.comments.size } # No warning
153
153
  @user.posts.each do |post|
154
154
  post.comments # No warning - single user, no N+1
155
155
  end
156
+
157
+ # Scope-defined preloads are recognized (v1.1.0+)
158
+ # In Post model:
159
+ class Post < ApplicationRecord
160
+ has_many :comments, -> { includes(:author) }
161
+ end
162
+
163
+ # In controller - EagerEye recognizes comments are already preloaded via scope!
164
+ posts.each do |post|
165
+ post.comments.map(&:author) # No warning - preloaded via scope
166
+ end
156
167
  ```
157
168
 
158
169
  ### 2. Serializer Nesting (N+1 in serializers)
@@ -313,40 +324,20 @@ Detects when `.pluck(:id)` or `.map(&:id)` results are used in `where` clauses i
313
324
 
314
325
  ```ruby
315
326
  # Bad - Two queries + memory overhead
316
- user_ids = User.active.pluck(:id) # Query 1: SELECT id FROM users
317
- Post.where(user_id: user_ids) # Query 2: SELECT * FROM posts WHERE user_id IN (1,2,3...)
318
- # Also holds potentially thousands of IDs in memory
319
-
320
- # Worse - Loads entire table into memory! (REPORTED AS ERROR)
321
- user_ids = User.all.pluck(:id) # Query 1: SELECT id FROM users -- ENTIRE TABLE!
322
- Post.where(user_id: user_ids) # Query 2: SELECT * FROM posts WHERE user_id IN (...)
327
+ user_ids = User.active.pluck(:id)
328
+ Post.where(user_id: user_ids) # ⚠️ Warning
323
329
 
324
- # Bad - Same problem with map
325
- user_ids = users.map(&:id)
330
+ # Worse - Loads entire table! 🔴 Error
331
+ user_ids = User.all.pluck(:id)
326
332
  Post.where(user_id: user_ids)
327
333
 
328
- # Good - Single subquery, no memory overhead
334
+ # Good - Single subquery
329
335
  Post.where(user_id: User.active.select(:id))
330
- # Single query: SELECT * FROM posts WHERE user_id IN (SELECT id FROM users WHERE active = true)
331
336
  ```
332
337
 
333
- **Severity Levels:**
334
-
335
- - ⚠️ **Warning (default)** - Scoped `.pluck(:id)` (e.g., `User.active.pluck(:id)`)
336
- - Two queries and memory overhead with moderately-sized arrays
337
- - Small arrays may be acceptable
338
-
339
- - 🔴 **Error** - Unscoped `.all.pluck(:id)` (e.g., `User.all.pluck(:id)`)
340
- - Loads entire table into memory
341
- - Highly inefficient and should always be refactored
342
-
343
- **Performance comparison with 10,000 users:**
344
-
345
- | Approach | Queries | Memory | Time |
346
- |----------|---------|--------|------|
347
- | `pluck` + `where` | 2 | ~80KB for IDs | ~45ms |
348
- | `.all.pluck` + `where` | 2 | ~40KB+ for all IDs | ~100ms+ |
349
- | `select` subquery | 1 | None | ~20ms |
338
+ **Severity:**
339
+ - ⚠️ **Warning** - Scoped `.pluck(:id)` (two queries, memory overhead)
340
+ - 🔴 **Error** - Unscoped `.all.pluck(:id)` (loads entire table)
350
341
 
351
342
  ## Inline Suppression
352
343
 
@@ -14,24 +14,49 @@ module EagerEye
14
14
  pluck_to_array: Detectors::PluckToArray
15
15
  }.freeze
16
16
 
17
- attr_reader :paths, :issues
17
+ attr_reader :paths, :issues, :association_preloads
18
18
 
19
19
  def initialize(paths: nil)
20
20
  @paths = Array(paths || EagerEye.configuration.app_path)
21
21
  @issues = []
22
+ @association_preloads = {}
22
23
  end
23
24
 
24
25
  def run
25
26
  @issues = []
27
+ collect_association_preloads
28
+ analyze_files
29
+ @issues
30
+ end
31
+
32
+ private
33
+
34
+ def collect_association_preloads
35
+ model_files.each do |file_path|
36
+ source = File.read(file_path)
37
+ ast = parse_source(source)
38
+ next unless ast
26
39
 
27
- ruby_files.each do |file_path|
28
- analyze_file(file_path)
40
+ model_name = extract_model_name(file_path)
41
+ parser = AssociationParser.new
42
+ parser.parse_model(ast, model_name)
43
+ @association_preloads.merge!(parser.preloaded_associations)
29
44
  end
45
+ rescue StandardError
46
+ # Silently skip errors in association parsing
47
+ end
30
48
 
31
- @issues
49
+ def model_files
50
+ Dir.glob(File.join(@paths[0], "models", "**", "*.rb"))
32
51
  end
33
52
 
34
- private
53
+ def extract_model_name(file_path)
54
+ File.basename(file_path, ".rb").camelize
55
+ end
56
+
57
+ def analyze_files
58
+ ruby_files.each { |file_path| analyze_file(file_path) }
59
+ end
35
60
 
36
61
  def ruby_files
37
62
  all_files = paths.flat_map do |path|
@@ -61,7 +86,10 @@ module EagerEye
61
86
  comment_parser = CommentParser.new(source)
62
87
 
63
88
  enabled_detectors.each do |detector|
64
- file_issues = detector.detect(ast, file_path)
89
+ args = [ast, file_path]
90
+ args << @association_preloads if detector.is_a?(Detectors::LoopAssociation)
91
+
92
+ file_issues = detector.detect(*args)
65
93
 
66
94
  # Filter suppressed issues
67
95
  file_issues.reject! do |issue|
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ class AssociationParser
5
+ ASSOCIATION_METHODS = %i[has_many has_one belongs_to has_and_belongs_to_many].freeze
6
+
7
+ attr_reader :preloaded_associations
8
+
9
+ def initialize
10
+ @preloaded_associations = {}
11
+ end
12
+
13
+ def parse_model(ast, model_name)
14
+ return unless ast
15
+
16
+ traverse(ast, model_name)
17
+ end
18
+
19
+ private
20
+
21
+ def traverse(node, model_name)
22
+ return unless node.is_a?(Parser::AST::Node)
23
+
24
+ check_association_definition(node, model_name)
25
+ node.children.each { |child| traverse(child, model_name) }
26
+ end
27
+
28
+ def check_association_definition(node, model_name)
29
+ return unless node.type == :send
30
+ return unless node.children[0].nil? # receiver is nil (class context)
31
+
32
+ method_name = node.children[1]
33
+ return unless ASSOCIATION_METHODS.include?(method_name)
34
+
35
+ association_name = extract_association_name(node)
36
+ return unless association_name
37
+
38
+ preloaded = extract_preloaded_associations(node)
39
+ return if preloaded.empty?
40
+
41
+ @preloaded_associations["#{model_name}##{association_name}"] = preloaded
42
+ end
43
+
44
+ def extract_association_name(node)
45
+ args = node.children[2..]
46
+ return nil if args.empty?
47
+
48
+ first_arg = args[0]
49
+ return nil unless first_arg&.type == :sym
50
+
51
+ first_arg.children[0]
52
+ end
53
+
54
+ def extract_preloaded_associations(node)
55
+ preloaded = Set.new
56
+ args = node.children[2..]
57
+ return preloaded if args.empty?
58
+
59
+ # Check for block with includes/preload/eager_load
60
+ block_node = args.find { |arg| arg&.type == :block }
61
+ return preloaded unless block_node
62
+
63
+ extract_from_block(block_node, preloaded)
64
+ preloaded
65
+ end
66
+
67
+ def extract_from_block(block_node, preloaded)
68
+ block_body = block_node.children[2]
69
+ traverse_for_preloads(block_body, preloaded)
70
+ end
71
+
72
+ def traverse_for_preloads(node, preloaded)
73
+ return unless node.is_a?(Parser::AST::Node)
74
+
75
+ extract_includes_from_method(node, preloaded) if preload_call?(node)
76
+
77
+ node.children.each { |child| traverse_for_preloads(child, preloaded) }
78
+ end
79
+
80
+ def preload_call?(node)
81
+ return false unless node.type == :send
82
+
83
+ method = node.children[1]
84
+ %i[includes preload eager_load].include?(method)
85
+ end
86
+
87
+ def extract_includes_from_method(node, included)
88
+ args = node.children[2..]
89
+ return if args.empty?
90
+
91
+ args.each { |arg| add_included_sym(arg, included) }
92
+ end
93
+
94
+ def add_included_sym(arg, included)
95
+ case arg&.type
96
+ when :sym
97
+ included << arg.children[0]
98
+ when :hash
99
+ arg.children.each { |pair| extract_sym_from_pair(pair, included) }
100
+ end
101
+ end
102
+
103
+ def extract_sym_from_pair(pair, included)
104
+ return unless pair.type == :pair
105
+
106
+ key = pair.children[0]
107
+ included << key.children[0] if key&.type == :sym
108
+ end
109
+ end
110
+ end
@@ -52,6 +52,34 @@ module EagerEye
52
52
  rescue Parser::SyntaxError
53
53
  nil
54
54
  end
55
+
56
+ def extract_method_args(node)
57
+ return [] unless node&.type == :send
58
+
59
+ node.children[2..]
60
+ end
61
+
62
+ def extract_symbols_from_args(args)
63
+ symbols = Set.new
64
+ return symbols if args.empty?
65
+
66
+ args.each do |arg|
67
+ case arg&.type
68
+ when :sym
69
+ symbols.add(arg.children[0])
70
+ when :hash
71
+ extract_symbols_from_hash(arg, symbols)
72
+ end
73
+ end
74
+ symbols
75
+ end
76
+
77
+ def extract_symbols_from_hash(hash_node, symbols)
78
+ hash_node.children.each do |pair|
79
+ key = pair.children[0]
80
+ symbols.add(key.children[0]) if key&.type == :sym
81
+ end
82
+ end
55
83
  end
56
84
  end
57
85
  end
@@ -31,10 +31,11 @@ module EagerEye
31
31
  :loop_association
32
32
  end
33
33
 
34
- def detect(ast, file_path)
34
+ def detect(ast, file_path, association_preloads = {})
35
35
  return [] unless ast
36
36
 
37
37
  issues = []
38
+ @association_preloads = association_preloads
38
39
  build_variable_maps(ast)
39
40
 
40
41
  traverse_ast(ast) do |node|
@@ -51,6 +52,8 @@ module EagerEye
51
52
 
52
53
  included = extract_included_associations(collection_node)
53
54
  included.merge(extract_variable_preloads(collection_node))
55
+ model_name = infer_model_name_from_collection(collection_node)
56
+ included.merge(get_association_preloads(model_name))
54
57
 
55
58
  find_association_calls(block_body, block_var, file_path, issues, included)
56
59
  end
@@ -60,6 +63,27 @@ module EagerEye
60
63
 
61
64
  private
62
65
 
66
+ def get_association_preloads(model_name)
67
+ key = "#{model_name}#*"
68
+ preloaded = Set.new
69
+ @association_preloads&.each do |assoc_key, assocs|
70
+ preloaded.merge(assocs) if assoc_key.start_with?(key)
71
+ end
72
+ preloaded
73
+ end
74
+
75
+ def infer_model_name_from_collection(node)
76
+ # Infer model name from collection (posts -> Post, users -> User)
77
+ return nil unless node&.type == :send
78
+
79
+ # Handle: Model.includes, @var.method calls
80
+ receiver = node.children[0]
81
+ case receiver&.type
82
+ when :const
83
+ receiver.children[1].to_s
84
+ end
85
+ end
86
+
63
87
  def iteration_block?(node)
64
88
  return false unless node.type == :block
65
89
 
@@ -71,14 +95,9 @@ module EagerEye
71
95
  end
72
96
 
73
97
  def extract_block_variable(block_node)
74
- args_node = block_node.children[1]
75
- return nil unless args_node&.type == :args
76
- return nil if args_node.children.empty?
77
-
78
- first_arg = args_node.children[0]
79
- return nil unless first_arg&.type == :arg
80
-
81
- first_arg.children[0]
98
+ args = block_node&.children&.fetch(1, nil)
99
+ first_child = args&.children&.first
100
+ first_child&.type == :arg ? first_child.children[0] : nil
82
101
  end
83
102
 
84
103
  def extract_included_associations(collection_node)
@@ -101,19 +120,21 @@ module EagerEye
101
120
  @variable_preloads = {}
102
121
  @single_record_variables = Set.new
103
122
 
104
- traverse_ast(ast) do |node|
105
- next unless %i[lvasgn ivasgn].include?(node.type)
123
+ traverse_ast(ast) { |node| process_variable_assignment(node) }
124
+ end
106
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
+ def process_variable_assignment(node)
127
+ return unless %i[lvasgn ivasgn].include?(node.type)
111
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
+ var_type = node.type == :lvasgn ? :lvar : :ivar
130
+ var_name = node.children[0]
131
+ value_node = node.children[1]
132
+ return unless value_node
133
+
134
+ key = [var_type, var_name]
135
+ preloaded = extract_included_associations(value_node)
136
+ @variable_preloads[key] = preloaded unless preloaded.empty?
137
+ @single_record_variables.add(key) if single_record_query?(value_node)
117
138
  end
118
139
 
119
140
  def extract_variable_preloads(node)
@@ -130,11 +151,18 @@ module EagerEye
130
151
  end
131
152
 
132
153
  def single_record_query?(node)
154
+ last_send = find_last_send_method(node)
155
+ last_send && SINGLE_RECORD_METHODS.include?(last_send)
156
+ end
157
+
158
+ def find_last_send_method(node)
133
159
  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])
160
+ current = current.children[0] while current&.type == :send && !single_record_method?(current)
161
+ current&.type == :send ? current.children[1] : nil
162
+ end
163
+
164
+ def single_record_method?(node)
165
+ SINGLE_RECORD_METHODS.include?(node.children[1])
138
166
  end
139
167
 
140
168
  def single_record_iteration?(node)
@@ -145,67 +173,50 @@ module EagerEye
145
173
  end
146
174
 
147
175
  def extract_includes_from_method(method_node, included_set)
148
- args = method_node.children[2..]
149
- args&.each do |arg|
150
- case arg&.type
151
- when :sym
152
- # includes(:product)
153
- included_set.add(arg.children[0])
154
- when :hash
155
- # includes(product: :manufacturer)
156
- extract_from_hash(arg, included_set)
157
- end
158
- end
176
+ args = extract_method_args(method_node)
177
+ included_set.merge(extract_symbols_from_args(args))
159
178
  end
160
179
 
161
180
  def extract_from_hash(hash_node, included_set)
162
- hash_node.children.each do |pair|
163
- key = pair.children[0]
164
- included_set.add(key.children[0]) if key&.type == :sym
165
- end
181
+ extract_symbols_from_hash(hash_node, included_set)
166
182
  end
167
183
 
168
184
  def find_association_calls(node, block_var, file_path, issues, included_associations = Set.new)
169
- reported_associations = Set.new
170
-
185
+ reported = Set.new
171
186
  traverse_ast(node) do |child|
172
- next unless child.type == :send
187
+ next unless should_report_issue?(child, block_var, reported, included_associations)
173
188
 
174
- receiver = child.children[0]
175
- method_name = child.children[1]
176
-
177
- # Only detect direct calls on block variable (post.author, not post.author.name)
178
- next unless direct_call_on_block_var?(receiver, block_var)
179
- next unless likely_association?(method_name)
180
-
181
- # Skip if association is already included
182
- next if included_associations.include?(method_name)
183
-
184
- # Avoid duplicate reports for same association on same line
185
- report_key = "#{child.loc.line}:#{method_name}"
186
- next if reported_associations.include?(report_key)
187
-
188
- reported_associations << report_key
189
-
190
- issues << create_issue(
191
- file_path: file_path,
192
- line_number: child.loc.line,
193
- message: "Potential N+1 query: `#{block_var}.#{method_name}` called inside iteration",
194
- suggestion: "Consider using `includes(:#{method_name})` on the collection before iterating"
195
- )
189
+ add_n_plus_one_issue(child, block_var, file_path, issues, reported)
196
190
  end
197
191
  end
198
192
 
199
- def direct_call_on_block_var?(receiver, block_var)
200
- return false unless receiver
193
+ def should_report_issue?(child, block_var, reported, included)
194
+ return false unless child.type == :send
195
+
196
+ receiver = child.children[0]
197
+ method = child.children[1]
198
+ return false unless receiver&.type == :lvar && receiver.children[0] == block_var
199
+ return false if excluded?(method, included)
201
200
 
202
- receiver.type == :lvar && receiver.children[0] == block_var
201
+ key = "#{child.loc.line}:#{method}"
202
+ !reported.include?(key) && reported.add(key)
203
203
  end
204
204
 
205
- def likely_association?(method_name)
206
- return false if EXCLUDED_METHODS.include?(method_name)
205
+ def excluded?(method, included)
206
+ EXCLUDED_METHODS.include?(method) ||
207
+ !ASSOCIATION_NAMES.include?(method.to_s) ||
208
+ included.include?(method)
209
+ end
207
210
 
208
- ASSOCIATION_NAMES.include?(method_name.to_s)
211
+ def add_n_plus_one_issue(node, block_var, file_path, issues, reported)
212
+ method = node.children[1]
213
+ reported << "#{node.loc.line}:#{method}"
214
+ issues << create_issue(
215
+ file_path: file_path,
216
+ line_number: node.loc.line,
217
+ message: "Potential N+1 query: `#{block_var}.#{method}` called inside loop",
218
+ suggestion: "Use `includes(:#{method})` before iterating"
219
+ )
209
220
  end
210
221
  end
211
222
  end
@@ -149,8 +149,8 @@ module EagerEye
149
149
  @issues << create_issue(
150
150
  file_path: @file_path,
151
151
  line_number: node.loc.line,
152
- message: "Using plucked/mapped array in `where` causes two queries and holds IDs in memory",
153
- suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.condition.select(:id))`",
152
+ message: "Using plucked array in `where` causes two queries and memory overhead",
153
+ suggestion: "Use `.select(:id)` subquery instead: `Model.where(col: OtherModel.select(:id))`",
154
154
  severity: :warning
155
155
  )
156
156
  end
@@ -159,8 +159,8 @@ module EagerEye
159
159
  @issues << create_issue(
160
160
  file_path: @file_path,
161
161
  line_number: node.loc.line,
162
- message: "Using `.all.pluck(:id)` in `where` loads entire table into memory and is highly inefficient",
163
- suggestion: "Use `.select(:id)` subquery instead: `Model.where(col: OtherModel.select(:id))`",
162
+ message: "Using `.all.pluck(:id)` loads entire table into memory - highly inefficient",
163
+ suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.select(:id))`",
164
164
  severity: :error
165
165
  )
166
166
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.0.10"
4
+ VERSION = "1.1.0"
5
5
  end
data/lib/eager_eye.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  require_relative "eager_eye/version"
4
4
  require_relative "eager_eye/configuration"
5
5
  require_relative "eager_eye/issue"
6
+ require_relative "eager_eye/association_parser"
6
7
  require_relative "eager_eye/detectors/base"
7
8
  require_relative "eager_eye/detectors/loop_association"
8
9
  require_relative "eager_eye/detectors/serializer_nesting"
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.10
4
+ version: 1.1.0
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-26 00:00:00.000000000 Z
11
+ date: 2025-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast
@@ -59,6 +59,7 @@ files:
59
59
  - exe/eager_eye
60
60
  - lib/eager_eye.rb
61
61
  - lib/eager_eye/analyzer.rb
62
+ - lib/eager_eye/association_parser.rb
62
63
  - lib/eager_eye/auto_fixer.rb
63
64
  - lib/eager_eye/cli.rb
64
65
  - lib/eager_eye/comment_parser.rb