eager_eye 1.2.15 → 1.3.0

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.
data/README.tr.md ADDED
@@ -0,0 +1,489 @@
1
+ <p align="center">
2
+ <img src="images/icon.png" alt="EagerEye" width="140">
3
+ </p>
4
+
5
+ <h1 align="center">EagerEye</h1>
6
+
7
+ <p align="center">
8
+ <strong>Rails uygulamandaki N+1 sorgularını yakala — uygulamayı çalıştırmadan.</strong><br>
9
+ <sub>Ruby AST tabanlı statik analiz. Hızlı. Sıfır runtime maliyeti. CI'a hazır.</sub>
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="README.md">English</a> · <strong>Türkçe</strong>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <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>
18
+ <a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/gem/v/eager_eye?color=red&label=gem" alt="Gem Version"></a>
19
+ <a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/gem/dt/eager_eye?color=blue&label=indirme" alt="İndirmeler"></a>
20
+ <a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%3E%3D%203.1-CC342D" alt="Ruby"></a>
21
+ <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/github/license/hamzagedikkaya/eager_eye" alt="Lisans"></a>
22
+ <a href="https://marketplace.visualstudio.com/items?itemName=hamzagedikkaya.eager-eye"><img src="https://img.shields.io/badge/VS%20Code-Eklenti-007ACC?logo=visualstudiocode&logoColor=white" alt="VS Code Eklentisi"></a>
23
+ </p>
24
+
25
+ > 💡 **Editör içinde uyarı görmeyi mi tercih edersin?** [VS Code eklentisini](https://marketplace.visualstudio.com/items?itemName=hamzagedikkaya.eager-eye) kur — aynı motor, kaydettiğinde çalışır, sorunları doğrudan ilgili satırın yanında gösterir. CLI ile aynı hızda, sadece daha akıcı bir geri bildirim döngüsü.
26
+
27
+ ---
28
+
29
+ ## Neden EagerEye?
30
+
31
+ **Bullet** N+1'leri test'lerin onlara denk geldiğinde bulur. **EagerEye** ise statik olarak bulur — kod hiç çalışmadan.
32
+
33
+ - 🎯 **Test'lerin kaçırdıklarını yakala** — Test suite'in girmediği kod yollarındaki N+1'ler de işaretlenir.
34
+ - ⚡ **Her PR'da CI'da çalıştır** — DB yok, fixture yok, Rails boot yok. Sadece `eager_eye app/`.
35
+ - 🔬 **11 detector tipi** — basit loop erişiminin ötesinde: serializer nesting, callback query'leri, decorator/delegation tuzakları, batch validation, scope chain'leri, plucked-array yanlış kullanımı ve dahası.
36
+ - 🤝 **Bullet ile iyi anlaşır** — statik + runtime farklı kör noktaları kapatır. İkisini birden kullan.
37
+
38
+ ## Kurulum
39
+
40
+ ```ruby
41
+ # Gemfile
42
+ gem "eager_eye", group: :development
43
+ ```
44
+
45
+ ```bash
46
+ bundle install
47
+ ```
48
+
49
+ Veya bağımsız:
50
+
51
+ ```bash
52
+ gem install eager_eye
53
+ ```
54
+
55
+ ## Hızlı Başlangıç
56
+
57
+ ```bash
58
+ # Varsayılan app/ dizinini tara
59
+ eager_eye
60
+
61
+ # Veya belirli yolları tara
62
+ eager_eye app/controllers app/serializers
63
+
64
+ # Config dosyası oluştur (opsiyonel)
65
+ rails g eager_eye:install
66
+
67
+ # Rake ile çalıştır
68
+ rake eager_eye:analyze
69
+ ```
70
+
71
+ Örnek çıktı:
72
+
73
+ ```text
74
+ app/controllers/posts_controller.rb
75
+ Line 15: [LoopAssociation] Olası N+1 sorgu: `post.author` iterasyon içinde çağrılıyor
76
+ Öneri: Iterasyondan önce koleksiyona `includes(:author)` ekle
77
+
78
+ Line 23: [MissingCounterCache] `comments` üzerinde `.count` çağrısı N+1'e yol açabilir
79
+ Öneri: belongs_to ilişkisine `counter_cache: true` ekle
80
+
81
+ Total: 2 issues (2 warnings, 0 errors)
82
+ ```
83
+
84
+ ## Neyi tespit eder
85
+
86
+ | # | Detector | Neyi yakalar |
87
+ |---|---|---|
88
+ | 1 | **LoopAssociation** | `each`/`map`/`find_each` vb. içinde preload edilmemiş ilişki çağrıları |
89
+ | 2 | **SerializerNesting** | Blueprinter / ActiveModel::Serializer / Alba block'larında nested ilişki erişimi |
90
+ | 3 | **MissingCounterCache** | Loop içinde counter cache'le çözülebilecek `.count` / `.size` çağrıları |
91
+ | 4 | **CustomMethodQuery** | Iterasyon içinde ilişki zincirinde `.where`, `.find_by`, `.exists?` vb. |
92
+ | 5 | **CountInIteration** | `.size` (preload kullanır) yeterken loop'ta `.count` (her zaman query) kullanımı |
93
+ | 6 | **CallbackQuery** | ActiveRecord callback'leri içinde iterasyon kaynaklı sorgular (`after_save`, `after_create`, ...) |
94
+ | 7 | **PluckToArray** | `.pluck(:id)` sonucunun subquery yerine `where(id: ...)`'a verilmesi; `.all.pluck` kritik olarak işaretlenir |
95
+ | 8 | **DelegationNPlusOne** | Loop içinde `delegate :method, to: :association` çağrıları, hedef preload edilmemişse |
96
+ | 9 | **DecoratorNPlusOne** | Draper / SimpleDelegator / Presenter / ViewObject erişimi, `.decorate` öncesi preload yoksa |
97
+ | 10 | **ScopeChainNPlusOne** | Loop içinde ilişki üzerine isimli scope'lar (`.recent`, `.active`) — görünmez query tetikleyicileri |
98
+ | 11 | **ValidationNPlusOne** | `validates :x, uniqueness: true` olan modellerde loop içinde `Model.create`/`save` |
99
+
100
+ EagerEye preload'ları sayfalama wrapper'ları (`pagy`, `paginate`, `kaminari`), per-method scope, çok satırlı builder zincirleri ve helper-method parametreleri arasında da takip eder — yani önceden ayarladığın eager-loading'lere saygı gösterir.
101
+
102
+ <details>
103
+ <summary><b>Her detector için detaylı örnekler →</b></summary>
104
+
105
+ ### 1. LoopAssociation
106
+
107
+ ```ruby
108
+ # Kötü
109
+ posts.each { |post| post.author.name } # her post için bir query
110
+
111
+ # İyi — zincirli
112
+ posts.includes(:author).each { |post| post.author.name }
113
+
114
+ # İyi — ayrı satır (preload atama üzerinden takip ediliyor)
115
+ @posts = Post.includes(:author)
116
+ @posts.each { |post| post.author.name }
117
+
118
+ # İyi — tek kayıt (N+1 mümkün değil)
119
+ @user = User.find(params[:id])
120
+ @user.posts.each { |post| post.comments }
121
+ ```
122
+
123
+ `.includes`, `.preload`, `.eager_load`, scope'lu `has_many` (`-> { includes(:author) }`) ve `@pagy, items = pagy(...)` gibi sayfalama wrapper'larını tanır.
124
+
125
+ ### 2. SerializerNesting
126
+
127
+ ```ruby
128
+ # Kötü
129
+ class PostSerializer < Blueprinter::Base
130
+ field :author_name { |post| post.author.name } # her serialize edilen post için query
131
+ end
132
+
133
+ # İyi — controller'da preload
134
+ @posts = Post.includes(:author)
135
+ render json: PostSerializer.render(@posts)
136
+ ```
137
+
138
+ Blueprinter, ActiveModel::Serializers ve Alba'yı destekler.
139
+
140
+ ### 3. MissingCounterCache
141
+
142
+ ```ruby
143
+ # Kötü — her post için COUNT sorgusu
144
+ posts.each { |post| post.comments.count }
145
+
146
+ # İyi — counter cache (Comment: belongs_to :post, counter_cache: true)
147
+ posts.each { |post| post.comments_count } # kolon okuma, query yok
148
+ ```
149
+
150
+ Sadece iterasyon içinde flag'lenir — tek seferlik çağrılar N+1 oluşturmaz.
151
+
152
+ ### 4. CustomMethodQuery
153
+
154
+ ```ruby
155
+ # Kötü — loop içinde where
156
+ @users.each { |user| user.teams.where(name: "Lakers").exists? }
157
+
158
+ # İyi — preload + Ruby'de filtreleme
159
+ @users.includes(:teams).each { |user| user.teams.any? { |t| t.name == "Lakers" } }
160
+ ```
161
+
162
+ Tespit edilen: `where`, `find_by`, `exists?`, `find`, `first`, `last`, `take`, `pluck`, `count`, `sum`, `average`, `minimum`, `maximum`. Per-model scope'lu — başka bir model'de `def foo` query metodu var diye `obj.foo`'yu flag'lemez.
163
+
164
+ ### 5. CountInIteration
165
+
166
+ ```ruby
167
+ # Kötü — .count includes olsa bile her zaman query atar
168
+ @users = User.includes(:posts)
169
+ @users.each { |user| user.posts.count } # her user için SELECT COUNT(*)
170
+
171
+ # İyi — .size preload'u kullanır
172
+ @users.each { |user| user.posts.size }
173
+ ```
174
+
175
+ | Metod | Yüklenmiş | Yüklenmemiş |
176
+ |---|---|---|
177
+ | `.count` | COUNT sorgusu | COUNT sorgusu |
178
+ | `.size` | array#size | COUNT sorgusu |
179
+ | `.length` | array#length | hepsini yükler sonra sayar |
180
+
181
+ ### 6. CallbackQuery
182
+
183
+ ```ruby
184
+ # Kötü — callback içinde N+1
185
+ class Order < ApplicationRecord
186
+ after_create :notify_subscribers
187
+
188
+ def notify_subscribers
189
+ customer.followers.each { |f| f.notifications.create!(...) } # N insert + N query
190
+ end
191
+ end
192
+
193
+ # İyi — background job'a devret
194
+ after_commit :schedule_notifications, on: :create
195
+ def schedule_notifications
196
+ NotifySubscribersJob.perform_later(id)
197
+ end
198
+ ```
199
+
200
+ ### 7. PluckToArray
201
+
202
+ ```ruby
203
+ # Uyarı — iki sorgu + bellek maliyeti
204
+ user_ids = User.active.pluck(:id)
205
+ Post.where(user_id: user_ids)
206
+
207
+ # Hata — tüm tabloyu yükler
208
+ user_ids = User.all.pluck(:id)
209
+ Post.where(user_id: user_ids)
210
+
211
+ # İyi — tek subquery
212
+ Post.where(user_id: User.active.select(:id))
213
+ ```
214
+
215
+ `.where(...).all.pluck(:id)` doğru şekilde scope'lu olarak tanınır, table scan olarak değil.
216
+
217
+ ### 8. DelegationNPlusOne
218
+
219
+ ```ruby
220
+ class Order < ApplicationRecord
221
+ belongs_to :user
222
+ delegate :full_name, :email, to: :user
223
+ end
224
+
225
+ # Kötü — attribute erişimi gibi görünür ama her order için user yükler
226
+ orders.each { |o| o.full_name }
227
+
228
+ # İyi
229
+ orders.includes(:user).each { |o| o.full_name }
230
+ ```
231
+
232
+ Cross-file: model dosyalarını `delegate ... to: :assoc` deklarasyonları için tarar.
233
+
234
+ ### 9. DecoratorNPlusOne
235
+
236
+ ```ruby
237
+ class PostDecorator < Draper::Decorator
238
+ def comment_summary
239
+ object.comments.map(&:body).join(", ") # her decorate edilen post için query
240
+ end
241
+ end
242
+
243
+ # Kötü
244
+ @posts = Post.all.decorate
245
+
246
+ # İyi
247
+ @posts = Post.includes(:comments).all.decorate
248
+ ```
249
+
250
+ Draper / SimpleDelegator / Presenter / ViewObject sınıfları içinde `object`, `__getobj__`, `source`, `model` referanslarını tanır.
251
+
252
+ ### 10. ScopeChainNPlusOne
253
+
254
+ ```ruby
255
+ class Comment < ApplicationRecord
256
+ scope :recent, -> { where("created_at > ?", 1.week.ago) }
257
+ end
258
+
259
+ # Kötü — her iterasyonda scope çağrısı
260
+ posts.each { |post| post.comments.recent }
261
+
262
+ # İyi — preload + filtreleme
263
+ posts.includes(:comments).each { |post| post.comments.select { |c| c.created_at > 1.week.ago } }
264
+ ```
265
+
266
+ Cross-file: model dosyalarını `scope :name, -> { ... }` deklarasyonları için tarar.
267
+
268
+ ### 11. ValidationNPlusOne
269
+
270
+ ```ruby
271
+ class User < ApplicationRecord
272
+ validates :email, uniqueness: true
273
+ end
274
+
275
+ # Kötü — her kayıt için SELECT + INSERT
276
+ params[:users].each { |p| User.create!(p) }
277
+
278
+ # İyi — tek bulk INSERT, DB unique index ile uniqueness'i sağlar
279
+ User.insert_all(params[:users])
280
+ ```
281
+
282
+ </details>
283
+
284
+ ## Inline suppression
285
+
286
+ False positive'leri veya bilinçli desenleri RuboCop tarzı yorumlarla bastır:
287
+
288
+ ```ruby
289
+ # Tek satır
290
+ user.posts.count # eager_eye:disable CountInIteration
291
+
292
+ # Sonraki satır
293
+ # eager_eye:disable-next-line LoopAssociation
294
+ @users.each { |u| u.profile }
295
+
296
+ # Block
297
+ # eager_eye:disable LoopAssociation, SerializerNesting
298
+ @users.each { |u| u.posts.each { |p| p.author } }
299
+ # eager_eye:enable LoopAssociation, SerializerNesting
300
+
301
+ # Tüm dosya (ilk 5 satırda olmalı)
302
+ # eager_eye:disable-file CustomMethodQuery
303
+
304
+ # Sebep ile
305
+ user.posts.count # eager_eye:disable CountInIteration -- counter_cache kullanılıyor
306
+
307
+ # Hepsini kapat
308
+ # eager_eye:disable all
309
+ ```
310
+
311
+ Detector isimleri hem CamelCase (`LoopAssociation`) hem snake_case (`loop_association`) olarak kabul edilir.
312
+
313
+ ## Auto-fix (deneysel)
314
+
315
+ ```bash
316
+ eager_eye --suggest-fixes # diff'i göster
317
+ eager_eye --fix # interaktif uygula
318
+ eager_eye --fix --force # onay sormadan hepsini uygula
319
+ ```
320
+
321
+ | Sorun | Otomatik düzeltme |
322
+ |---|---|
323
+ | `.where(id: ...)` içinde `.pluck(:id)` | → `.select(:id)` |
324
+ | Iterasyon içinde `.count` | → `.size` |
325
+ | Loop öncesi eksik `includes` | → `.includes(:assoc)` ekler |
326
+
327
+ > ⚠ `--fix` sonrası diff'i mutlaka gözden geçir ve testlerini tekrar çalıştır.
328
+
329
+ ## CI entegrasyonu
330
+
331
+ ```yaml
332
+ # .github/workflows/eager_eye.yml
333
+ name: EagerEye
334
+ on: [pull_request]
335
+
336
+ jobs:
337
+ analyze:
338
+ runs-on: ubuntu-latest
339
+ steps:
340
+ - uses: actions/checkout@v4
341
+ - uses: ruby/setup-ruby@v1
342
+ with:
343
+ ruby-version: "3.3"
344
+ - run: gem install eager_eye
345
+ - run: eager_eye app/ --format json > report.json
346
+ - run: |
347
+ issues=$(ruby -rjson -e 'puts JSON.parse(File.read("report.json"))["summary"]["total_issues"]')
348
+ [ "$issues" -gt 0 ] && echo "::warning::$issues olası N+1 sorunu bulundu" || true
349
+ ```
350
+
351
+ PR annotation'lı tam örnek için bkz. [examples/github_action.yml](examples/github_action.yml).
352
+
353
+ ### Baseline modu (brownfield projeler)
354
+
355
+ Çoğu mevcut Rails uygulamasında zaten yüzlerce N+1 sorunu var — her birinde
356
+ CI'yi düşürmek anlamlı değil. Bugünkü raporu baseline olarak yakalayıp CI'nin
357
+ **sadece regresyonlarda** (PR'ın eklediği yeni issue'larda) fail etmesini
358
+ sağlayabilirsiniz:
359
+
360
+ ```bash
361
+ # Tek seferlik: mevcut durumu baseline olarak yakala
362
+ eager_eye app/ --format json > .eager_eye_baseline.json
363
+
364
+ # CI'da: yalnızca YENİ issue'lar sayılır
365
+ eager_eye app/ --baseline .eager_eye_baseline.json
366
+ ```
367
+
368
+ Baseline dosyası standart `--format json` raporudur. Mevcut issue'ları
369
+ düzelttikçe baseline'ı yenileyin. Eşleşme anahtarı: `(detector, file_path,
370
+ line_number, message, severity, suggestion)` — bilinen bir issue'da bu
371
+ alanlardan biri değişirse baseline yenilenene kadar "yeni" olarak görünür.
372
+
373
+ ## RSpec entegrasyonu
374
+
375
+ ```ruby
376
+ # spec/rails_helper.rb
377
+ require "eager_eye/rspec"
378
+
379
+ # spec/eager_eye_spec.rb
380
+ RSpec.describe "EagerEye Analizi" do
381
+ it "controller'larda N+1 yok" do
382
+ expect("app/controllers").to pass_eager_eye
383
+ end
384
+
385
+ it "serializer'lar temiz" do
386
+ expect("app/serializers").to pass_eager_eye(only: [:serializer_nesting])
387
+ end
388
+
389
+ # Migration sırasında bir miktar tolere et
390
+ it "legacy kod kabul edilebilir" do
391
+ expect("app/services/legacy").to pass_eager_eye(max_issues: 10)
392
+ end
393
+ end
394
+ ```
395
+
396
+ Matcher seçenekleri: `only:` (Array<Symbol>), `exclude:` (Array<String> glob'lar), `max_issues:` (Integer, varsayılan 0).
397
+
398
+ ## Yapılandırma
399
+
400
+ ```yaml
401
+ # .eager_eye.yml
402
+ excluded_paths:
403
+ - app/legacy/**
404
+ - lib/tasks/**
405
+
406
+ enabled_detectors: # varsayılan: hepsi
407
+ - loop_association
408
+ - serializer_nesting
409
+ - custom_method_query
410
+ # ...
411
+
412
+ severity_levels:
413
+ loop_association: error
414
+ missing_counter_cache: info
415
+ # ...
416
+
417
+ min_severity: warning # info | warning | error
418
+ app_path: app
419
+ fail_on_issues: true
420
+ ```
421
+
422
+ Veya programatik olarak:
423
+
424
+ ```ruby
425
+ EagerEye.configure do |config|
426
+ config.excluded_paths = ["app/legacy/**"]
427
+ config.enabled_detectors = [:loop_association, :serializer_nesting]
428
+ config.min_severity = :warning
429
+ config.fail_on_issues = true
430
+ end
431
+ ```
432
+
433
+ ## CLI referansı
434
+
435
+ ```text
436
+ Kullanım: eager_eye [yollar] [seçenekler]
437
+
438
+ -f, --format FORMAT console | json (varsayılan: console)
439
+ -e, --exclude PATTERN hariç tutulacak glob (tekrarlanabilir)
440
+ -o, --only DETECTORS virgülle ayrılmış detector listesi
441
+ -s, --min-severity LEVEL info | warning | error
442
+ --no-fail her zaman 0 ile çık
443
+ --no-color düz çıktı
444
+ --baseline FILE önceki bir JSON raporuyla karşılaştır;
445
+ sadece YENİ issue'lar raporlanır (ve sayılır)
446
+ --suggest-fixes fix diff'lerini uygulamadan göster
447
+ --fix interaktif olarak auto-fix uygula
448
+ --fix --force tüm auto-fix'leri uygula
449
+ -v, --version
450
+ -h, --help
451
+ ```
452
+
453
+ ## Limitasyonlar
454
+
455
+ EagerEye statik analiz yapar. Bunun trade-off'ları var:
456
+
457
+ - **Runtime context yok** — `find_each` block'unun runtime'da gerçekten ne yaptığını göremez.
458
+ - **Heuristic ilişki tespiti** — model parse setinde olmadığında yaygın isim desenlerine (`author`, `user`, ...) düşer; küçük edge case'lerde fazla flag'leyebilir.
459
+ - **Cross-file akış** — preload'ları aynı sınıftaki metodlar arasında takip eder (controller → kendi private helper'ları), ama cross-file akış (controller → harici service object → iterasyon) henüz takip edilmiyor.
460
+ - **Sadece Ruby kodu** — SQL veya DB şemanı okumaz.
461
+
462
+ Tam kapsama için [Bullet](https://github.com/flyerhzm/bullet) ile birlikte kullan: statik (EagerEye) test'lerin girmediği yolları, runtime (Bullet) statik analizin göremediklerini yakalar.
463
+
464
+ ## Geliştirme
465
+
466
+ ```bash
467
+ bin/setup
468
+ bundle exec rspec
469
+ bundle exec rubocop
470
+ bin/console
471
+ ```
472
+
473
+ ## Katkı
474
+
475
+ Bug raporları ve PR'lar için: <https://github.com/hamzagedikkaya/eager_eye>.
476
+
477
+ 1. Fork'la
478
+ 2. `git checkout -b feature/yeni-ozellik`
479
+ 3. Spec ekle (bu repo ~%95 coverage'da)
480
+ 4. `git commit -am 'yeni özellik ekle'`
481
+ 5. Pull Request aç
482
+
483
+ ## Lisans
484
+
485
+ MIT — bkz. [LICENSE.txt](LICENSE.txt).
486
+
487
+ ## Davranış Kuralları
488
+
489
+ EagerEye'ın codebase'inde, issue tracker'larında ve tartışmalarında etkileşime giren herkesin [davranış kurallarına](CODE_OF_CONDUCT.md) uyması beklenir.
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "set"
5
+
6
+ module EagerEye
7
+ # Loads a previous JSON report and filters out issues already present
8
+ # in it. Used by `--baseline FILE` to surface only NEW issues introduced
9
+ # since the baseline was captured — the typical brownfield-CI workflow:
10
+ # accept existing issues, fail only on regressions.
11
+ class Baseline
12
+ class InvalidBaselineError < StandardError; end
13
+
14
+ def self.load_issues(path)
15
+ raw = File.read(path)
16
+ data = JSON.parse(raw)
17
+ issues_data = extract_issues_array(data)
18
+ issues_data.map { |h| Issue.from_h(h) }
19
+ rescue Errno::ENOENT
20
+ raise InvalidBaselineError, "Baseline file not found: #{path}"
21
+ rescue JSON::ParserError => e
22
+ raise InvalidBaselineError, "Invalid JSON in baseline #{path}: #{e.message}"
23
+ rescue KeyError => e
24
+ raise InvalidBaselineError, "Baseline issue missing field #{e.message}"
25
+ end
26
+
27
+ def self.filter(current_issues, baseline_path)
28
+ baseline_set = Set.new(load_issues(baseline_path))
29
+ current_issues.reject { |issue| baseline_set.include?(issue) }
30
+ end
31
+
32
+ def self.extract_issues_array(data)
33
+ case data
34
+ when Array then data
35
+ when Hash
36
+ issues = data["issues"] || data[:issues]
37
+ raise InvalidBaselineError, "Baseline JSON missing 'issues' array" unless issues.is_a?(Array)
38
+
39
+ issues
40
+ else
41
+ raise InvalidBaselineError, "Baseline JSON must be an object with 'issues' or a plain array"
42
+ end
43
+ end
44
+ private_class_method :extract_issues_array
45
+ end
46
+ end
data/lib/eager_eye/cli.rb CHANGED
@@ -16,6 +16,7 @@ module EagerEye
16
16
  return 0 if options[:help] || options[:version]
17
17
 
18
18
  issues = analyze
19
+ issues = apply_baseline(issues) if options[:baseline]
19
20
 
20
21
  if options[:suggest_fixes]
21
22
  fixer = AutoFixer.new(issues)
@@ -48,7 +49,8 @@ module EagerEye
48
49
  version: false,
49
50
  suggest_fixes: false,
50
51
  fix: false,
51
- force: false
52
+ force: false,
53
+ baseline: nil
52
54
  }
53
55
  end
54
56
 
@@ -104,6 +106,11 @@ module EagerEye
104
106
  opts.on("--no-fail", "Exit with 0 even if issues found") do
105
107
  options[:fail_on_issues] = false
106
108
  end
109
+
110
+ opts.on("--baseline FILE",
111
+ "Compare against a previous JSON report; only show issues NOT in baseline") do |path|
112
+ options[:baseline] = path
113
+ end
107
114
  end
108
115
 
109
116
  def add_info_options(opts)
@@ -165,5 +172,12 @@ module EagerEye
165
172
  def exit_code(issues)
166
173
  options[:fail_on_issues] && issues.any? ? 1 : 0
167
174
  end
175
+
176
+ def apply_baseline(issues)
177
+ Baseline.filter(issues, options[:baseline])
178
+ rescue Baseline::InvalidBaselineError => e
179
+ warn "Error: #{e.message}"
180
+ exit 1
181
+ end
168
182
  end
169
183
  end
@@ -16,6 +16,18 @@ module EagerEye
16
16
  @suggestion = suggestion
17
17
  end
18
18
 
19
+ def self.from_h(hash)
20
+ h = hash.transform_keys(&:to_sym)
21
+ new(
22
+ detector: h.fetch(:detector).to_sym,
23
+ file_path: h.fetch(:file_path),
24
+ line_number: h.fetch(:line_number),
25
+ message: h.fetch(:message),
26
+ severity: (h[:severity] || :warning).to_sym,
27
+ suggestion: h[:suggestion]
28
+ )
29
+ end
30
+
19
31
  def severity_level
20
32
  SEVERITY_ORDER[severity]
21
33
  end
@@ -26,12 +38,12 @@ module EagerEye
26
38
 
27
39
  def to_h
28
40
  {
29
- detector: detector,
30
- file_path: file_path,
31
- line_number: line_number,
32
- message: message,
33
- severity: severity,
34
- suggestion: suggestion
41
+ detector:,
42
+ file_path:,
43
+ line_number:,
44
+ message:,
45
+ severity:,
46
+ suggestion:
35
47
  }
36
48
  end
37
49
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.2.15"
4
+ VERSION = "1.3.0"
5
5
  end
data/lib/eager_eye.rb CHANGED
@@ -4,6 +4,7 @@ require "set"
4
4
  require_relative "eager_eye/version"
5
5
  require_relative "eager_eye/configuration"
6
6
  require_relative "eager_eye/issue"
7
+ require_relative "eager_eye/baseline"
7
8
  require_relative "eager_eye/association_parser"
8
9
  require_relative "eager_eye/delegation_parser"
9
10
  require_relative "eager_eye/scope_parser"
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.15
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-05-01 00:00:00.000000000 Z
11
+ date: 2026-05-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast
@@ -54,6 +54,7 @@ files:
54
54
  - CONTRIBUTING.md
55
55
  - LICENSE.txt
56
56
  - README.md
57
+ - README.tr.md
57
58
  - Rakefile
58
59
  - SECURITY.md
59
60
  - exe/eager_eye
@@ -61,6 +62,7 @@ files:
61
62
  - lib/eager_eye/analyzer.rb
62
63
  - lib/eager_eye/association_parser.rb
63
64
  - lib/eager_eye/auto_fixer.rb
65
+ - lib/eager_eye/baseline.rb
64
66
  - lib/eager_eye/cli.rb
65
67
  - lib/eager_eye/comment_parser.rb
66
68
  - lib/eager_eye/configuration.rb