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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +227 -541
- data/README.tr.md +489 -0
- data/lib/eager_eye/baseline.rb +46 -0
- data/lib/eager_eye/cli.rb +15 -1
- data/lib/eager_eye/issue.rb +18 -6
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +1 -0
- metadata +4 -2
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
|
data/lib/eager_eye/issue.rb
CHANGED
|
@@ -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
|
|
30
|
-
file_path
|
|
31
|
-
line_number
|
|
32
|
-
message
|
|
33
|
-
severity
|
|
34
|
-
suggestion:
|
|
41
|
+
detector:,
|
|
42
|
+
file_path:,
|
|
43
|
+
line_number:,
|
|
44
|
+
message:,
|
|
45
|
+
severity:,
|
|
46
|
+
suggestion:
|
|
35
47
|
}
|
|
36
48
|
end
|
|
37
49
|
|
data/lib/eager_eye/version.rb
CHANGED
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.
|
|
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-
|
|
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
|