eager_eye 1.0.10 → 1.1.1

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.
@@ -3,27 +3,9 @@
3
3
  module EagerEye
4
4
  module Detectors
5
5
  class CustomMethodQuery < Base
6
- QUERY_METHODS = %i[
7
- where
8
- find_by
9
- find_by!
10
- exists?
11
- find
12
- first
13
- last
14
- take
15
- pluck
16
- ids
17
- count
18
- sum
19
- average
20
- minimum
21
- maximum
22
- ].freeze
23
-
24
- # Array-only methods that should not be flagged when collection is clearly an array
6
+ QUERY_METHODS = %i[where find_by find_by! exists? find first last take pluck ids count sum average minimum
7
+ maximum].freeze
25
8
  ARRAY_METHODS = %i[first last take].freeze
26
-
27
9
  ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map].freeze
28
10
 
29
11
  def self.detector_name
@@ -51,34 +33,22 @@ module EagerEye
51
33
 
52
34
  if iteration_block?(node)
53
35
  block_var = extract_block_variable(node)
54
- block_body = extract_block_body(node)
55
- collection = extract_collection(node)
56
- yield(block_body, block_var, collection) if block_var && block_body
57
- end
58
-
59
- node.children.each do |child|
60
- find_iteration_blocks(child, &block)
36
+ block_body = node.children[2]
37
+ yield(block_body, block_var, node.children[0]) if block_var && block_body
61
38
  end
39
+ node.children.each { |child| find_iteration_blocks(child, &block) }
62
40
  end
63
41
 
64
42
  def iteration_block?(node)
65
- return false unless node.type == :block
66
-
67
- send_node = node.children[0]
68
- return false unless send_node&.type == :send
69
-
70
- method_name = send_node.children[1]
71
- ITERATION_METHODS.include?(method_name)
43
+ node.type == :block && node.children[0]&.type == :send &&
44
+ ITERATION_METHODS.include?(node.children[0].children[1])
72
45
  end
73
46
 
74
47
  def check_block_for_query_methods(node, block_var, is_array_collection = false) # rubocop:disable Style/OptionalBooleanParameter
75
48
  return unless node.is_a?(Parser::AST::Node)
76
49
 
77
50
  add_issue(node) if query_chain_on_association?(node, block_var, is_array_collection)
78
-
79
- node.children.each do |child|
80
- check_block_for_query_methods(child, block_var, is_array_collection)
81
- end
51
+ node.children.each { |child| check_block_for_query_methods(child, block_var, is_array_collection) }
82
52
  end
83
53
 
84
54
  def query_chain_on_association?(node, block_var, is_array_collection = false) # rubocop:disable Style/OptionalBooleanParameter
@@ -86,35 +56,23 @@ module EagerEye
86
56
 
87
57
  method_name = node.children[1]
88
58
  return false unless QUERY_METHODS.include?(method_name)
59
+ return false if is_array_collection && ARRAY_METHODS.include?(method_name) &&
60
+ receiver_is_only_block_var?(node.children[0], block_var)
89
61
 
90
- # Skip array-only methods when collection is clearly an array (.map result)
91
- # AND the receiver is only the block variable (not chained)
92
- if is_array_collection && ARRAY_METHODS.include?(method_name) &&
93
- receiver_is_only_block_var?(node.children[0], block_var)
94
- return false
95
- end
96
-
97
- receiver = node.children[0]
98
- receiver_chain_starts_with?(receiver, block_var)
62
+ receiver_chain_starts_with?(node.children[0], block_var)
99
63
  end
100
64
 
101
65
  def receiver_is_only_block_var?(node, block_var)
102
- # Returns true only if receiver is EXACTLY the block variable, not a chain
103
- node.is_a?(Parser::AST::Node) &&
104
- node.type == :lvar &&
105
- node.children[0] == block_var
66
+ node.is_a?(Parser::AST::Node) && node.type == :lvar && node.children[0] == block_var
106
67
  end
107
68
 
108
69
  def receiver_chain_starts_with?(node, block_var)
109
70
  return false unless node.is_a?(Parser::AST::Node)
110
71
 
111
72
  case node.type
112
- when :lvar
113
- node.children[0] == block_var
114
- when :send
115
- receiver_chain_starts_with?(node.children[0], block_var)
116
- else
117
- false
73
+ when :lvar then node.children[0] == block_var
74
+ when :send then receiver_chain_starts_with?(node.children[0], block_var)
75
+ else false
118
76
  end
119
77
  end
120
78
 
@@ -123,37 +81,16 @@ module EagerEye
123
81
  return nil unless args_node&.type == :args
124
82
 
125
83
  first_arg = args_node.children[0]
126
- return nil unless first_arg&.type == :arg
127
-
128
- first_arg.children[0]
84
+ first_arg&.type == :arg ? first_arg.children[0] : nil
129
85
  end
130
86
 
131
- def extract_block_body(block_node)
132
- block_node.children[2]
133
- end
134
-
135
- def extract_collection(block_node)
136
- # Extract the collection being iterated on
137
- # For: collection.each { |item| ... }
138
- # Returns: the send node representing the collection method call
139
- block_node.children[0]
140
- end
141
-
142
- def collection_is_array?(collection_node)
143
- return false unless collection_node.is_a?(Parser::AST::Node)
87
+ def collection_is_array?(node)
88
+ return false unless node.is_a?(Parser::AST::Node)
144
89
 
145
- case collection_node.type
146
- when :array
147
- # Literal array: [1, 2, 3].each { |item| ... }
148
- true
149
- when :send
150
- # Only consider these methods as definitely returning arrays when iterating
151
- method_name = collection_node.children[1]
152
- # map, select, collect, etc. on anything return arrays for iteration
153
- %i[map select collect flat_map to_a uniq compact].include?(method_name)
154
- else
155
- # Block variable itself won't tell us if it's an array
156
- false
90
+ case node.type
91
+ when :array then true
92
+ when :send then %i[map select collect flat_map to_a uniq compact].include?(node.children[1])
93
+ else false
157
94
  end
158
95
  end
159
96
 
@@ -173,14 +110,11 @@ module EagerEye
173
110
  return "" unless node.is_a?(Parser::AST::Node)
174
111
 
175
112
  case node.type
176
- when :lvar
177
- node.children[0].to_s
113
+ when :lvar then node.children[0].to_s
178
114
  when :send
179
115
  receiver_str = reconstruct_chain(node.children[0])
180
- method = node.children[1]
181
- receiver_str.empty? ? method.to_s : "#{receiver_str}.#{method}"
182
- else
183
- ""
116
+ receiver_str.empty? ? node.children[1].to_s : "#{receiver_str}.#{node.children[1]}"
117
+ else ""
184
118
  end
185
119
  end
186
120
  end
@@ -5,11 +5,8 @@ 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
- # Methods that return a single record (not a collection)
9
8
  SINGLE_RECORD_METHODS = %i[find find_by find_by! first first! last last! take take! second third fourth fifth
10
9
  forty_two sole find_sole_by].freeze
11
-
12
- # Common association names (belongs_to = singular, has_many = plural)
13
10
  ASSOCIATION_NAMES = Set.new(%w[
14
11
  author user owner creator admin member customer client post article comment category tag
15
12
  parent company organization project task item order product account profile setting image
@@ -18,8 +15,6 @@ module EagerEye
18
15
  tasks items orders products accounts profiles settings images avatars photos attachments
19
16
  documents
20
17
  ]).freeze
21
-
22
- # Methods that should NOT be treated as associations
23
18
  EXCLUDED_METHODS = %i[
24
19
  id to_s to_h to_a to_json to_xml inspect class object_id nil? blank? present? empty?
25
20
  any? none? size count length save save! update update! destroy destroy! delete delete!
@@ -31,10 +26,11 @@ module EagerEye
31
26
  :loop_association
32
27
  end
33
28
 
34
- def detect(ast, file_path)
29
+ def detect(ast, file_path, association_preloads = {})
35
30
  return [] unless ast
36
31
 
37
32
  issues = []
33
+ @association_preloads = association_preloads
38
34
  build_variable_maps(ast)
39
35
 
40
36
  traverse_ast(ast) do |node|
@@ -51,6 +47,8 @@ module EagerEye
51
47
 
52
48
  included = extract_included_associations(collection_node)
53
49
  included.merge(extract_variable_preloads(collection_node))
50
+ model_name = infer_model_name_from_collection(collection_node)
51
+ included.merge(get_association_preloads(model_name))
54
52
 
55
53
  find_association_calls(block_body, block_var, file_path, issues, included)
56
54
  end
@@ -60,40 +58,42 @@ module EagerEye
60
58
 
61
59
  private
62
60
 
63
- def iteration_block?(node)
64
- return false unless node.type == :block
61
+ def get_association_preloads(model_name)
62
+ key = "#{model_name}#*"
63
+ preloaded = Set.new
64
+ @association_preloads&.each do |assoc_key, assocs|
65
+ preloaded.merge(assocs) if assoc_key.start_with?(key)
66
+ end
67
+ preloaded
68
+ end
65
69
 
66
- send_node = node.children[0]
67
- return false unless send_node&.type == :send
70
+ def infer_model_name_from_collection(node)
71
+ return nil unless node&.type == :send
68
72
 
69
- method_name = send_node.children[1]
70
- ITERATION_METHODS.include?(method_name)
73
+ receiver = node.children[0]
74
+ receiver.children[1].to_s if receiver&.type == :const
71
75
  end
72
76
 
73
- 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
77
+ def iteration_block?(node)
78
+ node.type == :block && node.children[0]&.type == :send &&
79
+ ITERATION_METHODS.include?(node.children[0].children[1])
80
+ end
80
81
 
81
- first_arg.children[0]
82
+ def extract_block_variable(block_node)
83
+ args = block_node&.children&.[](1)
84
+ first_arg = args&.children&.first
85
+ first_arg&.type == :arg ? first_arg.children[0] : nil
82
86
  end
83
87
 
84
88
  def extract_included_associations(collection_node)
85
89
  included = Set.new
86
90
  return included unless collection_node&.type == :send
87
91
 
88
- # Traverse through chained method calls to find includes/preload/eager_load
89
92
  current = collection_node
90
93
  while current&.type == :send
91
- method_name = current.children[1]
92
- extract_includes_from_method(current, included) if PRELOAD_METHODS.include?(method_name)
93
-
94
+ extract_includes_from_method(current, included) if PRELOAD_METHODS.include?(current.children[1])
94
95
  current = current.children[0]
95
96
  end
96
-
97
97
  included
98
98
  end
99
99
 
@@ -101,19 +101,21 @@ module EagerEye
101
101
  @variable_preloads = {}
102
102
  @single_record_variables = Set.new
103
103
 
104
- traverse_ast(ast) do |node|
105
- next unless %i[lvasgn ivasgn].include?(node.type)
104
+ traverse_ast(ast) { |node| process_variable_assignment(node) }
105
+ end
106
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
107
+ def process_variable_assignment(node)
108
+ return unless %i[lvasgn ivasgn].include?(node.type)
111
109
 
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
110
+ var_type = node.type == :lvasgn ? :lvar : :ivar
111
+ var_name = node.children[0]
112
+ value_node = node.children[1]
113
+ return unless value_node
114
+
115
+ key = [var_type, var_name]
116
+ preloaded = extract_included_associations(value_node)
117
+ @variable_preloads[key] = preloaded unless preloaded.empty?
118
+ @single_record_variables.add(key) if single_record_query?(value_node)
117
119
  end
118
120
 
119
121
  def extract_variable_preloads(node)
@@ -130,11 +132,18 @@ module EagerEye
130
132
  end
131
133
 
132
134
  def single_record_query?(node)
135
+ last_send = find_last_send_method(node)
136
+ last_send && SINGLE_RECORD_METHODS.include?(last_send)
137
+ end
138
+
139
+ def find_last_send_method(node)
133
140
  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])
141
+ current = current.children[0] while current&.type == :send && !single_record_method?(current)
142
+ current&.type == :send ? current.children[1] : nil
143
+ end
144
+
145
+ def single_record_method?(node)
146
+ SINGLE_RECORD_METHODS.include?(node.children[1])
138
147
  end
139
148
 
140
149
  def single_record_iteration?(node)
@@ -145,67 +154,46 @@ module EagerEye
145
154
  end
146
155
 
147
156
  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
159
- end
160
-
161
- 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
157
+ args = extract_method_args(method_node)
158
+ included_set.merge(extract_symbols_from_args(args))
166
159
  end
167
160
 
168
161
  def find_association_calls(node, block_var, file_path, issues, included_associations = Set.new)
169
- reported_associations = Set.new
170
-
162
+ reported = Set.new
171
163
  traverse_ast(node) do |child|
172
- next unless child.type == :send
173
-
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)
164
+ next unless should_report_issue?(child, block_var, reported, included_associations)
183
165
 
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
- )
166
+ add_n_plus_one_issue(child, block_var, file_path, issues, reported)
196
167
  end
197
168
  end
198
169
 
199
- def direct_call_on_block_var?(receiver, block_var)
200
- return false unless receiver
170
+ def should_report_issue?(child, block_var, reported, included)
171
+ return false unless child.type == :send
201
172
 
202
- receiver.type == :lvar && receiver.children[0] == block_var
173
+ receiver = child.children[0]
174
+ method = child.children[1]
175
+ return false unless receiver&.type == :lvar && receiver.children[0] == block_var
176
+ return false if excluded?(method, included)
177
+
178
+ key = "#{child.loc.line}:#{method}"
179
+ !reported.include?(key) && reported.add(key)
203
180
  end
204
181
 
205
- def likely_association?(method_name)
206
- return false if EXCLUDED_METHODS.include?(method_name)
182
+ def excluded?(method, included)
183
+ EXCLUDED_METHODS.include?(method) ||
184
+ !ASSOCIATION_NAMES.include?(method.to_s) ||
185
+ included.include?(method)
186
+ end
207
187
 
208
- ASSOCIATION_NAMES.include?(method_name.to_s)
188
+ def add_n_plus_one_issue(node, block_var, file_path, issues, reported)
189
+ method = node.children[1]
190
+ reported << "#{node.loc.line}:#{method}"
191
+ issues << create_issue(
192
+ file_path: file_path,
193
+ line_number: node.loc.line,
194
+ message: "Potential N+1 query: `#{block_var}.#{method}` called inside loop",
195
+ suggestion: "Use `includes(:#{method})` before iterating"
196
+ )
209
197
  end
210
198
  end
211
199
  end
@@ -3,22 +3,14 @@
3
3
  module EagerEye
4
4
  module Detectors
5
5
  class MissingCounterCache < Base
6
- # Methods that trigger COUNT queries
7
6
  COUNT_METHODS = %i[count size length].freeze
8
-
9
- # Common has_many association names (plural)
10
7
  PLURAL_ASSOCIATIONS = %w[
11
- posts comments tags categories articles users members
12
- items orders products tasks projects images attachments
13
- documents files messages notifications reviews ratings
14
- followers followings likes favorites bookmarks votes
15
- children replies responses answers questions
8
+ posts comments tags categories articles users members items orders products tasks projects
9
+ images attachments documents files messages notifications reviews ratings followers followings
10
+ likes favorites bookmarks votes children replies responses answers questions
16
11
  ].freeze
17
-
18
- # Iteration methods that indicate a loop context
19
- ITERATION_METHODS = %i[each map collect select reject find_all
20
- filter filter_map flat_map each_with_index
21
- each_with_object reduce inject sum].freeze
12
+ ITERATION_METHODS = %i[each map collect select reject find_all filter filter_map flat_map
13
+ each_with_index each_with_object reduce inject sum].freeze
22
14
 
23
15
  def self.detector_name
24
16
  :missing_counter_cache
@@ -51,68 +43,45 @@ module EagerEye
51
43
 
52
44
  def count_on_association?(node)
53
45
  return false unless node.type == :send
54
-
55
- method_name = node.children[1]
56
- return false unless COUNT_METHODS.include?(method_name)
46
+ return false unless COUNT_METHODS.include?(node.children[1])
57
47
 
58
48
  receiver = node.children[0]
59
- return false unless receiver
60
-
61
- likely_association_receiver?(receiver)
49
+ receiver && likely_association_receiver?(receiver)
62
50
  end
63
51
 
64
52
  def likely_association_receiver?(node)
65
- return false unless node.type == :send
66
-
67
- method_name = node.children[1]
68
- PLURAL_ASSOCIATIONS.include?(method_name.to_s)
53
+ node.type == :send && PLURAL_ASSOCIATIONS.include?(node.children[1].to_s)
69
54
  end
70
55
 
71
56
  def extract_association_name(node)
72
57
  receiver = node.children[0]
73
- return nil unless receiver&.type == :send
74
-
75
- receiver.children[1].to_s
58
+ receiver.children[1].to_s if receiver&.type == :send
76
59
  end
77
60
 
78
- # Check if the node is inside an iteration block
79
61
  def inside_iteration?(node)
80
62
  parent = node
81
- while (parent = find_parent(parent))
63
+ while (parent = @parent_map[parent])
82
64
  return true if iteration_block?(parent)
83
65
  end
84
66
  false
85
67
  end
86
68
 
87
- def find_parent(node)
88
- @parent_map ||= {}
89
- @parent_map[node]
90
- end
91
-
92
- # Override traverse_ast to build parent map
93
69
  def traverse_ast(node, &block)
94
70
  return unless node.is_a?(Parser::AST::Node)
95
71
 
96
72
  @parent_map ||= {}
97
-
98
73
  yield node
99
-
100
74
  node.children.each do |child|
101
- if child.is_a?(Parser::AST::Node)
102
- @parent_map[child] = node
103
- traverse_ast(child, &block)
104
- end
75
+ next unless child.is_a?(Parser::AST::Node)
76
+
77
+ @parent_map[child] = node
78
+ traverse_ast(child, &block)
105
79
  end
106
80
  end
107
81
 
108
82
  def iteration_block?(node)
109
- return false unless node.type == :block
110
-
111
- send_node = node.children[0]
112
- return false unless send_node&.type == :send
113
-
114
- method_name = send_node.children[1]
115
- ITERATION_METHODS.include?(method_name)
83
+ node.type == :block && node.children[0]&.type == :send &&
84
+ ITERATION_METHODS.include?(node.children[0].children[1])
116
85
  end
117
86
  end
118
87
  end
@@ -49,108 +49,75 @@ module EagerEye
49
49
  add_issue(node) if regular_pluck?(node)
50
50
  end
51
51
 
52
- def local_variable_assignment?(node)
53
- node.type == :lvasgn
54
- end
52
+ def local_variable_assignment?(node) = node.type == :lvasgn
55
53
 
56
- def where_call?(node)
57
- node.type == :send && node.children[1] == :where
58
- end
54
+ def where_call?(node) = node.type == :send && node.children[1] == :where
59
55
 
60
56
  def pluck_call?(node)
61
- return false unless node.is_a?(Parser::AST::Node) && node.type == :send
62
-
63
- method = node.children[1]
64
- %i[pluck ids].include?(method)
57
+ node.is_a?(Parser::AST::Node) && node.type == :send && %i[pluck ids].include?(node.children[1])
65
58
  end
66
59
 
67
60
  def all_pluck_call?(node)
68
61
  return false unless pluck_call?(node)
69
62
 
70
63
  receiver = node.children[0]
71
- receiver.is_a?(Parser::AST::Node) && receiver.type == :send &&
72
- receiver.children[1] == :all
64
+ receiver.is_a?(Parser::AST::Node) && receiver.type == :send && receiver.children[1] == :all
73
65
  end
74
66
 
75
67
  def map_id_call?(node)
76
- return false unless node.is_a?(Parser::AST::Node)
77
-
78
- block_map?(node) || send_map?(node)
68
+ node.is_a?(Parser::AST::Node) && (block_map?(node) || send_map?(node))
79
69
  end
80
70
 
81
71
  def block_map?(node)
82
- return false unless node.type == :block
83
-
84
- send_node = node.children[0]
85
- send_node&.type == :send && %i[map collect].include?(send_node.children[1])
72
+ node.type == :block && node.children[0]&.type == :send &&
73
+ %i[map collect].include?(node.children[0].children[1])
86
74
  end
87
75
 
88
76
  def send_map?(node)
89
- return false unless node.type == :send
90
-
91
- method = node.children[1]
92
- %i[map collect].include?(method) &&
77
+ node.type == :send && %i[map collect].include?(node.children[1]) &&
93
78
  node.children[2..].any? { |arg| symbol_to_proc_id?(arg) }
94
79
  end
95
80
 
96
81
  def symbol_to_proc_id?(node)
97
82
  return false unless node.is_a?(Parser::AST::Node) && node.type == :block_pass
98
83
 
99
- sym = node.children[0]
100
- sym&.type == :sym && %i[id to_i].include?(sym.children[0])
84
+ node.children[0]&.type == :sym && %i[id to_i].include?(node.children[0].children[0])
101
85
  end
102
86
 
103
87
  def regular_pluck?(node)
104
- where_args = node.children[2..]
105
- where_args.any? { |arg| pluck_var_in_hash?(arg) }
88
+ node.children[2..].any? { |arg| pluck_var_in_hash?(arg) }
106
89
  end
107
90
 
108
91
  def critical_pluck?(node)
109
- where_args = node.children[2..]
110
- where_args.any? { |arg| critical_pluck_in_hash?(arg) }
92
+ node.children[2..].any? { |arg| critical_pluck_in_hash?(arg) }
111
93
  end
112
94
 
113
95
  def pluck_var_in_hash?(node)
114
96
  return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
115
97
 
116
- node.children.any? do |pair|
117
- next false unless pair.type == :pair
118
-
119
- pluck_value?(pair.children[1])
120
- end
98
+ node.children.any? { |pair| pair.type == :pair && pluck_value?(pair.children[1]) }
121
99
  end
122
100
 
123
101
  def critical_pluck_in_hash?(node)
124
102
  return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
125
103
 
126
- node.children.any? do |pair|
127
- next false unless pair.type == :pair
128
-
129
- critical_value?(pair.children[1])
130
- end
104
+ node.children.any? { |pair| pair.type == :pair && critical_value?(pair.children[1]) }
131
105
  end
132
106
 
133
107
  def pluck_value?(value)
134
- return false unless value.type == :lvar
135
-
136
- var_name = value.children[0]
137
- @pluck_variables.key?(var_name) || @map_id_variables.key?(var_name)
108
+ value.type == :lvar && (@pluck_variables.key?(value.children[0]) || @map_id_variables.key?(value.children[0]))
138
109
  end
139
110
 
140
111
  def critical_value?(value)
141
- if value.type == :lvar
142
- @critical_pluck_variables.key?(value.children[0])
143
- else
144
- all_pluck_call?(value)
145
- end
112
+ value.type == :lvar ? @critical_pluck_variables.key?(value.children[0]) : all_pluck_call?(value)
146
113
  end
147
114
 
148
115
  def add_issue(node)
149
116
  @issues << create_issue(
150
117
  file_path: @file_path,
151
118
  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))`",
119
+ message: "Using plucked array in `where` causes two queries and memory overhead",
120
+ suggestion: "Use `.select(:id)` subquery instead: `Model.where(col: OtherModel.select(:id))`",
154
121
  severity: :warning
155
122
  )
156
123
  end
@@ -159,8 +126,8 @@ module EagerEye
159
126
  @issues << create_issue(
160
127
  file_path: @file_path,
161
128
  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))`",
129
+ message: "Using `.all.pluck(:id)` loads entire table into memory - highly inefficient",
130
+ suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.select(:id))`",
164
131
  severity: :error
165
132
  )
166
133
  end