eager_eye 1.2.6 → 1.2.7

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: f996747b6c12e539180e66cc74c11c70acfab4a6e4b84aed4039cc7b58dafe53
4
- data.tar.gz: f23d5a21ceecc39c71b0303a5748472a8ea5e0f5f3256ffd5a10084a5af8a39c
3
+ metadata.gz: dd5177a12d64b54cc6cd426440f79018da9ccc7dd931db70c491c9154bb385a1
4
+ data.tar.gz: dec16deb3b55ab821225d5d0e06a6fa308c41fd472dad405bf125c7e5839f2e0
5
5
  SHA512:
6
- metadata.gz: a75d25669ab2d7e96dbccb107ae30f817e5fd6a291651b68bc56dbea21f50bd5c8b24b0633dd21d6faeeb5a8abd999abc6baf57ac9c7e8c3c06aa4542dc570d3
7
- data.tar.gz: 7d2e5af151c02923bf4e64f05bc277711228729fa08ddee3dd79699623efc33f3689290c1739c025d7851db0b59d90feeac99e3a65c9a13862fac8f36ddf46b6
6
+ metadata.gz: 4336421ff71269732f594163dc3dbe29ca70d1b10c19dd333d972ea63bff9da36a4e83a041263fa6f61d758c8b0551912e075e00ce8f05978ab59c7db698af41
7
+ data.tar.gz: 51de528f0dca6b3279c0cb1fe6f21e5c35592dda81220419c209d8d43faf35b41375cbfffd33a463546f5e81e4807f8c48268f618d8383c82a570f81da823efa
data/CHANGELOG.md CHANGED
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.7] - 2026-03-10
11
+
12
+ ### Added
13
+
14
+ - **New Detector: `ScopeChainNPlusOne`** - Detects scope calls on associations inside iterations
15
+ - Catches `post.comments.recent`, `post.comments.approved.count` patterns
16
+ - Parses model files for `scope :name, -> { ... }` declarations
17
+ - Flags known scope names called on association chains inside loops
18
+ - Each scope call executes a new query per iteration — suggests preloading or joined queries
19
+ - **New Parser: `ScopeParser`** - Collects scope definitions from model files for cross-file detection
20
+
10
21
  ## [1.2.6] - 2026-02-25
11
22
 
12
23
  ### 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.6-red.svg" alt="Gem Version"></a>
13
+ <a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.2.7-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>
@@ -42,7 +42,7 @@
42
42
 
43
43
  ## Features
44
44
 
45
- ✨ **Detects 9 types of N+1 problems:**
45
+ ✨ **Detects 10 types of N+1 problems:**
46
46
  - Loop associations (queries in iterations)
47
47
  - Serializer nesting issues
48
48
  - Missing counter caches
@@ -52,6 +52,7 @@
52
52
  - Pluck to array misuse
53
53
  - Delegation N+1s (hidden via `delegate :method, to: :association`)
54
54
  - Decorator N+1s (Draper, SimpleDelegator, Presenter, ViewObject)
55
+ - Scope chain N+1s (named scopes on associations in loops)
55
56
 
56
57
  🔧 **Developer-friendly:**
57
58
  - Inline suppression (like RuboCop)
@@ -398,6 +399,33 @@ Supports the following object references inside decorators:
398
399
  - `__getobj__` — SimpleDelegator standard
399
400
  - `source`, `model` — alternative Draper aliases
400
401
 
402
+ ### 10. Scope Chain N+1
403
+
404
+ Detects named scope calls on associations inside iterations. Unlike explicit query methods (`.where`, `.find_by`) caught by `CustomMethodQuery`, named scopes (`.recent`, `.active`, `.published`) are invisible query triggers.
405
+
406
+ ```ruby
407
+ # Model
408
+ class Comment < ApplicationRecord
409
+ scope :recent, -> { where("created_at > ?", 1.week.ago) }
410
+ scope :approved, -> { where(approved: true) }
411
+ end
412
+
413
+ # Bad - scope call per iteration
414
+ posts.each do |post|
415
+ post.comments.recent # Query for each post!
416
+ post.comments.approved.count # Query for each post!
417
+ end
418
+
419
+ # Good - preload and filter in Ruby
420
+ posts.includes(:comments).each do |post|
421
+ post.comments.select { |c| c.created_at > 1.week.ago }
422
+ end
423
+ ```
424
+
425
+ EagerEye detects these by:
426
+ 1. Scanning model files for `scope :name, -> { ... }` declarations
427
+ 2. Flagging known scope names called on association chains inside iteration blocks
428
+
401
429
  ## Inline Suppression
402
430
 
403
431
  Suppress false positives using inline comments (RuboCop-style):
@@ -442,6 +470,7 @@ Both CamelCase and snake_case formats are accepted:
442
470
  | Pluck to Array | `PluckToArray` | `pluck_to_array` |
443
471
  | Delegation N+1 | `DelegationNPlusOne` | `delegation_n_plus_one` |
444
472
  | Decorator N+1 | `DecoratorNPlusOne` | `decorator_n_plus_one` |
473
+ | Scope Chain N+1 | `ScopeChainNPlusOne` | `scope_chain_n_plus_one` |
445
474
  | All Detectors | `all` | `all` |
446
475
 
447
476
  ## Auto-fix (Experimental)
@@ -546,18 +575,20 @@ enabled_detectors:
546
575
  - pluck_to_array
547
576
  - delegation_n_plus_one
548
577
  - decorator_n_plus_one
578
+ - scope_chain_n_plus_one
549
579
 
550
580
  # Severity levels per detector (error, warning, info)
551
581
  severity_levels:
552
- loop_association: error # Definite N+1
582
+ loop_association: error # Definite N+1
553
583
  serializer_nesting: warning
554
584
  custom_method_query: warning
555
585
  count_in_iteration: warning
556
586
  callback_query: warning
557
- pluck_to_array: warning # Optimization
558
- delegation_n_plus_one: warning # Hidden delegation N+1
559
- decorator_n_plus_one: warning # Decorator/Presenter N+1
560
- missing_counter_cache: info # Suggestion
587
+ pluck_to_array: warning # Optimization
588
+ delegation_n_plus_one: warning # Hidden delegation N+1
589
+ decorator_n_plus_one: warning # Decorator/Presenter N+1
590
+ scope_chain_n_plus_one: warning # Scope chain on association
591
+ missing_counter_cache: info # Suggestion
561
592
 
562
593
  # Minimum severity to report (default: info)
563
594
  min_severity: warning
@@ -13,16 +13,18 @@ module EagerEye
13
13
  callback_query: Detectors::CallbackQuery,
14
14
  pluck_to_array: Detectors::PluckToArray,
15
15
  delegation_n_plus_one: Detectors::DelegationNPlusOne,
16
- decorator_n_plus_one: Detectors::DecoratorNPlusOne
16
+ decorator_n_plus_one: Detectors::DecoratorNPlusOne,
17
+ scope_chain_n_plus_one: Detectors::ScopeChainNPlusOne
17
18
  }.freeze
18
19
 
19
- attr_reader :paths, :issues, :association_preloads, :delegation_maps
20
+ attr_reader :paths, :issues, :association_preloads, :delegation_maps, :scope_maps
20
21
 
21
22
  def initialize(paths: nil)
22
23
  @paths = Array(paths || EagerEye.configuration.app_path)
23
24
  @issues = []
24
25
  @association_preloads = {}
25
26
  @delegation_maps = {}
27
+ @scope_maps = {}
26
28
  end
27
29
 
28
30
  def run
@@ -48,6 +50,10 @@ module EagerEye
48
50
  deleg_parser = DelegationParser.new
49
51
  deleg_parser.parse_model(ast, model_name)
50
52
  @delegation_maps.merge!(deleg_parser.delegation_maps)
53
+
54
+ scope_parser = ScopeParser.new
55
+ scope_parser.parse_model(ast, model_name)
56
+ @scope_maps.merge!(scope_parser.scope_maps)
51
57
  rescue Errno::ENOENT, Errno::EACCES
52
58
  next
53
59
  end
@@ -108,6 +114,7 @@ module EagerEye
108
114
  args = [ast, file_path]
109
115
  args << @association_preloads if detector.is_a?(Detectors::LoopAssociation)
110
116
  args << @delegation_maps if detector.is_a?(Detectors::DelegationNPlusOne)
117
+ args << @scope_maps if detector.is_a?(Detectors::ScopeChainNPlusOne)
111
118
  args
112
119
  end
113
120
 
@@ -9,6 +9,7 @@ module EagerEye
9
9
  loop_association serializer_nesting missing_counter_cache
10
10
  custom_method_query count_in_iteration callback_query
11
11
  pluck_to_array delegation_n_plus_one decorator_n_plus_one
12
+ scope_chain_n_plus_one
12
13
  ].freeze
13
14
 
14
15
  DEFAULT_SEVERITY_LEVELS = {
@@ -20,7 +21,8 @@ module EagerEye
20
21
  callback_query: :warning,
21
22
  pluck_to_array: :warning,
22
23
  delegation_n_plus_one: :warning,
23
- decorator_n_plus_one: :warning
24
+ decorator_n_plus_one: :warning,
25
+ scope_chain_n_plus_one: :warning
24
26
  }.freeze
25
27
 
26
28
  VALID_SEVERITIES = %i[info warning error].freeze
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.2.6"
4
+ VERSION = "1.2.7"
5
5
  end
data/lib/eager_eye.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "eager_eye/configuration"
6
6
  require_relative "eager_eye/issue"
7
7
  require_relative "eager_eye/association_parser"
8
8
  require_relative "eager_eye/delegation_parser"
9
+ require_relative "eager_eye/scope_parser"
9
10
  require_relative "eager_eye/detectors/base"
10
11
  require_relative "eager_eye/detectors/loop_association"
11
12
  require_relative "eager_eye/detectors/serializer_nesting"
@@ -16,6 +17,7 @@ require_relative "eager_eye/detectors/callback_query"
16
17
  require_relative "eager_eye/detectors/pluck_to_array"
17
18
  require_relative "eager_eye/detectors/delegation_n_plus_one"
18
19
  require_relative "eager_eye/detectors/decorator_n_plus_one"
20
+ require_relative "eager_eye/detectors/scope_chain_n_plus_one"
19
21
  require_relative "eager_eye/comment_parser"
20
22
  require_relative "eager_eye/analyzer"
21
23
  require_relative "eager_eye/fixers/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.6
4
+ version: 1.2.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-02-25 00:00:00.000000000 Z
11
+ date: 2026-03-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast