eager_eye 1.2.11 → 1.2.13
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 +10 -0
- data/README.md +15 -3
- data/lib/eager_eye/analyzer.rb +1 -1
- data/lib/eager_eye/detectors/callback_query.rb +1 -1
- data/lib/eager_eye/detectors/count_in_iteration.rb +1 -1
- data/lib/eager_eye/detectors/custom_method_query.rb +1 -1
- data/lib/eager_eye/detectors/delegation_n_plus_one.rb +1 -1
- data/lib/eager_eye/detectors/loop_association.rb +1 -1
- data/lib/eager_eye/detectors/missing_counter_cache.rb +1 -1
- data/lib/eager_eye/detectors/scope_chain_n_plus_one.rb +1 -1
- data/lib/eager_eye/detectors/validation_n_plus_one.rb +1 -1
- data/lib/eager_eye/fixer_registry.rb +3 -1
- data/lib/eager_eye/fixers/add_includes.rb +62 -0
- data/lib/eager_eye/fixers/count_to_size.rb +24 -0
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c9b4195fa833057ee04ef87d148e78de8463fdbca97e234ba9d5b08ecaab9afc
|
|
4
|
+
data.tar.gz: f5c45a109aba0f563866669c4b6a32ce0823990fe5de467d65f6baea8d405197
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8312903e2476381ad7d9fe8a5a22feb74eb97937c5d488efc96448df9b0f5973f9d612c35cccd5235900e2eb25011694c862bb1a45a5bdeb70b2a8e17be155e2
|
|
7
|
+
data.tar.gz: cc7b4336007a72b0d4131b311a2897d4dae9cf0b4ba18ceb4826f25931875f418067bd5a16e6d1fec472cbb29abd7d1f2d2f0b58cc943b871fdea4e9991fa623
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.12] - 2026-03-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`.jbuilder` File Support** - Analyzer now scans `.jbuilder` files for N+1 queries
|
|
15
|
+
- `json.array!` blocks are recognized as iteration patterns across all detectors
|
|
16
|
+
- Existing detectors (LoopAssociation, CountInIteration, etc.) work seamlessly on jbuilder views
|
|
17
|
+
- **`CountToSize` Auto-fix** - Automatically replaces `.count` with `.size` inside iteration blocks
|
|
18
|
+
- **`AddIncludes` Auto-fix** - Suggests and applies `.includes(:association)` before iteration calls
|
|
19
|
+
|
|
10
20
|
## [1.2.11] - 2026-03-17
|
|
11
21
|
|
|
12
22
|
### 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.12-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>
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
|
|
58
58
|
🔧 **Developer-friendly:**
|
|
59
59
|
- Inline suppression (like RuboCop)
|
|
60
|
-
- Auto-fix support (
|
|
60
|
+
- Auto-fix support (3 fixers: PluckToSelect, CountToSize, AddIncludes)
|
|
61
|
+
- `.jbuilder` file support (`json.array!` iteration detection)
|
|
61
62
|
- JSON/Console output formats
|
|
62
63
|
- RSpec integration
|
|
63
64
|
|
|
@@ -524,6 +525,8 @@ eager_eye --fix --force
|
|
|
524
525
|
| Issue | Fix |
|
|
525
526
|
|-------|-----|
|
|
526
527
|
| `.pluck(:id)` inline | → `.select(:id)` |
|
|
528
|
+
| `.count` in iteration | → `.size` |
|
|
529
|
+
| Missing `includes` before loop | → `.includes(:assoc)` inserted |
|
|
527
530
|
|
|
528
531
|
### Example
|
|
529
532
|
|
|
@@ -535,6 +538,15 @@ app/services/user_service.rb:
|
|
|
535
538
|
- Post.where(user_id: User.active.pluck(:id))
|
|
536
539
|
+ Post.where(user_id: User.active.select(:id))
|
|
537
540
|
|
|
541
|
+
app/controllers/posts_controller.rb:
|
|
542
|
+
Line 8:
|
|
543
|
+
- user.posts.count
|
|
544
|
+
+ user.posts.size
|
|
545
|
+
|
|
546
|
+
Line 5:
|
|
547
|
+
- @posts.each do |post|
|
|
548
|
+
+ @posts.includes(:author).each do |post|
|
|
549
|
+
|
|
538
550
|
$ eager_eye --fix
|
|
539
551
|
app/services/user_service.rb:12
|
|
540
552
|
- Post.where(user_id: User.active.pluck(:id))
|
|
@@ -739,7 +751,7 @@ EagerEye uses static analysis, which means:
|
|
|
739
751
|
- **No runtime context** - Cannot know if associations are already eager loaded elsewhere
|
|
740
752
|
- **Heuristic-based** - Uses naming conventions to identify associations (may have false positives)
|
|
741
753
|
- **Ruby code only** - Does not analyze SQL queries or ActiveRecord internals
|
|
742
|
-
- **Cross-file scope** - Cross-file analysis
|
|
754
|
+
- **Cross-file scope** - Cross-file analysis covers model-defined query methods; controller-to-view or service-to-service patterns are not yet tracked
|
|
743
755
|
|
|
744
756
|
For best results, use EagerEye alongside runtime tools like Bullet for comprehensive N+1 detection.
|
|
745
757
|
|
data/lib/eager_eye/analyzer.rb
CHANGED
|
@@ -99,7 +99,7 @@ module EagerEye
|
|
|
99
99
|
|
|
100
100
|
def resolve_path(path)
|
|
101
101
|
return [path] if File.file?(path)
|
|
102
|
-
return Dir.glob(File.join(path, "**", "*.rb")) if File.directory?(path)
|
|
102
|
+
return Dir.glob(File.join(path, "**", "*.{rb,jbuilder}")) if File.directory?(path)
|
|
103
103
|
|
|
104
104
|
Dir.glob(path)
|
|
105
105
|
end
|
|
@@ -27,7 +27,7 @@ module EagerEye
|
|
|
27
27
|
].freeze
|
|
28
28
|
|
|
29
29
|
ITERATION_METHODS = %i[each map select find_all reject collect
|
|
30
|
-
find_each find_in_batches in_batches].freeze
|
|
30
|
+
find_each find_in_batches in_batches array!].freeze
|
|
31
31
|
AR_BATCH_METHODS = %i[find_each find_in_batches in_batches].freeze
|
|
32
32
|
NON_AR_NAMESPACES = %w[Sidekiq Redis ActionCable ActionMailer Kafka].freeze
|
|
33
33
|
TRANSACTIONAL_CALLBACKS = %i[before_validation before_save before_create before_update before_destroy
|
|
@@ -5,7 +5,7 @@ module EagerEye
|
|
|
5
5
|
class CountInIteration < Base
|
|
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
|
-
find_each find_in_batches in_batches].freeze
|
|
8
|
+
find_each find_in_batches in_batches array!].freeze
|
|
9
9
|
ARRAY_METHOD_SUFFIXES = %w[_ids _tags _types _codes _names _values].freeze
|
|
10
10
|
|
|
11
11
|
def self.detector_name
|
|
@@ -10,7 +10,7 @@ module EagerEye
|
|
|
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
12
|
each_with_index each_with_object reduce inject
|
|
13
|
-
find_each find_in_batches in_batches].freeze
|
|
13
|
+
find_each find_in_batches in_batches array!].freeze
|
|
14
14
|
|
|
15
15
|
def self.detector_name
|
|
16
16
|
:custom_method_query
|
|
@@ -5,7 +5,7 @@ module EagerEye
|
|
|
5
5
|
class DelegationNPlusOne < Base
|
|
6
6
|
ITERATION_METHODS = %i[
|
|
7
7
|
each map collect select find_all reject filter filter_map flat_map
|
|
8
|
-
find_each find_in_batches in_batches
|
|
8
|
+
find_each find_in_batches in_batches array!
|
|
9
9
|
].freeze
|
|
10
10
|
PRELOAD_METHODS = %i[includes preload eager_load].freeze
|
|
11
11
|
|
|
@@ -5,7 +5,7 @@ module EagerEye
|
|
|
5
5
|
class LoopAssociation < Base
|
|
6
6
|
ITERATION_METHODS = %i[each map collect select find find_all reject filter filter_map flat_map
|
|
7
7
|
each_with_index each_with_object reduce inject
|
|
8
|
-
find_each find_in_batches in_batches].freeze
|
|
8
|
+
find_each find_in_batches in_batches array!].freeze
|
|
9
9
|
PRELOAD_METHODS = %i[includes preload eager_load].freeze
|
|
10
10
|
SINGLE_RECORD_METHODS = %i[find find_by find_by! first first! last last! take take! second third fourth fifth
|
|
11
11
|
forty_two sole find_sole_by].freeze
|
|
@@ -11,7 +11,7 @@ module EagerEye
|
|
|
11
11
|
].freeze
|
|
12
12
|
ITERATION_METHODS = %i[each map collect select reject find_all filter filter_map flat_map
|
|
13
13
|
each_with_index each_with_object reduce inject sum
|
|
14
|
-
find_each find_in_batches in_batches].freeze
|
|
14
|
+
find_each find_in_batches in_batches array!].freeze
|
|
15
15
|
|
|
16
16
|
def self.detector_name
|
|
17
17
|
:missing_counter_cache
|
|
@@ -4,7 +4,7 @@ module EagerEye
|
|
|
4
4
|
module Detectors
|
|
5
5
|
class ScopeChainNPlusOne < Base
|
|
6
6
|
ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map
|
|
7
|
-
find_each find_in_batches in_batches].freeze
|
|
7
|
+
find_each find_in_batches in_batches array!].freeze
|
|
8
8
|
|
|
9
9
|
def self.detector_name
|
|
10
10
|
:scope_chain_n_plus_one
|
|
@@ -4,7 +4,7 @@ module EagerEye
|
|
|
4
4
|
module Detectors
|
|
5
5
|
class ValidationNPlusOne < Base
|
|
6
6
|
ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map
|
|
7
|
-
find_each find_in_batches in_batches].freeze
|
|
7
|
+
find_each find_in_batches in_batches array!].freeze
|
|
8
8
|
CREATE_METHODS = %i[create create!].freeze
|
|
9
9
|
SAVE_METHODS = %i[save save!].freeze
|
|
10
10
|
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
class FixerRegistry
|
|
5
5
|
FIXERS = {
|
|
6
|
-
pluck_to_array: Fixers::PluckToSelect
|
|
6
|
+
pluck_to_array: Fixers::PluckToSelect,
|
|
7
|
+
count_in_iteration: Fixers::CountToSize,
|
|
8
|
+
loop_association: Fixers::AddIncludes
|
|
7
9
|
}.freeze
|
|
8
10
|
|
|
9
11
|
def self.fixer_for(issue, source_code)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Fixers
|
|
5
|
+
class AddIncludes < Base
|
|
6
|
+
ITERATION_METHODS_RE = %w[
|
|
7
|
+
each map collect select find_all reject filter filter_map
|
|
8
|
+
flat_map find_each find_in_batches in_batches
|
|
9
|
+
].join("|")
|
|
10
|
+
ITERATION_PATTERN = /\.(#{ITERATION_METHODS_RE})\b/
|
|
11
|
+
|
|
12
|
+
def fixable?
|
|
13
|
+
issue.detector == :loop_association &&
|
|
14
|
+
!association_name.nil? &&
|
|
15
|
+
!iteration_line_index.nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def diff
|
|
19
|
+
return nil unless fixable?
|
|
20
|
+
|
|
21
|
+
idx = iteration_line_index
|
|
22
|
+
original_line = @source_lines[idx]
|
|
23
|
+
fixed_line = insert_includes(original_line)
|
|
24
|
+
return nil if original_line == fixed_line
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
file: issue.file_path,
|
|
28
|
+
line: idx + 1,
|
|
29
|
+
original: original_line.chomp,
|
|
30
|
+
fixed: fixed_line.chomp
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
protected
|
|
35
|
+
|
|
36
|
+
def fixed_content
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def association_name
|
|
43
|
+
return nil unless issue.suggestion
|
|
44
|
+
|
|
45
|
+
match = issue.suggestion.match(/includes\(:(\w+)\)/)
|
|
46
|
+
match && match[1]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def iteration_line_index
|
|
50
|
+
start = issue.line_number - 2
|
|
51
|
+
start.downto([start - 10, 0].max) do |i|
|
|
52
|
+
return i if @source_lines[i]&.match?(ITERATION_PATTERN)
|
|
53
|
+
end
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def insert_includes(line)
|
|
58
|
+
line.sub(ITERATION_PATTERN, ".includes(:#{association_name})\\0")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Fixers
|
|
5
|
+
class CountToSize < Base
|
|
6
|
+
def fixable?
|
|
7
|
+
issue.detector == :count_in_iteration &&
|
|
8
|
+
single_line_count?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def fixed_content
|
|
14
|
+
line_content.sub(/\.count\b/, ".size")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def single_line_count?
|
|
20
|
+
line_content&.match?(/\.count\b/)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -25,6 +25,8 @@ require_relative "eager_eye/comment_parser"
|
|
|
25
25
|
require_relative "eager_eye/analyzer"
|
|
26
26
|
require_relative "eager_eye/fixers/base"
|
|
27
27
|
require_relative "eager_eye/fixers/pluck_to_select"
|
|
28
|
+
require_relative "eager_eye/fixers/count_to_size"
|
|
29
|
+
require_relative "eager_eye/fixers/add_includes"
|
|
28
30
|
require_relative "eager_eye/fixer_registry"
|
|
29
31
|
require_relative "eager_eye/auto_fixer"
|
|
30
32
|
require_relative "eager_eye/reporters/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.13
|
|
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-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|
|
@@ -80,7 +80,9 @@ files:
|
|
|
80
80
|
- lib/eager_eye/detectors/serializer_nesting.rb
|
|
81
81
|
- lib/eager_eye/detectors/validation_n_plus_one.rb
|
|
82
82
|
- lib/eager_eye/fixer_registry.rb
|
|
83
|
+
- lib/eager_eye/fixers/add_includes.rb
|
|
83
84
|
- lib/eager_eye/fixers/base.rb
|
|
85
|
+
- lib/eager_eye/fixers/count_to_size.rb
|
|
84
86
|
- lib/eager_eye/fixers/pluck_to_select.rb
|
|
85
87
|
- lib/eager_eye/generators/install_generator.rb
|
|
86
88
|
- lib/eager_eye/issue.rb
|