eager_eye 0.4.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +71 -0
- data/lib/eager_eye/analyzer.rb +10 -1
- data/lib/eager_eye/comment_parser.rb +144 -0
- data/lib/eager_eye/configuration.rb +1 -0
- data/lib/eager_eye/detectors/pluck_to_array.rb +149 -0
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 34e0c9e78e18d9d8c9a6cad8ee96a91c89ac469d9dc662e43a51f09d361a3200
|
|
4
|
+
data.tar.gz: 90554fbbe09380116398c8139c6d09ca5851d1e12d2eb52c32cf85f8c8fdc76a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 86605c1632ed47aaae05e26d810c6611dac2f356503502b71a94a389b1d58148cbb5146e5e6cbc5e9f809f4b6bf2b78d983cbf8484c0bab6d1e9a046ab0a242b
|
|
7
|
+
data.tar.gz: bb3e07e2b8e799067aa475f6b8ffea6a303e797e3eac252f46f597eec751f42451f91fd97eacf024aae8a6129267ef2b425fdd26163a6b893714cb22c2d9ddfa
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] - 2025-12-15
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Inline Suppression Comments** - RuboCop-style comment directives for suppressing false positives
|
|
15
|
+
- `# eager_eye:disable DetectorName` - Disable for single line (inline) or start block
|
|
16
|
+
- `# eager_eye:disable-next-line DetectorName` - Disable only the next line
|
|
17
|
+
- `# eager_eye:disable-file DetectorName` - Disable for entire file (must be in first 5 lines)
|
|
18
|
+
- `# eager_eye:enable DetectorName` - End a disable block
|
|
19
|
+
- Support for multiple detectors: `# eager_eye:disable LoopAssociation, CountInIteration`
|
|
20
|
+
- Support for reason comments: `# eager_eye:disable DetectorName -- reason here`
|
|
21
|
+
- `all` keyword to disable all detectors at once
|
|
22
|
+
- Both CamelCase (`LoopAssociation`) and snake_case (`loop_association`) detector names accepted
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
|
|
26
|
+
- Updated README with inline suppression documentation
|
|
27
|
+
- Added `CommentParser` module for parsing suppression directives
|
|
28
|
+
|
|
29
|
+
## [0.5.0] - 2025-12-15
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- **New Detector: `PluckToArray`** - Detects pluck/map results used in where clauses
|
|
34
|
+
- Catches `.pluck(:id)` and `.ids` results used in `where` clauses
|
|
35
|
+
- Catches `.map(&:id)` and `.collect(&:id)` patterns
|
|
36
|
+
- Suggests using `.select(:id)` subquery pattern for better performance
|
|
37
|
+
- Prevents two queries and memory overhead from holding IDs in arrays
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- Updated default `enabled_detectors` to include `:pluck_to_array`
|
|
42
|
+
- Updated README with new detector documentation and performance comparison
|
|
43
|
+
|
|
10
44
|
## [0.4.0] - 2025-12-15
|
|
11
45
|
|
|
12
46
|
### Added
|
data/README.md
CHANGED
|
@@ -246,6 +246,76 @@ 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
|
+
|
|
275
|
+
## Inline Suppression
|
|
276
|
+
|
|
277
|
+
Suppress false positives using inline comments (RuboCop-style):
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
# Disable for single line
|
|
281
|
+
user.posts.count # eager_eye:disable CountInIteration
|
|
282
|
+
|
|
283
|
+
# Disable for next line
|
|
284
|
+
# eager_eye:disable-next-line LoopAssociation
|
|
285
|
+
@users.each { |u| u.profile }
|
|
286
|
+
|
|
287
|
+
# Disable block
|
|
288
|
+
# eager_eye:disable LoopAssociation, SerializerNesting
|
|
289
|
+
@users.each do |user|
|
|
290
|
+
user.posts.each { |p| p.author }
|
|
291
|
+
end
|
|
292
|
+
# eager_eye:enable LoopAssociation, SerializerNesting
|
|
293
|
+
|
|
294
|
+
# Disable entire file (must be in first 5 lines)
|
|
295
|
+
# eager_eye:disable-file CustomMethodQuery
|
|
296
|
+
|
|
297
|
+
# With reason
|
|
298
|
+
user.posts.count # eager_eye:disable CountInIteration -- using counter_cache
|
|
299
|
+
|
|
300
|
+
# Disable all detectors
|
|
301
|
+
# eager_eye:disable all
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Available Detector Names
|
|
305
|
+
|
|
306
|
+
Both CamelCase and snake_case formats are accepted:
|
|
307
|
+
|
|
308
|
+
| Detector | CamelCase | snake_case |
|
|
309
|
+
|----------|-----------|------------|
|
|
310
|
+
| Loop Association | `LoopAssociation` | `loop_association` |
|
|
311
|
+
| Serializer Nesting | `SerializerNesting` | `serializer_nesting` |
|
|
312
|
+
| Missing Counter Cache | `MissingCounterCache` | `missing_counter_cache` |
|
|
313
|
+
| Custom Method Query | `CustomMethodQuery` | `custom_method_query` |
|
|
314
|
+
| Count in Iteration | `CountInIteration` | `count_in_iteration` |
|
|
315
|
+
| Callback Query | `CallbackQuery` | `callback_query` |
|
|
316
|
+
| Pluck to Array | `PluckToArray` | `pluck_to_array` |
|
|
317
|
+
| All Detectors | `all` | `all` |
|
|
318
|
+
|
|
249
319
|
## Configuration
|
|
250
320
|
|
|
251
321
|
### Config File (.eager_eye.yml)
|
|
@@ -264,6 +334,7 @@ enabled_detectors:
|
|
|
264
334
|
- custom_method_query
|
|
265
335
|
- count_in_iteration
|
|
266
336
|
- callback_query
|
|
337
|
+
- pluck_to_array
|
|
267
338
|
|
|
268
339
|
# Base path to analyze (default: app)
|
|
269
340
|
app_path: app
|
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -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
|
|
@@ -57,8 +58,16 @@ module EagerEye
|
|
|
57
58
|
ast = parse_source(source)
|
|
58
59
|
return unless ast
|
|
59
60
|
|
|
61
|
+
comment_parser = CommentParser.new(source)
|
|
62
|
+
|
|
60
63
|
enabled_detectors.each do |detector|
|
|
61
64
|
file_issues = detector.detect(ast, file_path)
|
|
65
|
+
|
|
66
|
+
# Filter suppressed issues
|
|
67
|
+
file_issues.reject! do |issue|
|
|
68
|
+
comment_parser.disabled_at?(issue.line_number, issue.detector)
|
|
69
|
+
end
|
|
70
|
+
|
|
62
71
|
@issues.concat(file_issues)
|
|
63
72
|
end
|
|
64
73
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
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
|
+
FILE_DISABLE_PATTERN = /eager_eye:disable-file\s+(.+?)(?:\s+--|$)/i
|
|
9
|
+
NEXT_LINE_PATTERN = /eager_eye:disable-next-line\s+(.+?)(?:\s+--|$)/i
|
|
10
|
+
BLOCK_DISABLE_PATTERN = /eager_eye:disable\s+(.+?)(?:\s+--|$)/i
|
|
11
|
+
|
|
12
|
+
def initialize(source_code)
|
|
13
|
+
@source_code = source_code
|
|
14
|
+
@lines = source_code.lines
|
|
15
|
+
@disabled_ranges = Hash.new { |h, k| h[k] = [] }
|
|
16
|
+
@file_disabled = Set.new
|
|
17
|
+
@current_disabled = Set.new
|
|
18
|
+
parse_comments
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def disabled_at?(line_number, detector_name)
|
|
22
|
+
return true if @file_disabled.include?(detector_name.to_s)
|
|
23
|
+
return true if @file_disabled.include?("all")
|
|
24
|
+
|
|
25
|
+
detector = detector_name.to_s
|
|
26
|
+
@disabled_ranges[detector].any? { |range| range.cover?(line_number) } ||
|
|
27
|
+
@disabled_ranges["all"].any? { |range| range.cover?(line_number) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse_comments
|
|
33
|
+
@lines.each_with_index do |line, index|
|
|
34
|
+
line_num = index + 1
|
|
35
|
+
process_line(line, line_num)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
close_unclosed_blocks
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def process_line(line, line_num)
|
|
42
|
+
directive = detect_directive(line, line_num)
|
|
43
|
+
apply_directive(directive, line_num) if directive
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def detect_directive(line, line_num)
|
|
47
|
+
detect_file_directive(line, line_num) ||
|
|
48
|
+
detect_next_line_directive(line) ||
|
|
49
|
+
detect_block_directive(line) ||
|
|
50
|
+
detect_enable_directive(line) ||
|
|
51
|
+
detect_inline_directive(line)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def detect_file_directive(line, line_num)
|
|
55
|
+
return unless line_num <= 5 && line =~ FILE_DISABLE_PATTERN
|
|
56
|
+
|
|
57
|
+
{ type: :file, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def detect_next_line_directive(line)
|
|
61
|
+
return unless line =~ NEXT_LINE_PATTERN
|
|
62
|
+
|
|
63
|
+
{ type: :next_line, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def detect_block_directive(line)
|
|
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)
|
|
73
|
+
return unless line =~ ENABLE_PATTERN
|
|
74
|
+
|
|
75
|
+
{ type: :block_end, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def detect_inline_directive(line)
|
|
79
|
+
return unless line =~ INLINE_DISABLE_PATTERN
|
|
80
|
+
|
|
81
|
+
{ type: :inline, detectors: parse_detector_names(::Regexp.last_match(1)) }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def apply_directive(directive, line_num)
|
|
85
|
+
case directive[:type]
|
|
86
|
+
when :file then apply_file_disable(directive[:detectors])
|
|
87
|
+
when :next_line then apply_next_line_disable(directive[:detectors], line_num)
|
|
88
|
+
when :block_start then apply_block_start(directive[:detectors], line_num)
|
|
89
|
+
when :block_end then apply_block_end(directive[:detectors], line_num)
|
|
90
|
+
when :inline then apply_inline_disable(directive[:detectors], line_num)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def apply_file_disable(detectors)
|
|
95
|
+
@file_disabled.merge(detectors)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def apply_next_line_disable(detectors, line_num)
|
|
99
|
+
next_line = line_num + 1
|
|
100
|
+
detectors.each { |d| @disabled_ranges[d] << (next_line..next_line) }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def apply_block_start(detectors, line_num)
|
|
104
|
+
detectors.each { |d| @current_disabled << { detector: d, start: line_num } }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def apply_block_end(detectors, line_num)
|
|
108
|
+
detectors.each do |d|
|
|
109
|
+
entry = @current_disabled.find { |e| e[:detector] == d }
|
|
110
|
+
next unless entry
|
|
111
|
+
|
|
112
|
+
@disabled_ranges[d] << (entry[:start]..line_num)
|
|
113
|
+
@current_disabled.delete(entry)
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def apply_inline_disable(detectors, line_num)
|
|
118
|
+
detectors.each { |d| @disabled_ranges[d] << (line_num..line_num) }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def close_unclosed_blocks
|
|
122
|
+
@current_disabled.each do |entry|
|
|
123
|
+
@disabled_ranges[entry[:detector]] << (entry[:start]..@lines.size)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def inline_disable?(line)
|
|
128
|
+
code_part = line.split("#").first
|
|
129
|
+
code_part && !code_part.strip.empty?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse_detector_names(str)
|
|
133
|
+
str.split(/[,\s]+/).map(&:strip).reject(&:empty?).map do |name|
|
|
134
|
+
normalize_detector_name(name)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def normalize_detector_name(name)
|
|
139
|
+
name.gsub(/([A-Z])/) { "_#{::Regexp.last_match(1).downcase}" }
|
|
140
|
+
.sub(/^_/, "")
|
|
141
|
+
.downcase
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -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
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -10,6 +10,8 @@ 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"
|
|
14
|
+
require_relative "eager_eye/comment_parser"
|
|
13
15
|
require_relative "eager_eye/analyzer"
|
|
14
16
|
require_relative "eager_eye/reporters/base"
|
|
15
17
|
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
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- lib/eager_eye.rb
|
|
60
60
|
- lib/eager_eye/analyzer.rb
|
|
61
61
|
- lib/eager_eye/cli.rb
|
|
62
|
+
- lib/eager_eye/comment_parser.rb
|
|
62
63
|
- lib/eager_eye/configuration.rb
|
|
63
64
|
- lib/eager_eye/detectors/base.rb
|
|
64
65
|
- lib/eager_eye/detectors/callback_query.rb
|
|
@@ -66,6 +67,7 @@ files:
|
|
|
66
67
|
- lib/eager_eye/detectors/custom_method_query.rb
|
|
67
68
|
- lib/eager_eye/detectors/loop_association.rb
|
|
68
69
|
- lib/eager_eye/detectors/missing_counter_cache.rb
|
|
70
|
+
- lib/eager_eye/detectors/pluck_to_array.rb
|
|
69
71
|
- lib/eager_eye/detectors/serializer_nesting.rb
|
|
70
72
|
- lib/eager_eye/generators/install_generator.rb
|
|
71
73
|
- lib/eager_eye/issue.rb
|