eager_eye 1.1.0 → 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 +1 -1
- data/CHANGELOG.md +8 -0
- data/README.md +1 -1
- data/SECURITY.md +1 -0
- data/lib/eager_eye/analyzer.rb +5 -14
- data/lib/eager_eye/association_parser.rb +6 -28
- 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 +3 -10
- 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 +7 -30
- data/lib/eager_eye/detectors/missing_counter_cache.rb +16 -47
- data/lib/eager_eye/detectors/pluck_to_array.rb +15 -48
- 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 +0 -1
- data/sig/eager_eye.rbs +0 -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: 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
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.1] - 2026-01-03
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
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
|
+
|
|
10
18
|
## [1.1.0] - 2025-12-28
|
|
11
19
|
|
|
12
20
|
### 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.1.
|
|
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>
|
data/SECURITY.md
CHANGED
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -33,17 +33,15 @@ module EagerEye
|
|
|
33
33
|
|
|
34
34
|
def collect_association_preloads
|
|
35
35
|
model_files.each do |file_path|
|
|
36
|
-
|
|
37
|
-
ast = parse_source(source)
|
|
36
|
+
ast = parse_source(File.read(file_path))
|
|
38
37
|
next unless ast
|
|
39
38
|
|
|
40
|
-
model_name = extract_model_name(file_path)
|
|
41
39
|
parser = AssociationParser.new
|
|
42
|
-
parser.parse_model(ast,
|
|
40
|
+
parser.parse_model(ast, extract_model_name(file_path))
|
|
43
41
|
@association_preloads.merge!(parser.preloaded_associations)
|
|
44
42
|
end
|
|
45
43
|
rescue StandardError
|
|
46
|
-
|
|
44
|
+
nil
|
|
47
45
|
end
|
|
48
46
|
|
|
49
47
|
def model_files
|
|
@@ -84,22 +82,15 @@ module EagerEye
|
|
|
84
82
|
return unless ast
|
|
85
83
|
|
|
86
84
|
comment_parser = CommentParser.new(source)
|
|
85
|
+
min_severity = EagerEye.configuration.min_severity
|
|
87
86
|
|
|
88
87
|
enabled_detectors.each do |detector|
|
|
89
88
|
args = [ast, file_path]
|
|
90
89
|
args << @association_preloads if detector.is_a?(Detectors::LoopAssociation)
|
|
91
90
|
|
|
92
91
|
file_issues = detector.detect(*args)
|
|
93
|
-
|
|
94
|
-
# Filter suppressed issues
|
|
95
|
-
file_issues.reject! do |issue|
|
|
96
|
-
comment_parser.disabled_at?(issue.line_number, issue.detector)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
# Filter by minimum severity
|
|
100
|
-
min_severity = EagerEye.configuration.min_severity
|
|
92
|
+
file_issues.reject! { |issue| comment_parser.disabled_at?(issue.line_number, issue.detector) }
|
|
101
93
|
file_issues.select! { |issue| issue.meets_minimum_severity?(min_severity) }
|
|
102
|
-
|
|
103
94
|
@issues.concat(file_issues)
|
|
104
95
|
end
|
|
105
96
|
rescue Errno::ENOENT, Errno::EACCES => e
|
|
@@ -42,33 +42,17 @@ module EagerEye
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def extract_association_name(node)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
first_arg = args[0]
|
|
49
|
-
return nil unless first_arg&.type == :sym
|
|
50
|
-
|
|
51
|
-
first_arg.children[0]
|
|
45
|
+
first_arg = node.children[2]
|
|
46
|
+
first_arg&.type == :sym ? first_arg.children[0] : nil
|
|
52
47
|
end
|
|
53
48
|
|
|
54
49
|
def extract_preloaded_associations(node)
|
|
55
50
|
preloaded = Set.new
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
# Check for block with includes/preload/eager_load
|
|
60
|
-
block_node = args.find { |arg| arg&.type == :block }
|
|
61
|
-
return preloaded unless block_node
|
|
62
|
-
|
|
63
|
-
extract_from_block(block_node, preloaded)
|
|
51
|
+
block_node = node.children[2..].find { |arg| arg&.type == :block }
|
|
52
|
+
traverse_for_preloads(block_node&.children&.[](2), preloaded) if block_node
|
|
64
53
|
preloaded
|
|
65
54
|
end
|
|
66
55
|
|
|
67
|
-
def extract_from_block(block_node, preloaded)
|
|
68
|
-
block_body = block_node.children[2]
|
|
69
|
-
traverse_for_preloads(block_body, preloaded)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
56
|
def traverse_for_preloads(node, preloaded)
|
|
73
57
|
return unless node.is_a?(Parser::AST::Node)
|
|
74
58
|
|
|
@@ -78,17 +62,11 @@ module EagerEye
|
|
|
78
62
|
end
|
|
79
63
|
|
|
80
64
|
def preload_call?(node)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
method = node.children[1]
|
|
84
|
-
%i[includes preload eager_load].include?(method)
|
|
65
|
+
node.type == :send && %i[includes preload eager_load].include?(node.children[1])
|
|
85
66
|
end
|
|
86
67
|
|
|
87
68
|
def extract_includes_from_method(node, included)
|
|
88
|
-
|
|
89
|
-
return if args.empty?
|
|
90
|
-
|
|
91
|
-
args.each { |arg| add_included_sym(arg, included) }
|
|
69
|
+
node.children[2..].each { |arg| add_included_sym(arg, included) }
|
|
92
70
|
end
|
|
93
71
|
|
|
94
72
|
def add_included_sym(arg, included)
|
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)
|
|
@@ -61,14 +58,10 @@ module EagerEye
|
|
|
61
58
|
|
|
62
59
|
def extract_symbols_from_args(args)
|
|
63
60
|
symbols = Set.new
|
|
64
|
-
return symbols if args.empty?
|
|
65
|
-
|
|
66
61
|
args.each do |arg|
|
|
67
62
|
case arg&.type
|
|
68
|
-
when :sym
|
|
69
|
-
|
|
70
|
-
when :hash
|
|
71
|
-
extract_symbols_from_hash(arg, symbols)
|
|
63
|
+
when :sym then symbols.add(arg.children[0])
|
|
64
|
+
when :hash then extract_symbols_from_hash(arg, symbols)
|
|
72
65
|
end
|
|
73
66
|
end
|
|
74
67
|
symbols
|
|
@@ -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)
|
|
@@ -3,27 +3,9 @@
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
module Detectors
|
|
5
5
|
class CustomMethodQuery < Base
|
|
6
|
-
QUERY_METHODS = %i[
|
|
7
|
-
|
|
8
|
-
find_by
|
|
9
|
-
find_by!
|
|
10
|
-
exists?
|
|
11
|
-
find
|
|
12
|
-
first
|
|
13
|
-
last
|
|
14
|
-
take
|
|
15
|
-
pluck
|
|
16
|
-
ids
|
|
17
|
-
count
|
|
18
|
-
sum
|
|
19
|
-
average
|
|
20
|
-
minimum
|
|
21
|
-
maximum
|
|
22
|
-
].freeze
|
|
23
|
-
|
|
24
|
-
# Array-only methods that should not be flagged when collection is clearly an array
|
|
6
|
+
QUERY_METHODS = %i[where find_by find_by! exists? find first last take pluck ids count sum average minimum
|
|
7
|
+
maximum].freeze
|
|
25
8
|
ARRAY_METHODS = %i[first last take].freeze
|
|
26
|
-
|
|
27
9
|
ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map].freeze
|
|
28
10
|
|
|
29
11
|
def self.detector_name
|
|
@@ -51,34 +33,22 @@ module EagerEye
|
|
|
51
33
|
|
|
52
34
|
if iteration_block?(node)
|
|
53
35
|
block_var = extract_block_variable(node)
|
|
54
|
-
block_body =
|
|
55
|
-
|
|
56
|
-
yield(block_body, block_var, collection) if block_var && block_body
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
node.children.each do |child|
|
|
60
|
-
find_iteration_blocks(child, &block)
|
|
36
|
+
block_body = node.children[2]
|
|
37
|
+
yield(block_body, block_var, node.children[0]) if block_var && block_body
|
|
61
38
|
end
|
|
39
|
+
node.children.each { |child| find_iteration_blocks(child, &block) }
|
|
62
40
|
end
|
|
63
41
|
|
|
64
42
|
def iteration_block?(node)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
send_node = node.children[0]
|
|
68
|
-
return false unless send_node&.type == :send
|
|
69
|
-
|
|
70
|
-
method_name = send_node.children[1]
|
|
71
|
-
ITERATION_METHODS.include?(method_name)
|
|
43
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
44
|
+
ITERATION_METHODS.include?(node.children[0].children[1])
|
|
72
45
|
end
|
|
73
46
|
|
|
74
47
|
def check_block_for_query_methods(node, block_var, is_array_collection = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
75
48
|
return unless node.is_a?(Parser::AST::Node)
|
|
76
49
|
|
|
77
50
|
add_issue(node) if query_chain_on_association?(node, block_var, is_array_collection)
|
|
78
|
-
|
|
79
|
-
node.children.each do |child|
|
|
80
|
-
check_block_for_query_methods(child, block_var, is_array_collection)
|
|
81
|
-
end
|
|
51
|
+
node.children.each { |child| check_block_for_query_methods(child, block_var, is_array_collection) }
|
|
82
52
|
end
|
|
83
53
|
|
|
84
54
|
def query_chain_on_association?(node, block_var, is_array_collection = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
@@ -86,35 +56,23 @@ module EagerEye
|
|
|
86
56
|
|
|
87
57
|
method_name = node.children[1]
|
|
88
58
|
return false unless QUERY_METHODS.include?(method_name)
|
|
59
|
+
return false if is_array_collection && ARRAY_METHODS.include?(method_name) &&
|
|
60
|
+
receiver_is_only_block_var?(node.children[0], block_var)
|
|
89
61
|
|
|
90
|
-
|
|
91
|
-
# AND the receiver is only the block variable (not chained)
|
|
92
|
-
if is_array_collection && ARRAY_METHODS.include?(method_name) &&
|
|
93
|
-
receiver_is_only_block_var?(node.children[0], block_var)
|
|
94
|
-
return false
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
receiver = node.children[0]
|
|
98
|
-
receiver_chain_starts_with?(receiver, block_var)
|
|
62
|
+
receiver_chain_starts_with?(node.children[0], block_var)
|
|
99
63
|
end
|
|
100
64
|
|
|
101
65
|
def receiver_is_only_block_var?(node, block_var)
|
|
102
|
-
|
|
103
|
-
node.is_a?(Parser::AST::Node) &&
|
|
104
|
-
node.type == :lvar &&
|
|
105
|
-
node.children[0] == block_var
|
|
66
|
+
node.is_a?(Parser::AST::Node) && node.type == :lvar && node.children[0] == block_var
|
|
106
67
|
end
|
|
107
68
|
|
|
108
69
|
def receiver_chain_starts_with?(node, block_var)
|
|
109
70
|
return false unless node.is_a?(Parser::AST::Node)
|
|
110
71
|
|
|
111
72
|
case node.type
|
|
112
|
-
when :lvar
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
receiver_chain_starts_with?(node.children[0], block_var)
|
|
116
|
-
else
|
|
117
|
-
false
|
|
73
|
+
when :lvar then node.children[0] == block_var
|
|
74
|
+
when :send then receiver_chain_starts_with?(node.children[0], block_var)
|
|
75
|
+
else false
|
|
118
76
|
end
|
|
119
77
|
end
|
|
120
78
|
|
|
@@ -123,37 +81,16 @@ module EagerEye
|
|
|
123
81
|
return nil unless args_node&.type == :args
|
|
124
82
|
|
|
125
83
|
first_arg = args_node.children[0]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
first_arg.children[0]
|
|
84
|
+
first_arg&.type == :arg ? first_arg.children[0] : nil
|
|
129
85
|
end
|
|
130
86
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def extract_collection(block_node)
|
|
136
|
-
# Extract the collection being iterated on
|
|
137
|
-
# For: collection.each { |item| ... }
|
|
138
|
-
# Returns: the send node representing the collection method call
|
|
139
|
-
block_node.children[0]
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def collection_is_array?(collection_node)
|
|
143
|
-
return false unless collection_node.is_a?(Parser::AST::Node)
|
|
87
|
+
def collection_is_array?(node)
|
|
88
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
144
89
|
|
|
145
|
-
case
|
|
146
|
-
when :array
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
when :send
|
|
150
|
-
# Only consider these methods as definitely returning arrays when iterating
|
|
151
|
-
method_name = collection_node.children[1]
|
|
152
|
-
# map, select, collect, etc. on anything return arrays for iteration
|
|
153
|
-
%i[map select collect flat_map to_a uniq compact].include?(method_name)
|
|
154
|
-
else
|
|
155
|
-
# Block variable itself won't tell us if it's an array
|
|
156
|
-
false
|
|
90
|
+
case node.type
|
|
91
|
+
when :array then true
|
|
92
|
+
when :send then %i[map select collect flat_map to_a uniq compact].include?(node.children[1])
|
|
93
|
+
else false
|
|
157
94
|
end
|
|
158
95
|
end
|
|
159
96
|
|
|
@@ -173,14 +110,11 @@ module EagerEye
|
|
|
173
110
|
return "" unless node.is_a?(Parser::AST::Node)
|
|
174
111
|
|
|
175
112
|
case node.type
|
|
176
|
-
when :lvar
|
|
177
|
-
node.children[0].to_s
|
|
113
|
+
when :lvar then node.children[0].to_s
|
|
178
114
|
when :send
|
|
179
115
|
receiver_str = reconstruct_chain(node.children[0])
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
else
|
|
183
|
-
""
|
|
116
|
+
receiver_str.empty? ? node.children[1].to_s : "#{receiver_str}.#{node.children[1]}"
|
|
117
|
+
else ""
|
|
184
118
|
end
|
|
185
119
|
end
|
|
186
120
|
end
|