eager_eye 0.4.0 → 0.5.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: a971d7fbafebc02b51a9767987541f67fdd216025ac04aa4bdf4f304cc3abad9
4
- data.tar.gz: db6eeeb3e1f511856144c70031f63c5216c73b917c00a17a31dbcd44444757a8
3
+ metadata.gz: 031e0f3547538856718932cd6121ac990ad8c11a43682ec076f68bb591943d55
4
+ data.tar.gz: 433563980a743704c361304dff56866fe4cc9795a1d6f927cd0b4a8fc0367c8f
5
5
  SHA512:
6
- metadata.gz: b847b72aff2e758f0a32effdf68057c499503f97251802263d5a1109e5eac3afd8841044d5d144a48b351c4c0909789e5528574f911101f3949e8f78d015dd0a
7
- data.tar.gz: 3938004802489b63234cc75ec7cfc79c580a0cf263c004762a63fb2d4d1a09350f49f158ab9f4e99cbe4a11b9a8772766be61531c1909d958bed5b82141b407c
6
+ metadata.gz: 5c9f874001fb0c71eb8c1858ef2739181482992b7455925afc696acfa1b7bebfa91888d305936ab6189f64ea1542afd7844b95c4ba453218d0c706d40d17304b
7
+ data.tar.gz: 93750c566392f234cb5009d30873130182a4e80722b9f29402b662caacc0c961a31a8c109937c378f9a04b3d0f6e134326a0edeb0d138255debc2c9fb4ceb9a3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] - 2025-12-15
11
+
12
+ ### Added
13
+
14
+ - **New Detector: `PluckToArray`** - Detects pluck/map results used in where clauses
15
+ - Catches `.pluck(:id)` and `.ids` results used in `where` clauses
16
+ - Catches `.map(&:id)` and `.collect(&:id)` patterns
17
+ - Suggests using `.select(:id)` subquery pattern for better performance
18
+ - Prevents two queries and memory overhead from holding IDs in arrays
19
+
20
+ ### Changed
21
+
22
+ - Updated default `enabled_detectors` to include `:pluck_to_array`
23
+ - Updated README with new detector documentation and performance comparison
24
+
10
25
  ## [0.4.0] - 2025-12-15
11
26
 
12
27
  ### Added
data/README.md CHANGED
@@ -246,6 +246,32 @@ def schedule_stats_update
246
246
  end
247
247
  ```
248
248
 
249
+ ### 7. Pluck to Array Misuse
250
+
251
+ Detects when `.pluck(:id)` or `.map(&:id)` results are used in `where` clauses instead of subqueries.
252
+
253
+ ```ruby
254
+ # Bad - Two queries + memory overhead
255
+ user_ids = User.active.pluck(:id) # Query 1: SELECT id FROM users
256
+ Post.where(user_id: user_ids) # Query 2: SELECT * FROM posts WHERE user_id IN (1,2,3...)
257
+ # Also holds potentially thousands of IDs in memory
258
+
259
+ # Bad - Same problem with map
260
+ user_ids = users.map(&:id)
261
+ Post.where(user_id: user_ids)
262
+
263
+ # Good - Single subquery, no memory overhead
264
+ Post.where(user_id: User.active.select(:id))
265
+ # Single query: SELECT * FROM posts WHERE user_id IN (SELECT id FROM users WHERE active = true)
266
+ ```
267
+
268
+ **Performance comparison with 10,000 users:**
269
+
270
+ | Approach | Queries | Memory | Time |
271
+ |----------|---------|--------|------|
272
+ | `pluck` + `where` | 2 | ~80KB for IDs | ~45ms |
273
+ | `select` subquery | 1 | None | ~20ms |
274
+
249
275
  ## Configuration
250
276
 
251
277
  ### Config File (.eager_eye.yml)
@@ -264,6 +290,7 @@ enabled_detectors:
264
290
  - custom_method_query
265
291
  - count_in_iteration
266
292
  - callback_query
293
+ - pluck_to_array
267
294
 
268
295
  # Base path to analyze (default: app)
269
296
  app_path: app
@@ -10,7 +10,8 @@ module EagerEye
10
10
  missing_counter_cache: Detectors::MissingCounterCache,
11
11
  custom_method_query: Detectors::CustomMethodQuery,
12
12
  count_in_iteration: Detectors::CountInIteration,
13
- callback_query: Detectors::CallbackQuery
13
+ callback_query: Detectors::CallbackQuery,
14
+ pluck_to_array: Detectors::PluckToArray
14
15
  }.freeze
15
16
 
16
17
  attr_reader :paths, :issues
@@ -7,6 +7,7 @@ module EagerEye
7
7
  DEFAULT_DETECTORS = %i[
8
8
  loop_association serializer_nesting missing_counter_cache
9
9
  custom_method_query count_in_iteration callback_query
10
+ pluck_to_array
10
11
  ].freeze
11
12
 
12
13
  def initialize
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ module Detectors
5
+ class PluckToArray < Base
6
+ def self.detector_name
7
+ :pluck_to_array
8
+ end
9
+
10
+ def detect(ast, file_path)
11
+ @issues = []
12
+ @file_path = file_path
13
+ @pluck_variables = {}
14
+ @map_id_variables = {}
15
+
16
+ return @issues unless ast
17
+
18
+ collect_pluck_assignments(ast)
19
+ collect_map_id_assignments(ast)
20
+ find_where_with_pluck_var(ast)
21
+
22
+ @issues
23
+ end
24
+
25
+ private
26
+
27
+ def collect_pluck_assignments(node)
28
+ return unless node.is_a?(Parser::AST::Node)
29
+
30
+ if local_variable_assignment?(node)
31
+ var_name = node.children[0]
32
+ value = node.children[1]
33
+
34
+ @pluck_variables[var_name] = node.loc.line if pluck_call?(value)
35
+ end
36
+
37
+ node.children.each do |child|
38
+ collect_pluck_assignments(child)
39
+ end
40
+ end
41
+
42
+ def collect_map_id_assignments(node)
43
+ return unless node.is_a?(Parser::AST::Node)
44
+
45
+ if local_variable_assignment?(node)
46
+ var_name = node.children[0]
47
+ value = node.children[1]
48
+
49
+ @map_id_variables[var_name] = node.loc.line if map_id_call?(value)
50
+ end
51
+
52
+ node.children.each do |child|
53
+ collect_map_id_assignments(child)
54
+ end
55
+ end
56
+
57
+ def find_where_with_pluck_var(node)
58
+ return unless node.is_a?(Parser::AST::Node)
59
+
60
+ add_issue(node) if where_call_with_pluck_var?(node)
61
+
62
+ node.children.each do |child|
63
+ find_where_with_pluck_var(child)
64
+ end
65
+ end
66
+
67
+ def local_variable_assignment?(node)
68
+ node.type == :lvasgn
69
+ end
70
+
71
+ def pluck_call?(node)
72
+ return false unless node.is_a?(Parser::AST::Node)
73
+ return false unless node.type == :send
74
+
75
+ method_name = node.children[1]
76
+ %i[pluck ids].include?(method_name)
77
+ end
78
+
79
+ def map_id_call?(node)
80
+ return false unless node.is_a?(Parser::AST::Node)
81
+
82
+ case node.type
83
+ when :block then block_map_call?(node)
84
+ when :send then send_map_id_call?(node)
85
+ else false
86
+ end
87
+ end
88
+
89
+ def block_map_call?(node)
90
+ send_node = node.children[0]
91
+ return false unless send_node&.type == :send
92
+
93
+ %i[map collect].include?(send_node.children[1])
94
+ end
95
+
96
+ def send_map_id_call?(node)
97
+ method_name = node.children[1]
98
+ return false unless %i[map collect].include?(method_name)
99
+
100
+ node.children[2..].any? { |arg| symbol_to_proc_id?(arg) }
101
+ end
102
+
103
+ def symbol_to_proc_id?(node)
104
+ return false unless node.is_a?(Parser::AST::Node)
105
+ return false unless node.type == :block_pass
106
+
107
+ sym_node = node.children[0]
108
+ return false unless sym_node&.type == :sym
109
+
110
+ %i[id to_i].include?(sym_node.children[0])
111
+ end
112
+
113
+ def where_call_with_pluck_var?(node)
114
+ return false unless node.type == :send
115
+ return false unless node.children[1] == :where
116
+
117
+ args = node.children[2..]
118
+ args.any? { |arg| hash_with_pluck_var?(arg) }
119
+ end
120
+
121
+ def hash_with_pluck_var?(node)
122
+ return false unless node.is_a?(Parser::AST::Node)
123
+ return false unless node.type == :hash
124
+
125
+ node.children.any? do |pair|
126
+ next false unless pair.type == :pair
127
+
128
+ value = pair.children[1]
129
+ if value.type == :lvar
130
+ var_name = value.children[0]
131
+ @pluck_variables.key?(var_name) || @map_id_variables.key?(var_name)
132
+ else
133
+ false
134
+ end
135
+ end
136
+ end
137
+
138
+ def add_issue(node)
139
+ @issues << create_issue(
140
+ file_path: @file_path,
141
+ line_number: node.loc.line,
142
+ message: "Using plucked/mapped array in `where` causes two queries and holds IDs in memory",
143
+ severity: :warning,
144
+ suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.condition.select(:id))`"
145
+ )
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/eager_eye.rb CHANGED
@@ -10,6 +10,7 @@ require_relative "eager_eye/detectors/missing_counter_cache"
10
10
  require_relative "eager_eye/detectors/custom_method_query"
11
11
  require_relative "eager_eye/detectors/count_in_iteration"
12
12
  require_relative "eager_eye/detectors/callback_query"
13
+ require_relative "eager_eye/detectors/pluck_to_array"
13
14
  require_relative "eager_eye/analyzer"
14
15
  require_relative "eager_eye/reporters/base"
15
16
  require_relative "eager_eye/reporters/console"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eager_eye
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
@@ -66,6 +66,7 @@ files:
66
66
  - lib/eager_eye/detectors/custom_method_query.rb
67
67
  - lib/eager_eye/detectors/loop_association.rb
68
68
  - lib/eager_eye/detectors/missing_counter_cache.rb
69
+ - lib/eager_eye/detectors/pluck_to_array.rb
69
70
  - lib/eager_eye/detectors/serializer_nesting.rb
70
71
  - lib/eager_eye/generators/install_generator.rb
71
72
  - lib/eager_eye/issue.rb