eager_eye 1.2.4 → 1.2.5
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 +1 -1
- data/CHANGELOG.md +11 -0
- data/README.md +34 -2
- data/lib/eager_eye/analyzer.rb +2 -1
- data/lib/eager_eye/configuration.rb +3 -2
- data/lib/eager_eye/detectors/callback_query.rb +38 -3
- data/lib/eager_eye/detectors/count_in_iteration.rb +9 -0
- data/lib/eager_eye/detectors/custom_method_query.rb +6 -1
- data/lib/eager_eye/detectors/decorator_n_plus_one.rb +137 -0
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +1 -0
- 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: 6fa0e6e3acb2c37751e7ab49a58826a016a68c7a33cc6305ddb6b82d6787c462
|
|
4
|
+
data.tar.gz: af81d8c0cfde13914b00a9f64b4b1bef6731b12902a1358c7f27473c2ab9a68d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a0e233e5c53c6ee3c7a32239ca20625a58deb1b385af022cb582433d02070f7b861873f44cef5fe786ede0307caee4105202e49844902e8d75e14039623bdc74
|
|
7
|
+
data.tar.gz: 238ca3ba9a777686867752bc4abbf1d10081d73215e3c0f7ed0e9360f7c380298b4736177c703f1c921fb22b4296e7544379bef69021408f9e9ad86280a3716c
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.5] - 2026-02-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **New Detector: `DecoratorNPlusOne`** - Detects N+1 queries inside decorator/presenter classes
|
|
15
|
+
- Catches `object.comments.map(...)`, `__getobj__.items.each { ... }`, `model.posts`, `source.tags` patterns
|
|
16
|
+
- Identifies decorator classes by inheritance (`Draper::Decorator`, `SimpleDelegator`, `Delegator`) or name suffix (`Decorator`, `Presenter`, `ViewObject`)
|
|
17
|
+
- Targets all four object reference styles: `object`, `__getobj__`, `source`, `model`
|
|
18
|
+
- Skips ActiveStorage methods (`attached?`, `blob`, `variant`, etc.) to prevent false positives
|
|
19
|
+
- Suggests eager loading in the controller before decorating the collection
|
|
20
|
+
|
|
10
21
|
## [1.2.4] - 2026-02-21
|
|
11
22
|
|
|
12
23
|
### 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.2.
|
|
13
|
+
<a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.2.5-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>
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
|
|
43
43
|
## Features
|
|
44
44
|
|
|
45
|
-
✨ **Detects
|
|
45
|
+
✨ **Detects 9 types of N+1 problems:**
|
|
46
46
|
- Loop associations (queries in iterations)
|
|
47
47
|
- Serializer nesting issues
|
|
48
48
|
- Missing counter caches
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
- Callback query N+1s
|
|
52
52
|
- Pluck to array misuse
|
|
53
53
|
- Delegation N+1s (hidden via `delegate :method, to: :association`)
|
|
54
|
+
- Decorator N+1s (Draper, SimpleDelegator, Presenter, ViewObject)
|
|
54
55
|
|
|
55
56
|
🔧 **Developer-friendly:**
|
|
56
57
|
- Inline suppression (like RuboCop)
|
|
@@ -369,6 +370,34 @@ EagerEye detects these by:
|
|
|
369
370
|
2. Tracking which methods delegate to which associations
|
|
370
371
|
3. Flagging calls to those methods inside iteration blocks when the association is not preloaded
|
|
371
372
|
|
|
373
|
+
### 9. Decorator N+1
|
|
374
|
+
|
|
375
|
+
Detects N+1 queries inside Draper decorators, SimpleDelegator subclasses, and classes named `Decorator`, `Presenter`, or `ViewObject`. Each decorator wraps a single record — when a collection is decorated without preloading, every method that accesses an association triggers a new query per record.
|
|
376
|
+
|
|
377
|
+
```ruby
|
|
378
|
+
# Bad - N+1 on each decorated post
|
|
379
|
+
class PostDecorator < Draper::Decorator
|
|
380
|
+
def comment_summary
|
|
381
|
+
object.comments.map(&:body).join(", ") # Query for each post!
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def tag_list
|
|
385
|
+
object.tags.map(&:name).join(", ") # Another query for each post!
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Controller - no includes = N+1
|
|
390
|
+
@posts = Post.all.decorate
|
|
391
|
+
|
|
392
|
+
# Good - Eager load before decorating
|
|
393
|
+
@posts = Post.includes(:comments, :tags).all.decorate
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Supports the following object references inside decorators:
|
|
397
|
+
- `object` — Draper standard
|
|
398
|
+
- `__getobj__` — SimpleDelegator standard
|
|
399
|
+
- `source`, `model` — alternative Draper aliases
|
|
400
|
+
|
|
372
401
|
## Inline Suppression
|
|
373
402
|
|
|
374
403
|
Suppress false positives using inline comments (RuboCop-style):
|
|
@@ -412,6 +441,7 @@ Both CamelCase and snake_case formats are accepted:
|
|
|
412
441
|
| Callback Query | `CallbackQuery` | `callback_query` |
|
|
413
442
|
| Pluck to Array | `PluckToArray` | `pluck_to_array` |
|
|
414
443
|
| Delegation N+1 | `DelegationNPlusOne` | `delegation_n_plus_one` |
|
|
444
|
+
| Decorator N+1 | `DecoratorNPlusOne` | `decorator_n_plus_one` |
|
|
415
445
|
| All Detectors | `all` | `all` |
|
|
416
446
|
|
|
417
447
|
## Auto-fix (Experimental)
|
|
@@ -515,6 +545,7 @@ enabled_detectors:
|
|
|
515
545
|
- callback_query
|
|
516
546
|
- pluck_to_array
|
|
517
547
|
- delegation_n_plus_one
|
|
548
|
+
- decorator_n_plus_one
|
|
518
549
|
|
|
519
550
|
# Severity levels per detector (error, warning, info)
|
|
520
551
|
severity_levels:
|
|
@@ -525,6 +556,7 @@ severity_levels:
|
|
|
525
556
|
callback_query: warning
|
|
526
557
|
pluck_to_array: warning # Optimization
|
|
527
558
|
delegation_n_plus_one: warning # Hidden delegation N+1
|
|
559
|
+
decorator_n_plus_one: warning # Decorator/Presenter N+1
|
|
528
560
|
missing_counter_cache: info # Suggestion
|
|
529
561
|
|
|
530
562
|
# Minimum severity to report (default: info)
|
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -12,7 +12,8 @@ module EagerEye
|
|
|
12
12
|
count_in_iteration: Detectors::CountInIteration,
|
|
13
13
|
callback_query: Detectors::CallbackQuery,
|
|
14
14
|
pluck_to_array: Detectors::PluckToArray,
|
|
15
|
-
delegation_n_plus_one: Detectors::DelegationNPlusOne
|
|
15
|
+
delegation_n_plus_one: Detectors::DelegationNPlusOne,
|
|
16
|
+
decorator_n_plus_one: Detectors::DecoratorNPlusOne
|
|
16
17
|
}.freeze
|
|
17
18
|
|
|
18
19
|
attr_reader :paths, :issues, :association_preloads, :delegation_maps
|
|
@@ -8,7 +8,7 @@ module EagerEye
|
|
|
8
8
|
DEFAULT_DETECTORS = %i[
|
|
9
9
|
loop_association serializer_nesting missing_counter_cache
|
|
10
10
|
custom_method_query count_in_iteration callback_query
|
|
11
|
-
pluck_to_array delegation_n_plus_one
|
|
11
|
+
pluck_to_array delegation_n_plus_one decorator_n_plus_one
|
|
12
12
|
].freeze
|
|
13
13
|
|
|
14
14
|
DEFAULT_SEVERITY_LEVELS = {
|
|
@@ -19,7 +19,8 @@ module EagerEye
|
|
|
19
19
|
count_in_iteration: :warning,
|
|
20
20
|
callback_query: :warning,
|
|
21
21
|
pluck_to_array: :warning,
|
|
22
|
-
delegation_n_plus_one: :warning
|
|
22
|
+
delegation_n_plus_one: :warning,
|
|
23
|
+
decorator_n_plus_one: :warning
|
|
23
24
|
}.freeze
|
|
24
25
|
|
|
25
26
|
VALID_SEVERITIES = %i[info warning error].freeze
|
|
@@ -29,6 +29,9 @@ module EagerEye
|
|
|
29
29
|
ITERATION_METHODS = %i[each map select find_all reject collect
|
|
30
30
|
find_each find_in_batches in_batches].freeze
|
|
31
31
|
AR_BATCH_METHODS = %i[find_each find_in_batches in_batches].freeze
|
|
32
|
+
NON_AR_NAMESPACES = %w[Sidekiq Redis ActionCable ActionMailer Kafka].freeze
|
|
33
|
+
TRANSACTIONAL_CALLBACKS = %i[before_validation before_save before_create before_update before_destroy
|
|
34
|
+
around_save around_create around_update around_destroy].freeze
|
|
32
35
|
|
|
33
36
|
def self.detector_name
|
|
34
37
|
:callback_query
|
|
@@ -87,7 +90,8 @@ module EagerEye
|
|
|
87
90
|
|
|
88
91
|
if iteration_block?(node)
|
|
89
92
|
block_var = extract_block_variable(node)
|
|
90
|
-
|
|
93
|
+
collection = node.children[0].children[0]
|
|
94
|
+
if block_var && !non_ar_collection?(collection) && contains_ar_query_on_variable?(node, block_var)
|
|
91
95
|
add_iteration_issue(node, method_name, callback_type)
|
|
92
96
|
find_query_calls_in_block(node, method_name, callback_type, block_var)
|
|
93
97
|
end
|
|
@@ -128,26 +132,41 @@ module EagerEye
|
|
|
128
132
|
|
|
129
133
|
def add_query_issue(node, method_name, callback_type)
|
|
130
134
|
query_method = node.children[1]
|
|
135
|
+
suggestion = if transactional_callback?(callback_type)
|
|
136
|
+
"Callbacks run on every save/create/update. Move the query outside the iteration or preload data"
|
|
137
|
+
else
|
|
138
|
+
"Callbacks run on every save/create/update. Consider moving to a background job"
|
|
139
|
+
end
|
|
131
140
|
|
|
132
141
|
@issues << create_issue(
|
|
133
142
|
file_path: @file_path,
|
|
134
143
|
line_number: node.loc.line,
|
|
135
144
|
message: "Query method `.#{query_method}` found in `#{callback_type}` callback `:#{method_name}`",
|
|
136
145
|
severity: :warning,
|
|
137
|
-
suggestion:
|
|
146
|
+
suggestion: suggestion
|
|
138
147
|
)
|
|
139
148
|
end
|
|
140
149
|
|
|
141
150
|
def add_iteration_issue(node, method_name, callback_type)
|
|
151
|
+
suggestion = if transactional_callback?(callback_type)
|
|
152
|
+
"Avoid DB queries in before_*/around_* callbacks. Preload data outside the iteration instead"
|
|
153
|
+
else
|
|
154
|
+
"Avoid iterations in callbacks. Use background jobs for bulk operations"
|
|
155
|
+
end
|
|
156
|
+
|
|
142
157
|
@issues << create_issue(
|
|
143
158
|
file_path: @file_path,
|
|
144
159
|
line_number: node.loc.line,
|
|
145
160
|
message: "Iteration found in `#{callback_type}` callback `:#{method_name}` - potential N+1",
|
|
146
161
|
severity: :error,
|
|
147
|
-
suggestion:
|
|
162
|
+
suggestion: suggestion
|
|
148
163
|
)
|
|
149
164
|
end
|
|
150
165
|
|
|
166
|
+
def transactional_callback?(callback_type)
|
|
167
|
+
TRANSACTIONAL_CALLBACKS.include?(callback_type)
|
|
168
|
+
end
|
|
169
|
+
|
|
151
170
|
def extract_block_variable(block_node)
|
|
152
171
|
args_node = block_node.children[1]
|
|
153
172
|
return nil unless args_node&.type == :args
|
|
@@ -171,6 +190,22 @@ module EagerEye
|
|
|
171
190
|
end
|
|
172
191
|
end
|
|
173
192
|
|
|
193
|
+
def non_ar_collection?(node)
|
|
194
|
+
ns = root_namespace(node)
|
|
195
|
+
ns && NON_AR_NAMESPACES.include?(ns)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def root_namespace(node)
|
|
199
|
+
return nil unless node.is_a?(Parser::AST::Node)
|
|
200
|
+
|
|
201
|
+
case node.type
|
|
202
|
+
when :const
|
|
203
|
+
node.children[0].nil? ? node.children[1].to_s : root_namespace(node.children[0])
|
|
204
|
+
when :send, :block
|
|
205
|
+
root_namespace(node.children[0])
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
174
209
|
def contains_ar_query_on_variable?(node, block_var)
|
|
175
210
|
return false unless node.is_a?(Parser::AST::Node)
|
|
176
211
|
return true if query_call?(node) && receiver_chain_starts_with?(node.children[0], block_var)
|
|
@@ -6,6 +6,7 @@ module EagerEye
|
|
|
6
6
|
COUNT_METHODS = %i[count].freeze
|
|
7
7
|
ITERATION_METHODS = %i[each map select find_all reject collect each_with_index each_with_object flat_map
|
|
8
8
|
find_each find_in_batches in_batches].freeze
|
|
9
|
+
ARRAY_METHOD_SUFFIXES = %w[_ids _tags _types _codes _names _values].freeze
|
|
9
10
|
|
|
10
11
|
def self.detector_name
|
|
11
12
|
:count_in_iteration
|
|
@@ -52,9 +53,17 @@ module EagerEye
|
|
|
52
53
|
|
|
53
54
|
def count_on_association?(node, block_var)
|
|
54
55
|
node.type == :send && COUNT_METHODS.include?(node.children[1]) &&
|
|
56
|
+
!array_returning_method?(node.children[0]) &&
|
|
55
57
|
association_call_on_block_var?(node.children[0], block_var)
|
|
56
58
|
end
|
|
57
59
|
|
|
60
|
+
def array_returning_method?(node)
|
|
61
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :send
|
|
62
|
+
|
|
63
|
+
method_name = node.children[1].to_s
|
|
64
|
+
ARRAY_METHOD_SUFFIXES.any? { |suffix| method_name.end_with?(suffix) }
|
|
65
|
+
end
|
|
66
|
+
|
|
58
67
|
def association_call_on_block_var?(node, block_var)
|
|
59
68
|
return false unless node.is_a?(Parser::AST::Node) && node.type == :send
|
|
60
69
|
|
|
@@ -7,7 +7,7 @@ module EagerEye
|
|
|
7
7
|
maximum].freeze
|
|
8
8
|
SAFE_QUERY_METHODS = %i[first last take count sum find size length ids].freeze
|
|
9
9
|
SAFE_TRANSFORM_METHODS = %i[keys values split [] params sort pluck ids to_s to_a to_i chars bytes].freeze
|
|
10
|
-
ARRAY_COLUMN_SUFFIXES = %w[_ids _tags _types _codes _names _values].freeze
|
|
10
|
+
ARRAY_COLUMN_SUFFIXES = %w[_ids _tags _types _codes _names _values _arr].freeze
|
|
11
11
|
ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map
|
|
12
12
|
find_each find_in_batches in_batches].freeze
|
|
13
13
|
|
|
@@ -62,6 +62,7 @@ module EagerEye
|
|
|
62
62
|
method_name = node.children[1]
|
|
63
63
|
return false unless QUERY_METHODS.include?(method_name)
|
|
64
64
|
return false if skip_array_method?(node, block_var, is_array_collection)
|
|
65
|
+
return false if receiver_is_query_chain?(node.children[0])
|
|
65
66
|
|
|
66
67
|
receiver_chain_starts_with?(node.children[0], block_var)
|
|
67
68
|
end
|
|
@@ -130,6 +131,10 @@ module EagerEye
|
|
|
130
131
|
SAFE_TRANSFORM_METHODS.include?(method_name) || array_column_method?(method_name)
|
|
131
132
|
end
|
|
132
133
|
|
|
134
|
+
def receiver_is_query_chain?(node)
|
|
135
|
+
node.is_a?(Parser::AST::Node) && node.type == :send && QUERY_METHODS.include?(node.children[1])
|
|
136
|
+
end
|
|
137
|
+
|
|
133
138
|
def array_column_method?(method_name)
|
|
134
139
|
method_str = method_name.to_s
|
|
135
140
|
ARRAY_COLUMN_SUFFIXES.any? { |suffix| method_str.end_with?(suffix) }
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Detectors
|
|
5
|
+
class DecoratorNPlusOne < Base
|
|
6
|
+
DECORATOR_PATTERNS = %w[Draper::Decorator SimpleDelegator Delegator].freeze
|
|
7
|
+
OBJECT_REFS = %i[object __getobj__ source model].freeze
|
|
8
|
+
ACTIVE_STORAGE_METHODS = %i[attached? attach attachment attachments blob blobs purge purge_later variant
|
|
9
|
+
preview].freeze
|
|
10
|
+
HAS_MANY_ASSOCIATIONS = %w[
|
|
11
|
+
authors users owners creators admins members customers clients
|
|
12
|
+
posts articles comments categories tags children companies organizations
|
|
13
|
+
projects tasks items orders products accounts profiles settings
|
|
14
|
+
images avatars photos attachments documents
|
|
15
|
+
].freeze
|
|
16
|
+
|
|
17
|
+
def self.detector_name
|
|
18
|
+
:decorator_n_plus_one
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def detect(ast, file_path)
|
|
22
|
+
return [] unless ast
|
|
23
|
+
|
|
24
|
+
issues = []
|
|
25
|
+
|
|
26
|
+
traverse_ast(ast) do |node|
|
|
27
|
+
next unless decorator_class?(node)
|
|
28
|
+
|
|
29
|
+
find_association_accesses(node, file_path, issues)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
issues
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def decorator_class?(node)
|
|
38
|
+
return false unless node.type == :class
|
|
39
|
+
|
|
40
|
+
class_name = extract_class_name(node)
|
|
41
|
+
return false unless class_name
|
|
42
|
+
|
|
43
|
+
decorator_name_pattern?(class_name) || inherits_from_decorator?(node)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def decorator_name_pattern?(class_name)
|
|
47
|
+
class_name.end_with?("Decorator", "Presenter", "ViewObject")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def extract_class_name(class_node)
|
|
51
|
+
name_node = class_node.children[0]
|
|
52
|
+
name_node.children[1].to_s if name_node&.type == :const
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def inherits_from_decorator?(class_node)
|
|
56
|
+
parent_node = class_node.children[1]
|
|
57
|
+
return false unless parent_node
|
|
58
|
+
|
|
59
|
+
parent_name = const_to_string(parent_node)
|
|
60
|
+
DECORATOR_PATTERNS.any? { |p| parent_name&.include?(p.split("::").last) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def const_to_string(node)
|
|
64
|
+
return nil unless node&.type == :const
|
|
65
|
+
|
|
66
|
+
parts = []
|
|
67
|
+
current = node
|
|
68
|
+
while current&.type == :const
|
|
69
|
+
parts.unshift(current.children[1].to_s)
|
|
70
|
+
current = current.children[0]
|
|
71
|
+
end
|
|
72
|
+
parts.join("::")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def find_association_accesses(class_node, file_path, issues)
|
|
76
|
+
body = class_node.children[2]
|
|
77
|
+
return unless body
|
|
78
|
+
|
|
79
|
+
traverse_ast(body) do |node|
|
|
80
|
+
next unless node.type == :def
|
|
81
|
+
|
|
82
|
+
find_associations_in_method(node.children[2], file_path, issues)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def find_associations_in_method(method_body, file_path, issues)
|
|
87
|
+
return unless method_body
|
|
88
|
+
|
|
89
|
+
storage_lines = collect_active_storage_lines(method_body)
|
|
90
|
+
traverse_ast(method_body) do |node|
|
|
91
|
+
next unless association_access?(node, storage_lines)
|
|
92
|
+
|
|
93
|
+
receiver = node.children[0]
|
|
94
|
+
method_name = node.children[1]
|
|
95
|
+
issues << create_decorator_issue(file_path, node.loc.line, receiver, method_name)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def association_access?(node, storage_lines)
|
|
100
|
+
return false unless node.type == :send
|
|
101
|
+
return false if storage_lines.include?(node.loc.line)
|
|
102
|
+
|
|
103
|
+
object_reference?(node.children[0]) && likely_association?(node.children[1])
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def collect_active_storage_lines(body)
|
|
107
|
+
lines = Set.new
|
|
108
|
+
traverse_ast(body) do |node|
|
|
109
|
+
next unless node.type == :send && ACTIVE_STORAGE_METHODS.include?(node.children[1])
|
|
110
|
+
|
|
111
|
+
lines << node.loc.line
|
|
112
|
+
end
|
|
113
|
+
lines
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def object_reference?(node)
|
|
117
|
+
return false unless node&.type == :send
|
|
118
|
+
|
|
119
|
+
node.children[0].nil? && OBJECT_REFS.include?(node.children[1])
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def likely_association?(method_name)
|
|
123
|
+
HAS_MANY_ASSOCIATIONS.include?(method_name.to_s)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def create_decorator_issue(file_path, line, receiver, method_name)
|
|
127
|
+
ref = receiver.children[1]
|
|
128
|
+
create_issue(
|
|
129
|
+
file_path: file_path,
|
|
130
|
+
line_number: line,
|
|
131
|
+
message: "N+1 in decorator: `#{ref}.#{method_name}` loads association on each decorated object",
|
|
132
|
+
suggestion: "Eager load :#{method_name} in the controller before decorating the collection"
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -15,6 +15,7 @@ require_relative "eager_eye/detectors/count_in_iteration"
|
|
|
15
15
|
require_relative "eager_eye/detectors/callback_query"
|
|
16
16
|
require_relative "eager_eye/detectors/pluck_to_array"
|
|
17
17
|
require_relative "eager_eye/detectors/delegation_n_plus_one"
|
|
18
|
+
require_relative "eager_eye/detectors/decorator_n_plus_one"
|
|
18
19
|
require_relative "eager_eye/comment_parser"
|
|
19
20
|
require_relative "eager_eye/analyzer"
|
|
20
21
|
require_relative "eager_eye/fixers/base"
|
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.2.
|
|
4
|
+
version: 1.2.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|
|
@@ -70,6 +70,7 @@ files:
|
|
|
70
70
|
- lib/eager_eye/detectors/concerns/non_ar_source_detector.rb
|
|
71
71
|
- lib/eager_eye/detectors/count_in_iteration.rb
|
|
72
72
|
- lib/eager_eye/detectors/custom_method_query.rb
|
|
73
|
+
- lib/eager_eye/detectors/decorator_n_plus_one.rb
|
|
73
74
|
- lib/eager_eye/detectors/delegation_n_plus_one.rb
|
|
74
75
|
- lib/eager_eye/detectors/loop_association.rb
|
|
75
76
|
- lib/eager_eye/detectors/missing_counter_cache.rb
|