eager_eye 1.0.5 → 1.0.6

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: 1e198460506f7e19906eeed320e9f9f11987ca1f4894b4b205a56d58799f3781
4
- data.tar.gz: e61be4439b88d9731678fae64cb530390c7d2ab06dede3bbef570f8923c5542c
3
+ metadata.gz: c0eccf7e9da999d8e273e9b41a11c96eb1284fdd944ce6af0e547f4201a3fbb9
4
+ data.tar.gz: 55b8ab8e753245bc5b638705bff5a0c10115c0f39f44d69f25f5b50dcb823ffa
5
5
  SHA512:
6
- metadata.gz: b32e0853b58fd412511418aedf8ce2d452d0f5a20b20e54eb3c2bb260c7233af7f7bbbe465f7efd7b511114152ffb72b381f54b88bd12c67463a63b547f6ff91
7
- data.tar.gz: bf371dcf0600ec9f57614eae0d2d52fee4a904f3be37bd5d2f8a0e8d7066135d78b86d6a914e90f61fbc26afcee09003f886c8415ce2d84ce3d35799825975ee
6
+ metadata.gz: 9f41668f38474f848cffe0e70473c689c27eb9a102a20cdc932f1039a2c4dce71cb91eda4fa2bb1012136d48b93976cb33ad8455a3e01db2a2c400037a75420e
7
+ data.tar.gz: 2d8f591ee31948302c8a14fe2deef868bc98e2d7fd23083c39a6ee8e9242296e25080938f4e00cef24757c2d2ec6df512d7e6ef144945420de593168e0d71e96
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.6] - 2025-12-22
11
+
12
+ ### Fixed
13
+
14
+ - Fixed false positive in `MissingCounterCache` detector for single `.count`/`.size`/`.length` calls
15
+ - Now only detects count calls **inside iterations** where N+1 queries actually occur
16
+ - Single calls like `post.comments.count` are no longer flagged (not N+1)
17
+ - Iteration patterns like `posts.each { |p| p.comments.count }` are correctly detected
18
+
10
19
  ## [1.0.5] - 2025-12-21
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.5-red.svg)](https://rubygems.org/gems/eager_eye)
4
+ [![Gem Version](https://img.shields.io/badge/gem-v1.0.6-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)
@@ -150,19 +150,26 @@ Supports multiple serializer libraries:
150
150
 
151
151
  ### 3. Missing Counter Cache
152
152
 
153
- Detects `.count` or `.size` calls on associations that could benefit from counter caches.
153
+ Detects `.count`, `.size`, or `.length` calls on associations **inside iterations** that could benefit from counter caches. Single calls outside loops are not flagged since they don't cause N+1 issues.
154
154
 
155
155
  ```ruby
156
- # Bad - COUNT query every time
157
- post.comments.count
158
- post.comments.size
156
+ # Bad - COUNT query for each post in iteration
157
+ posts.each do |post|
158
+ post.comments.count # Detected: N+1 query!
159
+ post.likes.size # Detected: N+1 query!
160
+ end
161
+
162
+ # OK - Single count call (not in iteration, no N+1)
163
+ post.comments.count # Not flagged - single query is fine
159
164
 
160
- # Good - Add counter cache
165
+ # Good - Add counter cache for iteration use cases
161
166
  # In Comment model:
162
167
  belongs_to :post, counter_cache: true
163
168
 
164
169
  # Then this is a simple column read:
165
- post.comments_count
170
+ posts.each do |post|
171
+ post.comments_count # No query - just reads the column
172
+ end
166
173
  ```
167
174
 
168
175
  ### 4. Custom Method Query (N+1 in query methods)
@@ -15,6 +15,11 @@ module EagerEye
15
15
  children replies responses answers questions
16
16
  ].freeze
17
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
22
+
18
23
  def self.detector_name
19
24
  :missing_counter_cache
20
25
  end
@@ -26,6 +31,7 @@ module EagerEye
26
31
 
27
32
  traverse_ast(ast) do |node|
28
33
  next unless count_on_association?(node)
34
+ next unless inside_iteration?(node)
29
35
 
30
36
  association_name = extract_association_name(node)
31
37
  next unless association_name
@@ -33,7 +39,7 @@ module EagerEye
33
39
  issues << create_issue(
34
40
  file_path: file_path,
35
41
  line_number: node.loc.line,
36
- message: "`.#{node.children[1]}` called on `#{association_name}` may cause N+1 queries",
42
+ message: "`.#{node.children[1]}` called on `#{association_name}` inside iteration may cause N+1 queries",
37
43
  suggestion: "Consider adding `counter_cache: true` to the belongs_to association"
38
44
  )
39
45
  end
@@ -68,6 +74,46 @@ module EagerEye
68
74
 
69
75
  receiver.children[1].to_s
70
76
  end
77
+
78
+ # Check if the node is inside an iteration block
79
+ def inside_iteration?(node)
80
+ parent = node
81
+ while (parent = find_parent(parent))
82
+ return true if iteration_block?(parent)
83
+ end
84
+ false
85
+ end
86
+
87
+ def find_parent(node)
88
+ @parent_map ||= {}
89
+ @parent_map[node]
90
+ end
91
+
92
+ # Override traverse_ast to build parent map
93
+ def traverse_ast(node, &block)
94
+ return unless node.is_a?(Parser::AST::Node)
95
+
96
+ @parent_map ||= {}
97
+
98
+ yield node
99
+
100
+ 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
105
+ end
106
+ end
107
+
108
+ 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)
116
+ end
71
117
  end
72
118
  end
73
119
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.0.5"
4
+ VERSION = "1.0.6"
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.5
4
+ version: 1.0.6
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-21 00:00:00.000000000 Z
11
+ date: 2025-12-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast