eager_eye 1.2.9 → 1.2.11
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 +21 -0
- data/README.md +2 -1
- data/lib/eager_eye/analyzer.rb +11 -5
- data/lib/eager_eye/detectors/base.rb +12 -0
- data/lib/eager_eye/detectors/custom_method_query.rb +30 -2
- data/lib/eager_eye/detectors/decorator_n_plus_one.rb +22 -8
- data/lib/eager_eye/detectors/loop_association.rb +33 -11
- data/lib/eager_eye/detectors/serializer_nesting.rb +23 -9
- data/lib/eager_eye/method_query_parser.rb +61 -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: 6f108cfabf63f2311f76b96145ea91535ef022703c3d10f73f04cbf6132110ba
|
|
4
|
+
data.tar.gz: ee909d0893f862770c3e481adf5e9041da306c86e821458dd5c069404738ff1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 387702e5516001bd86637e039c999dbb52cf30e1d20ec293afd23c70a4b591be34f0aa245bc96eb6d5bec4e1e34a3a9bc9e09295525db53550b2df6833a3a06c
|
|
7
|
+
data.tar.gz: 605eec5df28fdaf038d77c95c564e8ee3eb4768be9e900be58c96b99a0b715ad4d28b286c65a2c7844f61906146db39168761744386f73ce5f61e1e77fe7c4cb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.11] - 2026-03-17
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`each_with_object` / `reduce` / `inject` Support** - `LoopAssociation` and `CustomMethodQuery` detectors now recognize these iteration methods
|
|
15
|
+
- `each_with_object` and `each_with_index` blocks are detected (item is first param)
|
|
16
|
+
- `reduce` and `inject` blocks are detected with correct variable extraction (item is second param: `|memo, item|`)
|
|
17
|
+
- `extract_iteration_variable` helper added to `Base` for reuse across detectors
|
|
18
|
+
|
|
19
|
+
## [1.2.10] - 2026-03-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Cross-File Analysis** - Model methods containing queries are now detected when called from other files
|
|
24
|
+
- New `MethodQueryParser` scans model files for methods that execute queries (`.where`, `.find_by`, `.count`, `.pluck`, etc.)
|
|
25
|
+
- `LoopAssociation` detects cross-file query methods called inside loops (e.g. `user.active_orders` in a controller)
|
|
26
|
+
- `CustomMethodQuery` detects model query methods called inside iterations
|
|
27
|
+
- `SerializerNesting` detects model query methods called in serializer attribute blocks
|
|
28
|
+
- `DecoratorNPlusOne` detects model query methods called in decorator methods
|
|
29
|
+
- Two-pass architecture: first pass collects method signatures, second pass detects cross-file N+1 patterns
|
|
30
|
+
|
|
10
31
|
## [1.2.9] - 2026-03-13
|
|
11
32
|
|
|
12
33
|
### 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.2.
|
|
13
|
+
<a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.2.11-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>
|
|
@@ -739,6 +739,7 @@ EagerEye uses static analysis, which means:
|
|
|
739
739
|
- **No runtime context** - Cannot know if associations are already eager loaded elsewhere
|
|
740
740
|
- **Heuristic-based** - Uses naming conventions to identify associations (may have false positives)
|
|
741
741
|
- **Ruby code only** - Does not analyze SQL queries or ActiveRecord internals
|
|
742
|
+
- **Cross-file scope** - Cross-file analysis is limited to model-defined methods; controller-to-view or service-to-service patterns are not yet tracked
|
|
742
743
|
|
|
743
744
|
For best results, use EagerEye alongside runtime tools like Bullet for comprehensive N+1 detection.
|
|
744
745
|
|
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -19,23 +19,25 @@ module EagerEye
|
|
|
19
19
|
}.freeze
|
|
20
20
|
|
|
21
21
|
DETECTOR_EXTRA_ARGS = {
|
|
22
|
-
Detectors::LoopAssociation => %i[association_preloads association_names],
|
|
23
|
-
Detectors::SerializerNesting => %i[association_names],
|
|
22
|
+
Detectors::LoopAssociation => %i[association_preloads association_names method_queries],
|
|
23
|
+
Detectors::SerializerNesting => %i[association_names method_queries],
|
|
24
24
|
Detectors::MissingCounterCache => %i[association_names],
|
|
25
|
-
Detectors::DecoratorNPlusOne => %i[association_names],
|
|
25
|
+
Detectors::DecoratorNPlusOne => %i[association_names method_queries],
|
|
26
|
+
Detectors::CustomMethodQuery => %i[method_queries],
|
|
26
27
|
Detectors::DelegationNPlusOne => %i[delegation_maps],
|
|
27
28
|
Detectors::ScopeChainNPlusOne => %i[scope_maps],
|
|
28
29
|
Detectors::ValidationNPlusOne => %i[uniqueness_models]
|
|
29
30
|
}.freeze
|
|
30
31
|
|
|
31
|
-
attr_reader :paths, :issues, :association_preloads, :association_names, :
|
|
32
|
-
:uniqueness_models
|
|
32
|
+
attr_reader :paths, :issues, :association_preloads, :association_names, :method_queries, :delegation_maps,
|
|
33
|
+
:scope_maps, :uniqueness_models
|
|
33
34
|
|
|
34
35
|
def initialize(paths: nil)
|
|
35
36
|
@paths = Array(paths || EagerEye.configuration.app_path)
|
|
36
37
|
@issues = []
|
|
37
38
|
@association_preloads = {}
|
|
38
39
|
@association_names = Set.new
|
|
40
|
+
@method_queries = {}
|
|
39
41
|
@delegation_maps = {}
|
|
40
42
|
@scope_maps = {}
|
|
41
43
|
@uniqueness_models = Set.new
|
|
@@ -73,6 +75,10 @@ module EagerEye
|
|
|
73
75
|
validation_parser = ValidationParser.new
|
|
74
76
|
validation_parser.parse_model(ast, model_name)
|
|
75
77
|
@uniqueness_models.merge(validation_parser.uniqueness_models)
|
|
78
|
+
|
|
79
|
+
method_query_parser = MethodQueryParser.new
|
|
80
|
+
method_query_parser.parse_model(ast, model_name)
|
|
81
|
+
@method_queries.merge!(method_query_parser.method_queries)
|
|
76
82
|
rescue Errno::ENOENT, Errno::EACCES
|
|
77
83
|
next
|
|
78
84
|
end
|
|
@@ -73,12 +73,24 @@ module EagerEye
|
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
75
|
|
|
76
|
+
ACCUMULATOR_FIRST_METHODS = %i[reduce inject].freeze
|
|
77
|
+
|
|
76
78
|
def extract_block_variable(block_node)
|
|
77
79
|
args = block_node&.children&.[](1)
|
|
78
80
|
first_arg = args&.children&.first
|
|
79
81
|
first_arg&.type == :arg ? first_arg.children[0] : nil
|
|
80
82
|
end
|
|
81
83
|
|
|
84
|
+
def extract_iteration_variable(block_node)
|
|
85
|
+
idx = accumulator_first?(block_node) ? 1 : 0
|
|
86
|
+
arg = block_node.children[1]&.children&.[](idx)
|
|
87
|
+
arg&.type == :arg ? arg.children[0] : nil
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def accumulator_first?(block_node)
|
|
91
|
+
ACCUMULATOR_FIRST_METHODS.include?(block_node.children[0]&.children&.[](1))
|
|
92
|
+
end
|
|
93
|
+
|
|
82
94
|
def receiver_chain_starts_with?(node, target_var)
|
|
83
95
|
return false unless node.is_a?(Parser::AST::Node)
|
|
84
96
|
|
|
@@ -9,20 +9,23 @@ module EagerEye
|
|
|
9
9
|
SAFE_TRANSFORM_METHODS = %i[keys values split [] params sort pluck ids to_s to_a to_i chars bytes].freeze
|
|
10
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
|
+
each_with_index each_with_object reduce inject
|
|
12
13
|
find_each find_in_batches in_batches].freeze
|
|
13
14
|
|
|
14
15
|
def self.detector_name
|
|
15
16
|
:custom_method_query
|
|
16
17
|
end
|
|
17
18
|
|
|
18
|
-
def detect(ast, file_path)
|
|
19
|
+
def detect(ast, file_path, method_queries = {})
|
|
19
20
|
return [] unless ast
|
|
20
21
|
|
|
21
22
|
@issues = []
|
|
22
23
|
@file_path = file_path
|
|
24
|
+
@method_queries = method_queries
|
|
23
25
|
|
|
24
26
|
find_iteration_blocks(ast) do |block_body, block_var, collection, definitions|
|
|
25
27
|
check_block_for_query_methods(block_body, block_var, collection_is_array?(collection, definitions))
|
|
28
|
+
check_block_for_model_query_methods(block_body, block_var)
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
@issues
|
|
@@ -36,7 +39,7 @@ module EagerEye
|
|
|
36
39
|
definitions[node.children[0]] = node.children[1] if node.type == :lvasgn
|
|
37
40
|
|
|
38
41
|
if iteration_block?(node)
|
|
39
|
-
block_var =
|
|
42
|
+
block_var = extract_iteration_variable(node)
|
|
40
43
|
block_body = node.children[2]
|
|
41
44
|
yield(block_body, block_var, node.children[0], definitions) if block_var && block_body
|
|
42
45
|
end
|
|
@@ -112,6 +115,31 @@ module EagerEye
|
|
|
112
115
|
node.is_a?(Parser::AST::Node) && node.type == :send && QUERY_METHODS.include?(node.children[1])
|
|
113
116
|
end
|
|
114
117
|
|
|
118
|
+
def check_block_for_model_query_methods(node, block_var)
|
|
119
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
120
|
+
|
|
121
|
+
if model_query_call?(node, block_var)
|
|
122
|
+
method = node.children[1]
|
|
123
|
+
@issues << create_issue(
|
|
124
|
+
file_path: @file_path,
|
|
125
|
+
line_number: node.loc.line,
|
|
126
|
+
message: "Model method `.#{method}` contains a query and is called inside iteration",
|
|
127
|
+
suggestion: "This method executes a query on each iteration. Preload data or move the query outside."
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
node.children.each { |child| check_block_for_model_query_methods(child, block_var) }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def model_query_call?(node, block_var)
|
|
134
|
+
return false unless node.type == :send
|
|
135
|
+
|
|
136
|
+
receiver = node.children[0]
|
|
137
|
+
method = node.children[1]
|
|
138
|
+
return false unless receiver&.type == :lvar && receiver.children[0] == block_var
|
|
139
|
+
|
|
140
|
+
@method_queries&.any? { |_model, methods| methods.include?(method) }
|
|
141
|
+
end
|
|
142
|
+
|
|
115
143
|
def add_issue(node)
|
|
116
144
|
chain = reconstruct_chain(node.children[0])
|
|
117
145
|
@issues << create_issue(
|
|
@@ -14,10 +14,11 @@ module EagerEye
|
|
|
14
14
|
:decorator_n_plus_one
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
-
def detect(ast, file_path, association_names = Set.new)
|
|
17
|
+
def detect(ast, file_path, association_names = Set.new, method_queries = {})
|
|
18
18
|
return [] unless ast
|
|
19
19
|
|
|
20
20
|
@dynamic_associations = association_names
|
|
21
|
+
@method_queries = method_queries
|
|
21
22
|
issues = []
|
|
22
23
|
traverse_ast(ast) do |node|
|
|
23
24
|
next unless node.type == :class && decorator_class?(node)
|
|
@@ -65,21 +66,34 @@ module EagerEye
|
|
|
65
66
|
|
|
66
67
|
receiver = node.children[0]
|
|
67
68
|
method_name = node.children[1]
|
|
68
|
-
next unless object_reference?(receiver)
|
|
69
|
+
next unless object_reference?(receiver)
|
|
69
70
|
|
|
70
71
|
ref = receiver.children[1]
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
if likely_association?(method_name)
|
|
73
|
+
issues << create_issue(
|
|
74
|
+
file_path: file_path,
|
|
75
|
+
line_number: node.loc.line,
|
|
76
|
+
message: "N+1 in decorator: `#{ref}.#{method_name}` loads association on each decorated object",
|
|
77
|
+
suggestion: "Eager load :#{method_name} in the controller before decorating the collection"
|
|
78
|
+
)
|
|
79
|
+
elsif model_query_method?(method_name)
|
|
80
|
+
issues << create_issue(
|
|
81
|
+
file_path: file_path,
|
|
82
|
+
line_number: node.loc.line,
|
|
83
|
+
message: "N+1 in decorator: `#{ref}.#{method_name}` calls a query method defined in the model",
|
|
84
|
+
suggestion: "Preload the data in the controller before decorating the collection"
|
|
85
|
+
)
|
|
86
|
+
end
|
|
77
87
|
end
|
|
78
88
|
end
|
|
79
89
|
|
|
80
90
|
def object_reference?(node)
|
|
81
91
|
node&.type == :send && node.children[0].nil? && OBJECT_REFS.include?(node.children[1])
|
|
82
92
|
end
|
|
93
|
+
|
|
94
|
+
def model_query_method?(method_name)
|
|
95
|
+
@method_queries&.any? { |_model, methods| methods.include?(method_name) }
|
|
96
|
+
end
|
|
83
97
|
end
|
|
84
98
|
end
|
|
85
99
|
end
|
|
@@ -4,6 +4,7 @@ module EagerEye
|
|
|
4
4
|
module Detectors
|
|
5
5
|
class LoopAssociation < Base
|
|
6
6
|
ITERATION_METHODS = %i[each map collect select find find_all reject filter filter_map flat_map
|
|
7
|
+
each_with_index each_with_object reduce inject
|
|
7
8
|
find_each find_in_batches in_batches].freeze
|
|
8
9
|
PRELOAD_METHODS = %i[includes preload eager_load].freeze
|
|
9
10
|
SINGLE_RECORD_METHODS = %i[find find_by find_by! first first! last last! take take! second third fourth fifth
|
|
@@ -30,18 +31,19 @@ module EagerEye
|
|
|
30
31
|
:loop_association
|
|
31
32
|
end
|
|
32
33
|
|
|
33
|
-
def detect(ast, file_path, association_preloads = {}, association_names = Set.new)
|
|
34
|
+
def detect(ast, file_path, association_preloads = {}, association_names = Set.new, method_queries = {})
|
|
34
35
|
return [] unless ast
|
|
35
36
|
|
|
36
37
|
issues = []
|
|
37
38
|
@association_preloads = association_preloads
|
|
38
39
|
@dynamic_associations = association_names
|
|
40
|
+
@method_queries = method_queries
|
|
39
41
|
build_variable_maps(ast)
|
|
40
42
|
|
|
41
43
|
traverse_ast(ast) do |node|
|
|
42
44
|
next unless iteration_block?(node)
|
|
43
45
|
|
|
44
|
-
block_var =
|
|
46
|
+
block_var = extract_iteration_variable(node)
|
|
45
47
|
next unless block_var
|
|
46
48
|
|
|
47
49
|
block_body = node.children[2]
|
|
@@ -157,15 +159,23 @@ module EagerEye
|
|
|
157
159
|
def find_association_calls(node, block_var, file_path, issues, included_associations = Set.new)
|
|
158
160
|
reported = Set.new
|
|
159
161
|
traverse_ast(node) do |child|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
)
|
|
162
|
+
if reportable_association_call?(child, block_var, reported, included_associations)
|
|
163
|
+
method = child.children[1]
|
|
164
|
+
issues << create_issue(
|
|
165
|
+
file_path: file_path,
|
|
166
|
+
line_number: child.loc.line,
|
|
167
|
+
message: "Potential N+1 query: `#{block_var}.#{method}` called inside loop",
|
|
168
|
+
suggestion: "Use `includes(:#{method})` before iterating"
|
|
169
|
+
)
|
|
170
|
+
elsif reportable_method_query_call?(child, block_var, reported)
|
|
171
|
+
method = child.children[1]
|
|
172
|
+
issues << create_issue(
|
|
173
|
+
file_path: file_path,
|
|
174
|
+
line_number: child.loc.line,
|
|
175
|
+
message: "Potential N+1 query: `#{block_var}.#{method}` calls a query method defined in the model",
|
|
176
|
+
suggestion: "Preload the data or restructure to avoid per-record queries"
|
|
177
|
+
)
|
|
178
|
+
end
|
|
169
179
|
end
|
|
170
180
|
end
|
|
171
181
|
|
|
@@ -189,6 +199,18 @@ module EagerEye
|
|
|
189
199
|
def known_association?(method)
|
|
190
200
|
ASSOCIATION_NAMES.include?(method.to_s) || @dynamic_associations.include?(method)
|
|
191
201
|
end
|
|
202
|
+
|
|
203
|
+
def reportable_method_query_call?(node, block_var, reported)
|
|
204
|
+
return false unless block_var_send?(node, block_var)
|
|
205
|
+
return false if EXCLUDED_METHODS.include?(node.children[1])
|
|
206
|
+
return false unless @method_queries&.any? { |_, ms| ms.include?(node.children[1]) }
|
|
207
|
+
|
|
208
|
+
reported.add?("#{node.loc.line}:#{node.children[1]}")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def block_var_send?(node, block_var)
|
|
212
|
+
node.type == :send && node.children[0]&.type == :lvar && node.children[0].children[0] == block_var
|
|
213
|
+
end
|
|
192
214
|
end
|
|
193
215
|
end
|
|
194
216
|
end
|
|
@@ -15,10 +15,11 @@ module EagerEye
|
|
|
15
15
|
:serializer_nesting
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def detect(ast, file_path, association_names = Set.new)
|
|
18
|
+
def detect(ast, file_path, association_names = Set.new, method_queries = {})
|
|
19
19
|
return [] unless ast
|
|
20
20
|
|
|
21
21
|
@dynamic_associations = association_names
|
|
22
|
+
@method_queries = method_queries
|
|
22
23
|
issues = []
|
|
23
24
|
traverse_ast(ast) do |node|
|
|
24
25
|
next unless node.type == :class && serializer_class?(node)
|
|
@@ -86,14 +87,23 @@ module EagerEye
|
|
|
86
87
|
|
|
87
88
|
receiver = node.children[0]
|
|
88
89
|
method_name = node.children[1]
|
|
89
|
-
next unless object_reference?(receiver)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
next unless object_reference?(receiver)
|
|
91
|
+
|
|
92
|
+
if likely_association?(method_name)
|
|
93
|
+
issues << create_issue(
|
|
94
|
+
file_path: file_path,
|
|
95
|
+
line_number: node.loc.line,
|
|
96
|
+
message: "Nested association `#{receiver_name(receiver)}.#{method_name}` in serializer attribute",
|
|
97
|
+
suggestion: "Eager load :#{method_name} in controller or use association serializer"
|
|
98
|
+
)
|
|
99
|
+
elsif model_query_method?(method_name)
|
|
100
|
+
issues << create_issue(
|
|
101
|
+
file_path: file_path,
|
|
102
|
+
line_number: node.loc.line,
|
|
103
|
+
message: "Model method `#{receiver_name(receiver)}.#{method_name}` contains a query in serializer",
|
|
104
|
+
suggestion: "This method executes a query per serialized object. Preload or cache the data."
|
|
105
|
+
)
|
|
106
|
+
end
|
|
97
107
|
end
|
|
98
108
|
end
|
|
99
109
|
|
|
@@ -114,6 +124,10 @@ module EagerEye
|
|
|
114
124
|
else "object"
|
|
115
125
|
end
|
|
116
126
|
end
|
|
127
|
+
|
|
128
|
+
def model_query_method?(method_name)
|
|
129
|
+
@method_queries&.any? { |_model, methods| methods.include?(method_name) }
|
|
130
|
+
end
|
|
117
131
|
end
|
|
118
132
|
end
|
|
119
133
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
class MethodQueryParser
|
|
5
|
+
QUERY_METHODS = %i[where find find_by find_by! exists? any? none? many? one?
|
|
6
|
+
count sum average minimum maximum
|
|
7
|
+
pluck ids select
|
|
8
|
+
update_all delete_all destroy_all
|
|
9
|
+
create create! save save!
|
|
10
|
+
first last take].freeze
|
|
11
|
+
|
|
12
|
+
ASSOCIATION_METHODS = %i[has_many has_one belongs_to has_and_belongs_to_many].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader :method_queries
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@method_queries = {}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse_model(ast, model_name)
|
|
21
|
+
return unless ast
|
|
22
|
+
|
|
23
|
+
traverse(ast, model_name)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def traverse(node, model_name)
|
|
29
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
30
|
+
|
|
31
|
+
check_method(node, model_name)
|
|
32
|
+
node.children.each { |child| traverse(child, model_name) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check_method(node, model_name)
|
|
36
|
+
return unless instance_method_def?(node)
|
|
37
|
+
|
|
38
|
+
method_name = node.children[0]
|
|
39
|
+
body = node.children[2]
|
|
40
|
+
return unless body && contains_query?(body)
|
|
41
|
+
|
|
42
|
+
@method_queries[model_name] ||= Set.new
|
|
43
|
+
@method_queries[model_name] << method_name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def instance_method_def?(node)
|
|
47
|
+
node.type == :def
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def contains_query?(node)
|
|
51
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
52
|
+
return true if query_call?(node)
|
|
53
|
+
|
|
54
|
+
node.children.any? { |child| contains_query?(child) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def query_call?(node)
|
|
58
|
+
node.type == :send && QUERY_METHODS.include?(node.children[1])
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -8,6 +8,7 @@ require_relative "eager_eye/association_parser"
|
|
|
8
8
|
require_relative "eager_eye/delegation_parser"
|
|
9
9
|
require_relative "eager_eye/scope_parser"
|
|
10
10
|
require_relative "eager_eye/validation_parser"
|
|
11
|
+
require_relative "eager_eye/method_query_parser"
|
|
11
12
|
require_relative "eager_eye/detectors/base"
|
|
12
13
|
require_relative "eager_eye/detectors/loop_association"
|
|
13
14
|
require_relative "eager_eye/detectors/serializer_nesting"
|
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.11
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|
|
@@ -84,6 +84,7 @@ files:
|
|
|
84
84
|
- lib/eager_eye/fixers/pluck_to_select.rb
|
|
85
85
|
- lib/eager_eye/generators/install_generator.rb
|
|
86
86
|
- lib/eager_eye/issue.rb
|
|
87
|
+
- lib/eager_eye/method_query_parser.rb
|
|
87
88
|
- lib/eager_eye/railtie.rb
|
|
88
89
|
- lib/eager_eye/reporters/base.rb
|
|
89
90
|
- lib/eager_eye/reporters/console.rb
|