eager_eye 1.1.0 → 1.1.2
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 +16 -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 +22 -47
- 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: 22770991f73a5bc9ca8694ae4e45212fc6c0517036dad5a8734a11af90aa4496
|
|
4
|
+
data.tar.gz: bcdbdf3076b011a7a4eb83d62d34b4173d7f6c5cb1dff3c0ac767bbd0f47c316
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fcf06b3db031246fcd6549bff94a13f5e30b29da2d72a6e3c2d233fcb9740cf4a60f807e190e3f082620eda08755e90a01e96915ac873f4205f3938c44de1e8a
|
|
7
|
+
data.tar.gz: 93a926cb2d961f1b5c8341df4c7ac0dbcb3d482e9bbc00b61b7d84ed3c457432611792f999d9c02b7dcb164cf3d238c561d89392b5cb67bb287e57dcbb008a12
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.1.2] - 2026-01-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **CallbackQuery False Positive** - Only flag iterations that contain actual AR query methods
|
|
15
|
+
- Non-AR iterations (Redis, Sidekiq, mailers) are no longer flagged
|
|
16
|
+
- `Sidekiq::ScheduledSet.new.select { |job| ... }.each(&:delete)` no longer triggers warning
|
|
17
|
+
|
|
18
|
+
## [1.1.1] - 2026-01-03
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **SerializerNesting False Positive** - No longer flags `belongs_to` associations
|
|
23
|
+
- `user.author`, `subscription.user` etc. (singular) are now ignored
|
|
24
|
+
- Only `has_many` associations (plural names) are flagged as potential N+1
|
|
25
|
+
|
|
10
26
|
## [1.1.0] - 2025-12-28
|
|
11
27
|
|
|
12
28
|
### 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.2-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
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
node.children.each do |child|
|
|
90
|
-
check_callback_methods(child)
|
|
76
|
+
body = node.children[2]
|
|
77
|
+
find_iterations_with_queries(body, method_name, @callback_methods[method_name]) if body
|
|
91
78
|
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
79
|
|
|
102
|
-
|
|
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)
|
|
@@ -107,13 +85,13 @@ module EagerEye
|
|
|
107
85
|
|
|
108
86
|
if iteration_block?(node)
|
|
109
87
|
block_var = extract_block_variable(node)
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
if block_var && contains_ar_query_on_variable?(node, block_var)
|
|
89
|
+
add_iteration_issue(node, method_name, callback_type)
|
|
90
|
+
find_query_calls_in_block(node, method_name, callback_type, block_var)
|
|
91
|
+
end
|
|
112
92
|
end
|
|
113
93
|
|
|
114
|
-
node.children.each
|
|
115
|
-
find_iterations_with_queries(child, method_name, callback_type)
|
|
116
|
-
end
|
|
94
|
+
node.children.each { |child| find_iterations_with_queries(child, method_name, callback_type) }
|
|
117
95
|
end
|
|
118
96
|
|
|
119
97
|
def find_query_calls_in_block(node, method_name, callback_type, block_var)
|
|
@@ -123,26 +101,16 @@ module EagerEye
|
|
|
123
101
|
add_query_issue(node, method_name, callback_type)
|
|
124
102
|
end
|
|
125
103
|
|
|
126
|
-
node.children.each
|
|
127
|
-
find_query_calls_in_block(child, method_name, callback_type, block_var)
|
|
128
|
-
end
|
|
104
|
+
node.children.each { |child| find_query_calls_in_block(child, method_name, callback_type, block_var) }
|
|
129
105
|
end
|
|
130
106
|
|
|
131
107
|
def query_call?(node)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
method = node.children[1]
|
|
135
|
-
QUERY_INDICATORS.include?(method)
|
|
108
|
+
node.type == :send && QUERY_INDICATORS.include?(node.children[1])
|
|
136
109
|
end
|
|
137
110
|
|
|
138
111
|
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)
|
|
112
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
113
|
+
ITERATION_METHODS.include?(node.children[0].children[1])
|
|
146
114
|
end
|
|
147
115
|
|
|
148
116
|
def add_query_issue(node, method_name, callback_type)
|
|
@@ -189,6 +157,13 @@ module EagerEye
|
|
|
189
157
|
false
|
|
190
158
|
end
|
|
191
159
|
end
|
|
160
|
+
|
|
161
|
+
def contains_ar_query_on_variable?(node, block_var)
|
|
162
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
163
|
+
return true if query_call?(node) && receiver_chain_starts_with?(node.children[0], block_var)
|
|
164
|
+
|
|
165
|
+
node.children.any? { |child| contains_ar_query_on_variable?(child, block_var) }
|
|
166
|
+
end
|
|
192
167
|
end
|
|
193
168
|
end
|
|
194
169
|
end
|
|
@@ -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)
|