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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27aed443ecc5e83aeb4fe0baa9aa9ccbcec462df45767c8294d867562c9066b5
4
- data.tar.gz: f2a9f245154a8fe043d9a9f0f68edaf940a5c3f8e2bd3f0255962603ef48d4d3
3
+ metadata.gz: 8b6c076a6dd51cb872bfb213cbb83369a7d9f159bb20054b41901710b738b4df
4
+ data.tar.gz: 15850c235c48858cbbb2585c83ffaeacf1331a029b959f973aede31ef584ae24
5
5
  SHA512:
6
- metadata.gz: f6f769f5d65827fd4059671674f8947f172b1ef9ff72a82375c25ac3e31c35c81099384e79de97036b8c867613343fd2e078f7c23f59fd68bf5daac8cd8a8a19
7
- data.tar.gz: f6c0743373d769ec6d147ccb1045b6a667d6015551a705b31eaefe6fc511568ecf06683cb99d7034f2d59cd83997732096d8e7c12b732b42c6ee460b9d152b5c
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.0.10] - 2025-12-26
10
+ ## [1.1.1] - 2026-01-03
11
11
 
12
- ### Changed
12
+ ### Fixed
13
13
 
14
- - **PluckToArray Severity Levels** - Differentiate between scoped and unscoped pluck usage
15
- - Scoped `.pluck(:id)` (e.g., `User.active.pluck(:id)`) **Warning** (default)
16
- - Unscoped `.all.pluck(:id)` **Error** (loads entire table into memory)
17
- - Small arrays with scoped pluck may be acceptable - users can suppress with comments
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
- - **Improved PluckToArray Detection** - Now detects and prioritizes `.all.pluck` patterns
22
- - Tracks `.all.pluck(:id)` as critical issues (error severity)
23
- - Detects both variable assignments and inline usage
24
- - Better suggestions for high-priority cases
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
@@ -74,7 +74,7 @@ end
74
74
  ## Code Style
75
75
 
76
76
  - Follow RuboCop rules (run `bundle exec rubocop`)
77
- - Maintain 90%+ test coverage
77
+ - Maintain 95%+ test coverage
78
78
  - Document public methods
79
79
  - Keep methods small and focused
80
80
 
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.0.10-red.svg" alt="Gem Version"></a>
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) # Query 1: SELECT id FROM users
317
- Post.where(user_id: user_ids) # Query 2: SELECT * FROM posts WHERE user_id IN (1,2,3...)
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
- # Bad - Same problem with map
325
- user_ids = users.map(&:id)
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, no memory overhead
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 Levels:**
334
-
335
- - ⚠️ **Warning (default)** - Scoped `.pluck(:id)` (e.g., `User.active.pluck(:id)`)
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
@@ -4,6 +4,7 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------- | ------------------ |
7
+ | 1.1.x | :white_check_mark: |
7
8
  | 1.0.x | :white_check_mark: |
8
9
  | < 1.0 | :x: |
9
10
 
@@ -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
- ruby_files.each do |file_path|
28
- analyze_file(file_path)
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
- @issues
47
+ def model_files
48
+ Dir.glob(File.join(@paths[0], "models", "**", "*.rb"))
32
49
  end
33
50
 
34
- private
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
- file_issues = detector.detect(ast, file_path)
88
+ args = [ast, file_path]
89
+ args << @association_preloads if detector.is_a?(Detectors::LoopAssociation)
65
90
 
66
- # Filter suppressed issues
67
- file_issues.reject! do |issue|
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
@@ -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
- if @interactive
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
- source = read_file(issue.file_path)
41
- fixer = FixerRegistry.fixer_for(issue, source)
42
- next unless fixer&.fixable?
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
- return 0 unless options[:fail_on_issues]
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
- return false unless node.type == :send
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 method_definition?(node)
74
+ if node.type == :def && @callback_methods.key?(node.children[0])
82
75
  method_name = node.children[0]
83
- if @callback_methods.key?(method_name)
84
- callback_type = @callback_methods[method_name]
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 do |child|
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 do |child|
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 do |child|
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
- return false unless node.type == :send
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
- return false unless node.type == :block
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 = extract_block_body(node)
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 do |child|
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
- return false unless node.type == :block
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
- return false unless node.type == :send
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
- # post.comments.count -> receiver is post.comments
86
- # post.comments -> receiver is post (lvar)
87
- if receiver.type == :lvar && receiver.children[0] == block_var
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
- node.children[0] == block_var
103
- when :send
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
- return nil unless first_arg&.type == :arg
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)