eager_eye 1.0.8 → 1.0.10
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 +4 -4
- data/CHANGELOG.md +28 -0
- data/README.md +56 -20
- data/lib/eager_eye/comment_parser.rb +34 -20
- data/lib/eager_eye/detectors/pluck_to_array.rb +91 -70
- data/lib/eager_eye/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 27aed443ecc5e83aeb4fe0baa9aa9ccbcec462df45767c8294d867562c9066b5
|
|
4
|
+
data.tar.gz: f2a9f245154a8fe043d9a9f0f68edaf940a5c3f8e2bd3f0255962603ef48d4d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f6f769f5d65827fd4059671674f8947f172b1ef9ff72a82375c25ac3e31c35c81099384e79de97036b8c867613343fd2e078f7c23f59fd68bf5daac8cd8a8a19
|
|
7
|
+
data.tar.gz: f6c0743373d769ec6d147ccb1045b6a667d6015551a705b31eaefe6fc511568ecf06683cb99d7034f2d59cd83997732096d8e7c12b732b42c6ee460b9d152b5c
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ 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
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
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
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
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
|
|
25
|
+
|
|
26
|
+
## [1.0.9] - 2025-12-26
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **Inline Suppression Comments** - Suppress specific warnings with RuboCop-style inline comments
|
|
31
|
+
- `# eager_eye:disable-next-line` - Suppress next line
|
|
32
|
+
- `# eager_eye:disable CallbackQuery` - Suppress specific detector inline
|
|
33
|
+
- `# eager_eye:disable-block` / `# eager_eye:enable-block` - Suppress block of code
|
|
34
|
+
- `# eager_eye:disable-file DetectorName` - Suppress entire file
|
|
35
|
+
- Supports both CamelCase and snake_case detector names
|
|
36
|
+
- Can disable all detectors with `all` keyword
|
|
37
|
+
|
|
10
38
|
## [1.0.8] - 2025-12-25
|
|
11
39
|
|
|
12
40
|
### Added
|
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.
|
|
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>
|
|
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>
|
|
@@ -23,23 +23,44 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
##
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
## Table of Contents
|
|
27
|
+
- [Features](#features)
|
|
28
|
+
- [Installation](#installation)
|
|
29
|
+
- [Quick Start](#quick-start)
|
|
30
|
+
- [Detected Issues](#detected-issues)
|
|
31
|
+
- [Inline Suppression](#inline-suppression)
|
|
32
|
+
- [Auto-fix](#auto-fix-experimental)
|
|
33
|
+
- [RSpec Integration](#rspec-integration)
|
|
34
|
+
- [Configuration](#configuration)
|
|
35
|
+
- [CI Integration](#ci-integration)
|
|
36
|
+
- [CLI Reference](#cli-reference)
|
|
37
|
+
- [Output Formats](#output-formats)
|
|
38
|
+
- [Limitations](#limitations)
|
|
39
|
+
- [VS Code Extension](#vs-code-extension)
|
|
40
|
+
- [Development](#development)
|
|
41
|
+
- [Contributing](#contributing)
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
✨ **Detects 7 types of N+1 problems:**
|
|
46
|
+
- Loop associations (queries in iterations)
|
|
47
|
+
- Serializer nesting issues
|
|
48
|
+
- Missing counter caches
|
|
49
|
+
- Custom method queries
|
|
50
|
+
- Count in iteration patterns
|
|
51
|
+
- Callback query N+1s
|
|
52
|
+
- Pluck to array misuse
|
|
53
|
+
|
|
54
|
+
🔧 **Developer-friendly:**
|
|
55
|
+
- Inline suppression (like RuboCop)
|
|
56
|
+
- Auto-fix support (experimental)
|
|
57
|
+
- JSON/Console output formats
|
|
58
|
+
- RSpec integration
|
|
59
|
+
|
|
60
|
+
🚀 **CI-ready:**
|
|
61
|
+
- No test suite required
|
|
62
|
+
- GitHub Actions examples included
|
|
63
|
+
- Severity levels and filtering
|
|
43
64
|
|
|
44
65
|
## Installation
|
|
45
66
|
|
|
@@ -186,10 +207,10 @@ end
|
|
|
186
207
|
|
|
187
208
|
### 4. Custom Method Query (N+1 in query methods)
|
|
188
209
|
|
|
189
|
-
Detects query methods (`.where`, `.find_by`, `.exists?`, etc.) called on associations inside loops.
|
|
210
|
+
Detects query methods (`.where`, `.find_by`, `.exists?`, etc.) called on associations inside loops.
|
|
190
211
|
|
|
191
212
|
```ruby
|
|
192
|
-
# Bad -
|
|
213
|
+
# Bad - where inside loop
|
|
193
214
|
class User < ApplicationRecord
|
|
194
215
|
def supports?(team_name)
|
|
195
216
|
teams.where(name: team_name).exists?
|
|
@@ -296,6 +317,10 @@ user_ids = User.active.pluck(:id) # Query 1: SELECT id FROM users
|
|
|
296
317
|
Post.where(user_id: user_ids) # Query 2: SELECT * FROM posts WHERE user_id IN (1,2,3...)
|
|
297
318
|
# Also holds potentially thousands of IDs in memory
|
|
298
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 (...)
|
|
323
|
+
|
|
299
324
|
# Bad - Same problem with map
|
|
300
325
|
user_ids = users.map(&:id)
|
|
301
326
|
Post.where(user_id: user_ids)
|
|
@@ -305,11 +330,22 @@ Post.where(user_id: User.active.select(:id))
|
|
|
305
330
|
# Single query: SELECT * FROM posts WHERE user_id IN (SELECT id FROM users WHERE active = true)
|
|
306
331
|
```
|
|
307
332
|
|
|
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
|
+
|
|
308
343
|
**Performance comparison with 10,000 users:**
|
|
309
344
|
|
|
310
345
|
| Approach | Queries | Memory | Time |
|
|
311
346
|
|----------|---------|--------|------|
|
|
312
347
|
| `pluck` + `where` | 2 | ~80KB for IDs | ~45ms |
|
|
348
|
+
| `.all.pluck` + `where` | 2 | ~40KB+ for all IDs | ~100ms+ |
|
|
313
349
|
| `select` subquery | 1 | None | ~20ms |
|
|
314
350
|
|
|
315
351
|
## Inline Suppression
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
class CommentParser
|
|
5
|
-
DISABLE_PATTERN = /eager_eye:disable(?:-next-line|-file)?\s+(.+?)(?:\s+--|$)/i
|
|
6
|
-
ENABLE_PATTERN = /eager_eye:enable\s+(.+?)(?:\s+--|$)/i
|
|
7
|
-
INLINE_DISABLE_PATTERN = /eager_eye:disable\s+(.+?)(?:\s+--|$)/i
|
|
8
5
|
FILE_DISABLE_PATTERN = /eager_eye:disable-file\s+(.+?)(?:\s+--|$)/i
|
|
9
|
-
NEXT_LINE_PATTERN = /eager_eye:disable-next-line
|
|
10
|
-
|
|
6
|
+
NEXT_LINE_PATTERN = /eager_eye:disable-next-line(?:\s+(.+?))?(?:\s+--|$)/i
|
|
7
|
+
BLOCK_START_PATTERN = /eager_eye:disable-block(?:\s+(.+?))?(?:\s+--|$)/i
|
|
8
|
+
BLOCK_END_PATTERN = /eager_eye:enable-block(?:\s+(.+?))?(?:\s+--|$)/i
|
|
9
|
+
INLINE_DISABLE_PATTERN = /eager_eye:disable\s+(.+?)(?:\s+--|$)/i
|
|
10
|
+
ENABLE_PATTERN = /eager_eye:enable(?:\s+(.+?))?(?:\s+--|$)/i
|
|
11
11
|
|
|
12
12
|
def initialize(source_code)
|
|
13
13
|
@source_code = source_code.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
@@ -46,39 +46,46 @@ module EagerEye
|
|
|
46
46
|
def detect_directive(line, line_num)
|
|
47
47
|
detect_file_directive(line, line_num) ||
|
|
48
48
|
detect_next_line_directive(line) ||
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
detect_inline_directive(line)
|
|
49
|
+
detect_block_end_directive(line) ||
|
|
50
|
+
detect_block_or_inline_directive(line)
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
def detect_file_directive(line, line_num)
|
|
55
54
|
return unless line_num <= 5 && line =~ FILE_DISABLE_PATTERN
|
|
56
55
|
|
|
57
|
-
|
|
56
|
+
detectors = parse_detector_names(::Regexp.last_match(1) || "all")
|
|
57
|
+
{ type: :file, detectors: detectors }
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
def detect_next_line_directive(line)
|
|
61
61
|
return unless line =~ NEXT_LINE_PATTERN
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
detectors = parse_detector_names(::Regexp.last_match(1) || "all")
|
|
64
|
+
{ type: :next_line, detectors: detectors }
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
def
|
|
67
|
-
return unless line =~ BLOCK_DISABLE_PATTERN && !inline_disable?(line)
|
|
68
|
-
|
|
69
|
-
{ type: :block_start, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def detect_enable_directive(line)
|
|
67
|
+
def detect_block_end_directive(line)
|
|
73
68
|
return unless line =~ ENABLE_PATTERN
|
|
74
69
|
|
|
75
|
-
|
|
70
|
+
detectors = parse_detector_names(::Regexp.last_match(1) || "all")
|
|
71
|
+
{ type: :block_end, detectors: detectors }
|
|
76
72
|
end
|
|
77
73
|
|
|
78
|
-
def
|
|
74
|
+
def detect_block_or_inline_directive(line)
|
|
75
|
+
if line =~ BLOCK_START_PATTERN && !code_before_comment?(line)
|
|
76
|
+
detectors = parse_detector_names(::Regexp.last_match(1) || "all")
|
|
77
|
+
return { type: :block_start, detectors: detectors }
|
|
78
|
+
end
|
|
79
|
+
|
|
79
80
|
return unless line =~ INLINE_DISABLE_PATTERN
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
detectors = parse_detector_names(::Regexp.last_match(1) || "all")
|
|
83
|
+
|
|
84
|
+
if code_before_comment?(line)
|
|
85
|
+
{ type: :inline, detectors: detectors }
|
|
86
|
+
else
|
|
87
|
+
{ type: :block_start, detectors: detectors }
|
|
88
|
+
end
|
|
82
89
|
end
|
|
83
90
|
|
|
84
91
|
def apply_directive(directive, line_num)
|
|
@@ -129,7 +136,14 @@ module EagerEye
|
|
|
129
136
|
code_part && !code_part.strip.empty?
|
|
130
137
|
end
|
|
131
138
|
|
|
139
|
+
def code_before_comment?(line)
|
|
140
|
+
code_part = line.split("#").first
|
|
141
|
+
code_part && !code_part.strip.empty?
|
|
142
|
+
end
|
|
143
|
+
|
|
132
144
|
def parse_detector_names(str)
|
|
145
|
+
return ["all"] if str.nil? || str.strip.empty?
|
|
146
|
+
|
|
133
147
|
str.split(/[,\s]+/).map(&:strip).reject(&:empty?).map do |name|
|
|
134
148
|
normalize_detector_name(name)
|
|
135
149
|
end
|
|
@@ -12,126 +12,136 @@ module EagerEye
|
|
|
12
12
|
@file_path = file_path
|
|
13
13
|
@pluck_variables = {}
|
|
14
14
|
@map_id_variables = {}
|
|
15
|
+
@critical_pluck_variables = {}
|
|
15
16
|
|
|
16
17
|
return @issues unless ast
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
collect_map_id_assignments(ast)
|
|
20
|
-
find_where_with_pluck_var(ast)
|
|
21
|
-
|
|
19
|
+
visit(ast)
|
|
22
20
|
@issues
|
|
23
21
|
end
|
|
24
22
|
|
|
25
23
|
private
|
|
26
24
|
|
|
27
|
-
def
|
|
25
|
+
def visit(node)
|
|
28
26
|
return unless node.is_a?(Parser::AST::Node)
|
|
29
27
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
value = node.children[1]
|
|
33
|
-
|
|
34
|
-
@pluck_variables[var_name] = node.loc.line if pluck_call?(value)
|
|
35
|
-
end
|
|
28
|
+
collect_assignments(node)
|
|
29
|
+
check_where_calls(node)
|
|
36
30
|
|
|
37
|
-
node.children.each
|
|
38
|
-
collect_pluck_assignments(child)
|
|
39
|
-
end
|
|
31
|
+
node.children.each { |child| visit(child) }
|
|
40
32
|
end
|
|
41
33
|
|
|
42
|
-
def
|
|
43
|
-
return unless
|
|
34
|
+
def collect_assignments(node)
|
|
35
|
+
return unless local_variable_assignment?(node)
|
|
44
36
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
value = node.children[1]
|
|
37
|
+
var_name = node.children[0]
|
|
38
|
+
value = node.children[1]
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
node.children.each do |child|
|
|
53
|
-
collect_map_id_assignments(child)
|
|
54
|
-
end
|
|
40
|
+
@critical_pluck_variables[var_name] = node.loc.line if all_pluck_call?(value)
|
|
41
|
+
@pluck_variables[var_name] = node.loc.line if pluck_call?(value)
|
|
42
|
+
@map_id_variables[var_name] = node.loc.line if map_id_call?(value)
|
|
55
43
|
end
|
|
56
44
|
|
|
57
|
-
def
|
|
58
|
-
return unless
|
|
45
|
+
def check_where_calls(node)
|
|
46
|
+
return unless where_call?(node)
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
node.children.each do |child|
|
|
63
|
-
find_where_with_pluck_var(child)
|
|
64
|
-
end
|
|
48
|
+
add_critical_issue(node) if critical_pluck?(node)
|
|
49
|
+
add_issue(node) if regular_pluck?(node)
|
|
65
50
|
end
|
|
66
51
|
|
|
67
52
|
def local_variable_assignment?(node)
|
|
68
53
|
node.type == :lvasgn
|
|
69
54
|
end
|
|
70
55
|
|
|
56
|
+
def where_call?(node)
|
|
57
|
+
node.type == :send && node.children[1] == :where
|
|
58
|
+
end
|
|
59
|
+
|
|
71
60
|
def pluck_call?(node)
|
|
72
|
-
return false unless node.is_a?(Parser::AST::Node)
|
|
73
|
-
return false unless node.type == :send
|
|
61
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :send
|
|
74
62
|
|
|
75
|
-
|
|
76
|
-
%i[pluck ids].include?(
|
|
63
|
+
method = node.children[1]
|
|
64
|
+
%i[pluck ids].include?(method)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def all_pluck_call?(node)
|
|
68
|
+
return false unless pluck_call?(node)
|
|
69
|
+
|
|
70
|
+
receiver = node.children[0]
|
|
71
|
+
receiver.is_a?(Parser::AST::Node) && receiver.type == :send &&
|
|
72
|
+
receiver.children[1] == :all
|
|
77
73
|
end
|
|
78
74
|
|
|
79
75
|
def map_id_call?(node)
|
|
80
76
|
return false unless node.is_a?(Parser::AST::Node)
|
|
81
77
|
|
|
82
|
-
|
|
83
|
-
when :block then block_map_call?(node)
|
|
84
|
-
when :send then send_map_id_call?(node)
|
|
85
|
-
else false
|
|
86
|
-
end
|
|
78
|
+
block_map?(node) || send_map?(node)
|
|
87
79
|
end
|
|
88
80
|
|
|
89
|
-
def
|
|
90
|
-
|
|
91
|
-
return false unless send_node&.type == :send
|
|
81
|
+
def block_map?(node)
|
|
82
|
+
return false unless node.type == :block
|
|
92
83
|
|
|
93
|
-
|
|
84
|
+
send_node = node.children[0]
|
|
85
|
+
send_node&.type == :send && %i[map collect].include?(send_node.children[1])
|
|
94
86
|
end
|
|
95
87
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
return false unless %i[map collect].include?(method_name)
|
|
88
|
+
def send_map?(node)
|
|
89
|
+
return false unless node.type == :send
|
|
99
90
|
|
|
100
|
-
node.children[
|
|
91
|
+
method = node.children[1]
|
|
92
|
+
%i[map collect].include?(method) &&
|
|
93
|
+
node.children[2..].any? { |arg| symbol_to_proc_id?(arg) }
|
|
101
94
|
end
|
|
102
95
|
|
|
103
96
|
def symbol_to_proc_id?(node)
|
|
104
|
-
return false unless node.is_a?(Parser::AST::Node)
|
|
105
|
-
return false unless node.type == :block_pass
|
|
97
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :block_pass
|
|
106
98
|
|
|
107
|
-
|
|
108
|
-
|
|
99
|
+
sym = node.children[0]
|
|
100
|
+
sym&.type == :sym && %i[id to_i].include?(sym.children[0])
|
|
101
|
+
end
|
|
109
102
|
|
|
110
|
-
|
|
103
|
+
def regular_pluck?(node)
|
|
104
|
+
where_args = node.children[2..]
|
|
105
|
+
where_args.any? { |arg| pluck_var_in_hash?(arg) }
|
|
111
106
|
end
|
|
112
107
|
|
|
113
|
-
def
|
|
114
|
-
|
|
115
|
-
|
|
108
|
+
def critical_pluck?(node)
|
|
109
|
+
where_args = node.children[2..]
|
|
110
|
+
where_args.any? { |arg| critical_pluck_in_hash?(arg) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def pluck_var_in_hash?(node)
|
|
114
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
node.children.any? do |pair|
|
|
117
|
+
next false unless pair.type == :pair
|
|
118
|
+
|
|
119
|
+
pluck_value?(pair.children[1])
|
|
120
|
+
end
|
|
119
121
|
end
|
|
120
122
|
|
|
121
|
-
def
|
|
122
|
-
return false unless node.is_a?(Parser::AST::Node)
|
|
123
|
-
return false unless node.type == :hash
|
|
123
|
+
def critical_pluck_in_hash?(node)
|
|
124
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
|
|
124
125
|
|
|
125
126
|
node.children.any? do |pair|
|
|
126
127
|
next false unless pair.type == :pair
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
critical_value?(pair.children[1])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
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)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def critical_value?(value)
|
|
141
|
+
if value.type == :lvar
|
|
142
|
+
@critical_pluck_variables.key?(value.children[0])
|
|
143
|
+
else
|
|
144
|
+
all_pluck_call?(value)
|
|
135
145
|
end
|
|
136
146
|
end
|
|
137
147
|
|
|
@@ -140,7 +150,18 @@ module EagerEye
|
|
|
140
150
|
file_path: @file_path,
|
|
141
151
|
line_number: node.loc.line,
|
|
142
152
|
message: "Using plucked/mapped array in `where` causes two queries and holds IDs in memory",
|
|
143
|
-
suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.condition.select(:id))`"
|
|
153
|
+
suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.condition.select(:id))`",
|
|
154
|
+
severity: :warning
|
|
155
|
+
)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def add_critical_issue(node)
|
|
159
|
+
@issues << create_issue(
|
|
160
|
+
file_path: @file_path,
|
|
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))`",
|
|
164
|
+
severity: :error
|
|
144
165
|
)
|
|
145
166
|
end
|
|
146
167
|
end
|
data/lib/eager_eye/version.rb
CHANGED
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.
|
|
4
|
+
version: 1.0.10
|
|
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-
|
|
11
|
+
date: 2025-12-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|