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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/CHANGELOG.md +20 -10
- data/CONTRIBUTING.md +1 -1
- data/README.md +20 -29
- data/SECURITY.md +1 -0
- data/lib/eager_eye/analyzer.rb +33 -14
- data/lib/eager_eye/association_parser.rb +88 -0
- data/lib/eager_eye/auto_fixer.rb +4 -12
- data/lib/eager_eye/cli.rb +2 -7
- data/lib/eager_eye/comment_parser.rb +0 -5
- data/lib/eager_eye/detectors/base.rb +25 -4
- data/lib/eager_eye/detectors/callback_query.rb +11 -45
- data/lib/eager_eye/detectors/count_in_iteration.rb +16 -54
- data/lib/eager_eye/detectors/custom_method_query.rb +25 -91
- data/lib/eager_eye/detectors/loop_association.rb +77 -89
- data/lib/eager_eye/detectors/missing_counter_cache.rb +16 -47
- data/lib/eager_eye/detectors/pluck_to_array.rb +19 -52
- data/lib/eager_eye/detectors/serializer_nesting.rb +20 -70
- data/lib/eager_eye/fixers/pluck_to_select.rb +2 -9
- data/lib/eager_eye/issue.rb +2 -9
- data/lib/eager_eye/railtie.rb +7 -20
- data/lib/eager_eye/reporters/console.rb +7 -18
- data/lib/eager_eye/rspec/matchers.rb +1 -6
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +1 -1
- data/sig/eager_eye.rbs +0 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8b6c076a6dd51cb872bfb213cbb83369a7d9f159bb20054b41901710b738b4df
|
|
4
|
+
data.tar.gz: 15850c235c48858cbbb2585c83ffaeacf1331a029b959f973aede31ef584ae24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0454d92551dcc8d96ae35513ae89c3198a707ee6aac43538c7233d679cb9328c9190c8def794736c53cf61b06419051baa937a9358288a046590de14d15c22b1
|
|
7
|
+
data.tar.gz: bfd0dc368252cf5d0118a5dcf912b981ea02bf6f158a80a59a1fc475f0f5810c302c223c4126dfc639f8a5ad5e8be3fbcfd3ed87d5c21d3e148405a5f5033b1a
|
data/.rubocop.yml
CHANGED
|
@@ -19,6 +19,9 @@ Metrics/BlockLength:
|
|
|
19
19
|
- "*.gemspec"
|
|
20
20
|
- "lib/eager_eye/railtie.rb"
|
|
21
21
|
|
|
22
|
+
Metrics/ClassLength:
|
|
23
|
+
Max: 155
|
|
24
|
+
|
|
22
25
|
Metrics/ParameterLists:
|
|
23
26
|
Max: 6
|
|
24
27
|
|
|
@@ -28,8 +31,5 @@ Metrics/AbcSize:
|
|
|
28
31
|
Metrics/MethodLength:
|
|
29
32
|
Max: 30
|
|
30
33
|
|
|
31
|
-
Metrics/ClassLength:
|
|
32
|
-
Max: 150
|
|
33
|
-
|
|
34
34
|
Layout/FirstArrayElementIndentation:
|
|
35
35
|
EnforcedStyle: consistent
|
data/CHANGELOG.md
CHANGED
|
@@ -7,21 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [1.
|
|
10
|
+
## [1.1.1] - 2026-01-03
|
|
11
11
|
|
|
12
|
-
###
|
|
12
|
+
### Fixed
|
|
13
13
|
|
|
14
|
-
- **
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
14
|
+
- **SerializerNesting False Positive** - No longer flags `belongs_to` associations
|
|
15
|
+
- `user.author`, `subscription.user` etc. (singular) are now ignored
|
|
16
|
+
- Only `has_many` associations (plural names) are flagged as potential N+1
|
|
17
|
+
|
|
18
|
+
## [1.1.0] - 2025-12-28
|
|
18
19
|
|
|
19
20
|
### Added
|
|
20
21
|
|
|
21
|
-
- **
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
22
|
+
- **Association Scope Preloading Detection** - LoopAssociation now recognizes associations with built-in preloading
|
|
23
|
+
- Detects `has_many :posts, -> { includes(:comments) }` patterns
|
|
24
|
+
- Recognizes scope-defined preloads to reduce false positives
|
|
25
|
+
- Parses model files to extract association definitions and preload scopes
|
|
26
|
+
|
|
27
|
+
## [1.0.10] - 2025-12-27
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **PluckToArray Severity Levels** - Scoped pluck is warning, `.all.pluck` is error
|
|
32
|
+
- Scoped `.pluck(:id)` → **Warning** (acceptable for small arrays)
|
|
33
|
+
- Unscoped `.all.pluck(:id)` → **Error** (loads entire table)
|
|
34
|
+
- Improved detection and suggestions for critical patterns
|
|
25
35
|
|
|
26
36
|
## [1.0.9] - 2025-12-26
|
|
27
37
|
|
data/CONTRIBUTING.md
CHANGED
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.
|
|
13
|
+
<a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.1.1-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>
|
|
@@ -153,6 +153,17 @@ posts.each { |post| post.comments.size } # No warning
|
|
|
153
153
|
@user.posts.each do |post|
|
|
154
154
|
post.comments # No warning - single user, no N+1
|
|
155
155
|
end
|
|
156
|
+
|
|
157
|
+
# Scope-defined preloads are recognized (v1.1.0+)
|
|
158
|
+
# In Post model:
|
|
159
|
+
class Post < ApplicationRecord
|
|
160
|
+
has_many :comments, -> { includes(:author) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# In controller - EagerEye recognizes comments are already preloaded via scope!
|
|
164
|
+
posts.each do |post|
|
|
165
|
+
post.comments.map(&:author) # No warning - preloaded via scope
|
|
166
|
+
end
|
|
156
167
|
```
|
|
157
168
|
|
|
158
169
|
### 2. Serializer Nesting (N+1 in serializers)
|
|
@@ -313,40 +324,20 @@ Detects when `.pluck(:id)` or `.map(&:id)` results are used in `where` clauses i
|
|
|
313
324
|
|
|
314
325
|
```ruby
|
|
315
326
|
# Bad - Two queries + memory overhead
|
|
316
|
-
user_ids = User.active.pluck(:id)
|
|
317
|
-
Post.where(user_id: user_ids)
|
|
318
|
-
# Also holds potentially thousands of IDs in memory
|
|
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 (...)
|
|
327
|
+
user_ids = User.active.pluck(:id)
|
|
328
|
+
Post.where(user_id: user_ids) # ⚠️ Warning
|
|
323
329
|
|
|
324
|
-
#
|
|
325
|
-
user_ids =
|
|
330
|
+
# Worse - Loads entire table! 🔴 Error
|
|
331
|
+
user_ids = User.all.pluck(:id)
|
|
326
332
|
Post.where(user_id: user_ids)
|
|
327
333
|
|
|
328
|
-
# Good - Single subquery
|
|
334
|
+
# Good - Single subquery
|
|
329
335
|
Post.where(user_id: User.active.select(:id))
|
|
330
|
-
# Single query: SELECT * FROM posts WHERE user_id IN (SELECT id FROM users WHERE active = true)
|
|
331
336
|
```
|
|
332
337
|
|
|
333
|
-
**Severity
|
|
334
|
-
|
|
335
|
-
-
|
|
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
|
-
|
|
343
|
-
**Performance comparison with 10,000 users:**
|
|
344
|
-
|
|
345
|
-
| Approach | Queries | Memory | Time |
|
|
346
|
-
|----------|---------|--------|------|
|
|
347
|
-
| `pluck` + `where` | 2 | ~80KB for IDs | ~45ms |
|
|
348
|
-
| `.all.pluck` + `where` | 2 | ~40KB+ for all IDs | ~100ms+ |
|
|
349
|
-
| `select` subquery | 1 | None | ~20ms |
|
|
338
|
+
**Severity:**
|
|
339
|
+
- ⚠️ **Warning** - Scoped `.pluck(:id)` (two queries, memory overhead)
|
|
340
|
+
- 🔴 **Error** - Unscoped `.all.pluck(:id)` (loads entire table)
|
|
350
341
|
|
|
351
342
|
## Inline Suppression
|
|
352
343
|
|
data/SECURITY.md
CHANGED
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -14,24 +14,47 @@ module EagerEye
|
|
|
14
14
|
pluck_to_array: Detectors::PluckToArray
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
-
attr_reader :paths, :issues
|
|
17
|
+
attr_reader :paths, :issues, :association_preloads
|
|
18
18
|
|
|
19
19
|
def initialize(paths: nil)
|
|
20
20
|
@paths = Array(paths || EagerEye.configuration.app_path)
|
|
21
21
|
@issues = []
|
|
22
|
+
@association_preloads = {}
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def run
|
|
25
26
|
@issues = []
|
|
27
|
+
collect_association_preloads
|
|
28
|
+
analyze_files
|
|
29
|
+
@issues
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
def collect_association_preloads
|
|
35
|
+
model_files.each do |file_path|
|
|
36
|
+
ast = parse_source(File.read(file_path))
|
|
37
|
+
next unless ast
|
|
38
|
+
|
|
39
|
+
parser = AssociationParser.new
|
|
40
|
+
parser.parse_model(ast, extract_model_name(file_path))
|
|
41
|
+
@association_preloads.merge!(parser.preloaded_associations)
|
|
29
42
|
end
|
|
43
|
+
rescue StandardError
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
def model_files
|
|
48
|
+
Dir.glob(File.join(@paths[0], "models", "**", "*.rb"))
|
|
32
49
|
end
|
|
33
50
|
|
|
34
|
-
|
|
51
|
+
def extract_model_name(file_path)
|
|
52
|
+
File.basename(file_path, ".rb").camelize
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def analyze_files
|
|
56
|
+
ruby_files.each { |file_path| analyze_file(file_path) }
|
|
57
|
+
end
|
|
35
58
|
|
|
36
59
|
def ruby_files
|
|
37
60
|
all_files = paths.flat_map do |path|
|
|
@@ -59,19 +82,15 @@ module EagerEye
|
|
|
59
82
|
return unless ast
|
|
60
83
|
|
|
61
84
|
comment_parser = CommentParser.new(source)
|
|
85
|
+
min_severity = EagerEye.configuration.min_severity
|
|
62
86
|
|
|
63
87
|
enabled_detectors.each do |detector|
|
|
64
|
-
|
|
88
|
+
args = [ast, file_path]
|
|
89
|
+
args << @association_preloads if detector.is_a?(Detectors::LoopAssociation)
|
|
65
90
|
|
|
66
|
-
|
|
67
|
-
file_issues.reject!
|
|
68
|
-
comment_parser.disabled_at?(issue.line_number, issue.detector)
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
# Filter by minimum severity
|
|
72
|
-
min_severity = EagerEye.configuration.min_severity
|
|
91
|
+
file_issues = detector.detect(*args)
|
|
92
|
+
file_issues.reject! { |issue| comment_parser.disabled_at?(issue.line_number, issue.detector) }
|
|
73
93
|
file_issues.select! { |issue| issue.meets_minimum_severity?(min_severity) }
|
|
74
|
-
|
|
75
94
|
@issues.concat(file_issues)
|
|
76
95
|
end
|
|
77
96
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
class AssociationParser
|
|
5
|
+
ASSOCIATION_METHODS = %i[has_many has_one belongs_to has_and_belongs_to_many].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :preloaded_associations
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@preloaded_associations = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse_model(ast, model_name)
|
|
14
|
+
return unless ast
|
|
15
|
+
|
|
16
|
+
traverse(ast, model_name)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def traverse(node, model_name)
|
|
22
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
23
|
+
|
|
24
|
+
check_association_definition(node, model_name)
|
|
25
|
+
node.children.each { |child| traverse(child, model_name) }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def check_association_definition(node, model_name)
|
|
29
|
+
return unless node.type == :send
|
|
30
|
+
return unless node.children[0].nil? # receiver is nil (class context)
|
|
31
|
+
|
|
32
|
+
method_name = node.children[1]
|
|
33
|
+
return unless ASSOCIATION_METHODS.include?(method_name)
|
|
34
|
+
|
|
35
|
+
association_name = extract_association_name(node)
|
|
36
|
+
return unless association_name
|
|
37
|
+
|
|
38
|
+
preloaded = extract_preloaded_associations(node)
|
|
39
|
+
return if preloaded.empty?
|
|
40
|
+
|
|
41
|
+
@preloaded_associations["#{model_name}##{association_name}"] = preloaded
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def extract_association_name(node)
|
|
45
|
+
first_arg = node.children[2]
|
|
46
|
+
first_arg&.type == :sym ? first_arg.children[0] : nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def extract_preloaded_associations(node)
|
|
50
|
+
preloaded = Set.new
|
|
51
|
+
block_node = node.children[2..].find { |arg| arg&.type == :block }
|
|
52
|
+
traverse_for_preloads(block_node&.children&.[](2), preloaded) if block_node
|
|
53
|
+
preloaded
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def traverse_for_preloads(node, preloaded)
|
|
57
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
58
|
+
|
|
59
|
+
extract_includes_from_method(node, preloaded) if preload_call?(node)
|
|
60
|
+
|
|
61
|
+
node.children.each { |child| traverse_for_preloads(child, preloaded) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def preload_call?(node)
|
|
65
|
+
node.type == :send && %i[includes preload eager_load].include?(node.children[1])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def extract_includes_from_method(node, included)
|
|
69
|
+
node.children[2..].each { |arg| add_included_sym(arg, included) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def add_included_sym(arg, included)
|
|
73
|
+
case arg&.type
|
|
74
|
+
when :sym
|
|
75
|
+
included << arg.children[0]
|
|
76
|
+
when :hash
|
|
77
|
+
arg.children.each { |pair| extract_sym_from_pair(pair, included) }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def extract_sym_from_pair(pair, included)
|
|
82
|
+
return unless pair.type == :pair
|
|
83
|
+
|
|
84
|
+
key = pair.children[0]
|
|
85
|
+
included << key.children[0] if key&.type == :sym
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
data/lib/eager_eye/auto_fixer.rb
CHANGED
|
@@ -12,11 +12,7 @@ module EagerEye
|
|
|
12
12
|
fixes = collect_fixes
|
|
13
13
|
return puts "No auto-fixable issues found." if fixes.empty?
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
apply_interactively(fixes)
|
|
17
|
-
else
|
|
18
|
-
apply_all(fixes)
|
|
19
|
-
end
|
|
15
|
+
@interactive ? apply_interactively(fixes) : apply_all(fixes)
|
|
20
16
|
end
|
|
21
17
|
|
|
22
18
|
def suggest
|
|
@@ -37,12 +33,9 @@ module EagerEye
|
|
|
37
33
|
|
|
38
34
|
def collect_fixes
|
|
39
35
|
@issues.filter_map do |issue|
|
|
40
|
-
|
|
41
|
-
fixer
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
fixer.diff
|
|
45
|
-
end.compact
|
|
36
|
+
fixer = FixerRegistry.fixer_for(issue, read_file(issue.file_path))
|
|
37
|
+
fixer&.fixable? ? fixer.diff : nil
|
|
38
|
+
end
|
|
46
39
|
end
|
|
47
40
|
|
|
48
41
|
def read_file(path)
|
|
@@ -71,7 +64,6 @@ module EagerEye
|
|
|
71
64
|
end
|
|
72
65
|
|
|
73
66
|
def apply_all(fixes)
|
|
74
|
-
# Group by file to minimize file operations
|
|
75
67
|
fixes.group_by { |f| f[:file] }.each do |file, file_fixes|
|
|
76
68
|
lines = File.readlines(file)
|
|
77
69
|
|
data/lib/eager_eye/cli.rb
CHANGED
|
@@ -137,9 +137,7 @@ module EagerEye
|
|
|
137
137
|
|
|
138
138
|
def analyze
|
|
139
139
|
configure_from_options!
|
|
140
|
-
|
|
141
|
-
analyzer = Analyzer.new(paths: options[:paths])
|
|
142
|
-
analyzer.run
|
|
140
|
+
Analyzer.new(paths: options[:paths]).run
|
|
143
141
|
end
|
|
144
142
|
|
|
145
143
|
def configure_from_options!
|
|
@@ -165,10 +163,7 @@ module EagerEye
|
|
|
165
163
|
end
|
|
166
164
|
|
|
167
165
|
def exit_code(issues)
|
|
168
|
-
|
|
169
|
-
return 0 if issues.empty?
|
|
170
|
-
|
|
171
|
-
1
|
|
166
|
+
options[:fail_on_issues] && issues.any? ? 1 : 0
|
|
172
167
|
end
|
|
173
168
|
end
|
|
174
169
|
end
|
|
@@ -131,11 +131,6 @@ module EagerEye
|
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
def inline_disable?(line)
|
|
135
|
-
code_part = line.split("#").first
|
|
136
|
-
code_part && !code_part.strip.empty?
|
|
137
|
-
end
|
|
138
|
-
|
|
139
134
|
def code_before_comment?(line)
|
|
140
135
|
code_part = line.split("#").first
|
|
141
136
|
code_part && !code_part.strip.empty?
|
|
@@ -41,10 +41,7 @@ module EagerEye
|
|
|
41
41
|
return unless node.is_a?(Parser::AST::Node)
|
|
42
42
|
|
|
43
43
|
yield node
|
|
44
|
-
|
|
45
|
-
node.children.each do |child|
|
|
46
|
-
traverse_ast(child, &block)
|
|
47
|
-
end
|
|
44
|
+
node.children.each { |child| traverse_ast(child, &block) }
|
|
48
45
|
end
|
|
49
46
|
|
|
50
47
|
def parse_source(source)
|
|
@@ -52,6 +49,30 @@ module EagerEye
|
|
|
52
49
|
rescue Parser::SyntaxError
|
|
53
50
|
nil
|
|
54
51
|
end
|
|
52
|
+
|
|
53
|
+
def extract_method_args(node)
|
|
54
|
+
return [] unless node&.type == :send
|
|
55
|
+
|
|
56
|
+
node.children[2..]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def extract_symbols_from_args(args)
|
|
60
|
+
symbols = Set.new
|
|
61
|
+
args.each do |arg|
|
|
62
|
+
case arg&.type
|
|
63
|
+
when :sym then symbols.add(arg.children[0])
|
|
64
|
+
when :hash then extract_symbols_from_hash(arg, symbols)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
symbols
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def extract_symbols_from_hash(hash_node, symbols)
|
|
71
|
+
hash_node.children.each do |pair|
|
|
72
|
+
key = pair.children[0]
|
|
73
|
+
symbols.add(key.children[0]) if key&.type == :sym
|
|
74
|
+
end
|
|
75
|
+
end
|
|
55
76
|
end
|
|
56
77
|
end
|
|
57
78
|
end
|
|
@@ -51,18 +51,11 @@ module EagerEye
|
|
|
51
51
|
return unless node.is_a?(Parser::AST::Node)
|
|
52
52
|
|
|
53
53
|
extract_callback_method_name(node) if callback_definition?(node)
|
|
54
|
-
|
|
55
|
-
node.children.each do |child|
|
|
56
|
-
find_callback_definitions(child)
|
|
57
|
-
end
|
|
54
|
+
node.children.each { |child| find_callback_definitions(child) }
|
|
58
55
|
end
|
|
59
56
|
|
|
60
57
|
def callback_definition?(node)
|
|
61
|
-
|
|
62
|
-
return false unless node.children[0].nil?
|
|
63
|
-
|
|
64
|
-
method_name = node.children[1]
|
|
65
|
-
CALLBACK_METHODS.include?(method_name)
|
|
58
|
+
node.type == :send && node.children[0].nil? && CALLBACK_METHODS.include?(node.children[1])
|
|
66
59
|
end
|
|
67
60
|
|
|
68
61
|
def extract_callback_method_name(node)
|
|
@@ -78,28 +71,13 @@ module EagerEye
|
|
|
78
71
|
def check_callback_methods(node)
|
|
79
72
|
return unless node.is_a?(Parser::AST::Node)
|
|
80
73
|
|
|
81
|
-
if
|
|
74
|
+
if node.type == :def && @callback_methods.key?(node.children[0])
|
|
82
75
|
method_name = node.children[0]
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
check_method_body_for_queries(node, method_name, callback_type)
|
|
86
|
-
end
|
|
76
|
+
body = node.children[2]
|
|
77
|
+
find_iterations_with_queries(body, method_name, @callback_methods[method_name]) if body
|
|
87
78
|
end
|
|
88
79
|
|
|
89
|
-
node.children.each
|
|
90
|
-
check_callback_methods(child)
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def method_definition?(node)
|
|
95
|
-
node.type == :def
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def check_method_body_for_queries(method_node, method_name, callback_type)
|
|
99
|
-
method_body = method_node.children[2]
|
|
100
|
-
return unless method_body
|
|
101
|
-
|
|
102
|
-
find_iterations_with_queries(method_body, method_name, callback_type)
|
|
80
|
+
node.children.each { |child| check_callback_methods(child) }
|
|
103
81
|
end
|
|
104
82
|
|
|
105
83
|
def find_iterations_with_queries(node, method_name, callback_type)
|
|
@@ -111,9 +89,7 @@ module EagerEye
|
|
|
111
89
|
find_query_calls_in_block(node, method_name, callback_type, block_var) if block_var
|
|
112
90
|
end
|
|
113
91
|
|
|
114
|
-
node.children.each
|
|
115
|
-
find_iterations_with_queries(child, method_name, callback_type)
|
|
116
|
-
end
|
|
92
|
+
node.children.each { |child| find_iterations_with_queries(child, method_name, callback_type) }
|
|
117
93
|
end
|
|
118
94
|
|
|
119
95
|
def find_query_calls_in_block(node, method_name, callback_type, block_var)
|
|
@@ -123,26 +99,16 @@ module EagerEye
|
|
|
123
99
|
add_query_issue(node, method_name, callback_type)
|
|
124
100
|
end
|
|
125
101
|
|
|
126
|
-
node.children.each
|
|
127
|
-
find_query_calls_in_block(child, method_name, callback_type, block_var)
|
|
128
|
-
end
|
|
102
|
+
node.children.each { |child| find_query_calls_in_block(child, method_name, callback_type, block_var) }
|
|
129
103
|
end
|
|
130
104
|
|
|
131
105
|
def query_call?(node)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
method = node.children[1]
|
|
135
|
-
QUERY_INDICATORS.include?(method)
|
|
106
|
+
node.type == :send && QUERY_INDICATORS.include?(node.children[1])
|
|
136
107
|
end
|
|
137
108
|
|
|
138
109
|
def iteration_block?(node)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
send_node = node.children[0]
|
|
142
|
-
return false unless send_node&.type == :send
|
|
143
|
-
|
|
144
|
-
method_name = send_node.children[1]
|
|
145
|
-
ITERATION_METHODS.include?(method_name)
|
|
110
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
111
|
+
ITERATION_METHODS.include?(node.children[0].children[1])
|
|
146
112
|
end
|
|
147
113
|
|
|
148
114
|
def add_query_issue(node, method_name, callback_type)
|
|
@@ -3,14 +3,8 @@
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
module Detectors
|
|
5
5
|
class CountInIteration < Base
|
|
6
|
-
# count always executes a COUNT query
|
|
7
|
-
# size and length use memory when collection is loaded
|
|
8
6
|
COUNT_METHODS = %i[count].freeze
|
|
9
|
-
|
|
10
|
-
ITERATION_METHODS = %i[
|
|
11
|
-
each map select find_all reject collect
|
|
12
|
-
each_with_index each_with_object flat_map
|
|
13
|
-
].freeze
|
|
7
|
+
ITERATION_METHODS = %i[each map select find_all reject collect each_with_index each_with_object flat_map].freeze
|
|
14
8
|
|
|
15
9
|
def self.detector_name
|
|
16
10
|
:count_in_iteration
|
|
@@ -36,74 +30,48 @@ module EagerEye
|
|
|
36
30
|
|
|
37
31
|
if iteration_block?(node)
|
|
38
32
|
block_var = extract_block_variable(node)
|
|
39
|
-
block_body =
|
|
33
|
+
block_body = node.children[2]
|
|
40
34
|
yield(block_body, block_var) if block_var && block_body
|
|
41
35
|
end
|
|
42
36
|
|
|
43
|
-
node.children.each
|
|
44
|
-
find_iteration_blocks(child, &block)
|
|
45
|
-
end
|
|
37
|
+
node.children.each { |child| find_iteration_blocks(child, &block) }
|
|
46
38
|
end
|
|
47
39
|
|
|
48
40
|
def iteration_block?(node)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
send_node = node.children[0]
|
|
52
|
-
return false unless send_node&.type == :send
|
|
53
|
-
|
|
54
|
-
method_name = send_node.children[1]
|
|
55
|
-
ITERATION_METHODS.include?(method_name)
|
|
41
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
42
|
+
ITERATION_METHODS.include?(node.children[0].children[1])
|
|
56
43
|
end
|
|
57
44
|
|
|
58
45
|
def check_for_count_calls(node, block_var)
|
|
59
46
|
return unless node.is_a?(Parser::AST::Node)
|
|
60
47
|
|
|
61
48
|
add_issue(node) if count_on_association?(node, block_var)
|
|
62
|
-
|
|
63
|
-
node.children.each do |child|
|
|
64
|
-
check_for_count_calls(child, block_var)
|
|
65
|
-
end
|
|
49
|
+
node.children.each { |child| check_for_count_calls(child, block_var) }
|
|
66
50
|
end
|
|
67
51
|
|
|
68
52
|
def count_on_association?(node, block_var)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
method_name = node.children[1]
|
|
72
|
-
return false unless COUNT_METHODS.include?(method_name)
|
|
73
|
-
|
|
74
|
-
receiver = node.children[0]
|
|
75
|
-
association_call_on_block_var?(receiver, block_var)
|
|
53
|
+
node.type == :send && COUNT_METHODS.include?(node.children[1]) &&
|
|
54
|
+
association_call_on_block_var?(node.children[0], block_var)
|
|
76
55
|
end
|
|
77
56
|
|
|
78
57
|
def association_call_on_block_var?(node, block_var)
|
|
79
|
-
return false unless node.is_a?(Parser::AST::Node)
|
|
80
|
-
return false unless node.type == :send
|
|
58
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :send
|
|
81
59
|
|
|
82
60
|
receiver = node.children[0]
|
|
83
61
|
return false unless receiver.is_a?(Parser::AST::Node)
|
|
84
62
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
true
|
|
89
|
-
elsif receiver.type == :send
|
|
90
|
-
# Nested: post.author.posts.count
|
|
91
|
-
chain_starts_with_block_var?(receiver, block_var)
|
|
92
|
-
else
|
|
93
|
-
false
|
|
94
|
-
end
|
|
63
|
+
return true if receiver.type == :lvar && receiver.children[0] == block_var
|
|
64
|
+
|
|
65
|
+
receiver.type == :send && chain_starts_with_block_var?(receiver, block_var)
|
|
95
66
|
end
|
|
96
67
|
|
|
97
68
|
def chain_starts_with_block_var?(node, block_var)
|
|
98
69
|
return false unless node.is_a?(Parser::AST::Node)
|
|
99
70
|
|
|
100
71
|
case node.type
|
|
101
|
-
when :lvar
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
chain_starts_with_block_var?(node.children[0], block_var)
|
|
105
|
-
else
|
|
106
|
-
false
|
|
72
|
+
when :lvar then node.children[0] == block_var
|
|
73
|
+
when :send then chain_starts_with_block_var?(node.children[0], block_var)
|
|
74
|
+
else false
|
|
107
75
|
end
|
|
108
76
|
end
|
|
109
77
|
|
|
@@ -112,13 +80,7 @@ module EagerEye
|
|
|
112
80
|
return nil unless args_node&.type == :args
|
|
113
81
|
|
|
114
82
|
first_arg = args_node.children[0]
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
first_arg.children[0]
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def extract_block_body(block_node)
|
|
121
|
-
block_node.children[2]
|
|
83
|
+
first_arg&.type == :arg ? first_arg.children[0] : nil
|
|
122
84
|
end
|
|
123
85
|
|
|
124
86
|
def add_issue(node)
|