eager_eye 1.0.7 → 1.0.8

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: f19a4d72ee31d2ea100782d4fd9da6594db5f9ec78e9290723461441f58e264d
4
- data.tar.gz: d8b347ecb77010db7ebff3db74610de1516c653991d7264321f302d2b3eaf0ea
3
+ metadata.gz: 0b2e83de186394077d36507bd9c14dbb356777bddff49689c602b43dc8d74b80
4
+ data.tar.gz: 5ee79306b8bd9f602e9f09ed1c62bf5adb5920d57104c486d8799a757458eb6f
5
5
  SHA512:
6
- metadata.gz: 6e676774f31b89d5a6948dda61c18e6c08a522f83c224d796a16dc954f3ae2fd6ef63e91bb1d4e9afe652ab4447538af5e521875bc2544833cabf429549582ff
7
- data.tar.gz: a6bde55b11a3cb95f2550ddd5592b855e2ed6accd36abf3e1fb33f00ba34343291e0eab1a17253de95990c876a9119d15ef1b9e2548cc706e7dea0c6c446a510
6
+ metadata.gz: 36d9dabec10f32dcf21c88efdd5125e5f7146cf61e0e500adc4f3c5b57608303cf0002d7ac48c32eb87b8f163fa6c3125d53bce4e052e9c1dd178f244b69e2a3
7
+ data.tar.gz: 6bc77a99dd29179c9179010ff1c6bb195048115b3b72e545ec51d1af1d286a430a35b223a138fc78e66dbb0172d9e2ff2f39c9f9639be9edc97a47eb928e3df6
data/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.8] - 2025-12-25
11
+
12
+ ### Added
13
+
14
+ - **Severity Levels** - error/warning/info with `--min-severity` filtering
15
+ - Defaults: `loop_association`=error, `missing_counter_cache`=info, others=warning
16
+ - Configurable via `.eager_eye.yml` (`severity_levels`, `min_severity`)
17
+
10
18
  ## [1.0.7] - 2025-12-24
11
19
 
12
20
  ### Fixed
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.7-red.svg" alt="Gem Version"></a>
13
+ <a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.0.8-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>
@@ -457,6 +457,19 @@ enabled_detectors:
457
457
  - callback_query
458
458
  - pluck_to_array
459
459
 
460
+ # Severity levels per detector (error, warning, info)
461
+ severity_levels:
462
+ loop_association: error # Definite N+1
463
+ serializer_nesting: warning
464
+ custom_method_query: warning
465
+ count_in_iteration: warning
466
+ callback_query: warning
467
+ pluck_to_array: warning # Optimization
468
+ missing_counter_cache: info # Suggestion
469
+
470
+ # Minimum severity to report (default: info)
471
+ min_severity: warning
472
+
460
473
  # Base path to analyze (default: app)
461
474
  app_path: app
462
475
 
@@ -470,6 +483,8 @@ fail_on_issues: true
470
483
  EagerEye.configure do |config|
471
484
  config.excluded_paths = ["app/legacy/**"]
472
485
  config.enabled_detectors = [:loop_association, :serializer_nesting]
486
+ config.severity_levels = { loop_association: :error, missing_counter_cache: :info }
487
+ config.min_severity = :warning
473
488
  config.app_path = "app"
474
489
  config.fail_on_issues = true
475
490
  end
@@ -512,6 +527,7 @@ Options:
512
527
  -f, --format FORMAT Output format: console, json (default: console)
513
528
  -e, --exclude PATTERN Exclude files matching pattern (can be used multiple times)
514
529
  -o, --only DETECTORS Run only specified detectors (comma-separated)
530
+ -s, --min-severity LEVEL Minimum severity to report (info, warning, error)
515
531
  --no-fail Exit with 0 even when issues are found
516
532
  --no-color Disable colored output
517
533
  -v, --version Show version
@@ -68,6 +68,10 @@ module EagerEye
68
68
  comment_parser.disabled_at?(issue.line_number, issue.detector)
69
69
  end
70
70
 
71
+ # Filter by minimum severity
72
+ min_severity = EagerEye.configuration.min_severity
73
+ file_issues.select! { |issue| issue.meets_minimum_severity?(min_severity) }
74
+
71
75
  @issues.concat(file_issues)
72
76
  end
73
77
  rescue Errno::ENOENT, Errno::EACCES => e
data/lib/eager_eye/cli.rb CHANGED
@@ -41,6 +41,7 @@ module EagerEye
41
41
  format: :console,
42
42
  exclude: [],
43
43
  only: [],
44
+ min_severity: nil,
44
45
  fail_on_issues: true,
45
46
  colorize: $stdout.tty?,
46
47
  help: false,
@@ -92,6 +93,11 @@ module EagerEye
92
93
  opts.on("-o", "--only DETECTORS", "Run only specified detectors (comma-separated)") do |detectors|
93
94
  options[:only] = detectors.split(",").map(&:strip).map(&:to_sym)
94
95
  end
96
+
97
+ opts.on("-s", "--min-severity LEVEL", %i[info warning error],
98
+ "Minimum severity to report (info, warning, error)") do |level|
99
+ options[:min_severity] = level
100
+ end
95
101
  end
96
102
 
97
103
  def add_behavior_options(opts)
@@ -140,6 +146,7 @@ module EagerEye
140
146
  EagerEye.configure do |config|
141
147
  config.excluded_paths += options[:exclude]
142
148
  config.enabled_detectors = options[:only] unless options[:only].empty?
149
+ config.min_severity = options[:min_severity] if options[:min_severity]
143
150
  end
144
151
  end
145
152
 
@@ -2,7 +2,8 @@
2
2
 
3
3
  module EagerEye
4
4
  class Configuration
5
- attr_accessor :excluded_paths, :enabled_detectors, :app_path, :fail_on_issues
5
+ attr_accessor :excluded_paths, :enabled_detectors, :app_path, :fail_on_issues,
6
+ :severity_levels, :min_severity
6
7
 
7
8
  DEFAULT_DETECTORS = %i[
8
9
  loop_association serializer_nesting missing_counter_cache
@@ -10,11 +11,33 @@ module EagerEye
10
11
  pluck_to_array
11
12
  ].freeze
12
13
 
14
+ DEFAULT_SEVERITY_LEVELS = {
15
+ loop_association: :error,
16
+ serializer_nesting: :warning,
17
+ missing_counter_cache: :info,
18
+ custom_method_query: :warning,
19
+ count_in_iteration: :warning,
20
+ callback_query: :warning,
21
+ pluck_to_array: :warning
22
+ }.freeze
23
+
24
+ VALID_SEVERITIES = %i[info warning error].freeze
25
+
13
26
  def initialize
14
27
  @excluded_paths = []
15
28
  @enabled_detectors = DEFAULT_DETECTORS.dup
16
29
  @app_path = "app"
17
30
  @fail_on_issues = true
31
+ @severity_levels = DEFAULT_SEVERITY_LEVELS.dup
32
+ @min_severity = :info
33
+ end
34
+
35
+ def severity_for(detector_name)
36
+ severity_levels.fetch(detector_name.to_sym, :warning)
37
+ end
38
+
39
+ def valid_severity?(severity)
40
+ VALID_SEVERITIES.include?(severity.to_sym)
18
41
  end
19
42
  end
20
43
  end
@@ -9,6 +9,10 @@ module EagerEye
9
9
  def detector_name
10
10
  raise NotImplementedError, "Subclasses must implement .detector_name"
11
11
  end
12
+
13
+ def default_severity
14
+ :warning
15
+ end
12
16
  end
13
17
 
14
18
  def detect(_ast, _file_path)
@@ -17,17 +21,22 @@ module EagerEye
17
21
 
18
22
  protected
19
23
 
20
- def create_issue(file_path:, line_number:, message:, severity: :warning, suggestion: nil)
24
+ def create_issue(file_path:, line_number:, message:, severity: nil, suggestion: nil)
25
+ resolved_severity = severity || configured_severity
21
26
  Issue.new(
22
27
  detector: self.class.detector_name,
23
28
  file_path: file_path,
24
29
  line_number: line_number,
25
30
  message: message,
26
- severity: severity,
31
+ severity: resolved_severity,
27
32
  suggestion: suggestion
28
33
  )
29
34
  end
30
35
 
36
+ def configured_severity
37
+ EagerEye.configuration.severity_for(self.class.detector_name)
38
+ end
39
+
31
40
  def traverse_ast(node, &block)
32
41
  return unless node.is_a?(Parser::AST::Node)
33
42
 
@@ -128,7 +128,6 @@ module EagerEye
128
128
  file_path: @file_path,
129
129
  line_number: node.loc.line,
130
130
  message: "`.count` called on `#{receiver_chain}` inside iteration always executes a COUNT query",
131
- severity: :warning,
132
131
  suggestion: "Use `.size` instead (uses loaded collection) or add `counter_cache: true`"
133
132
  )
134
133
  end
@@ -165,7 +165,6 @@ module EagerEye
165
165
  file_path: @file_path,
166
166
  line_number: node.loc.line,
167
167
  message: "Query method `.#{method_name}` called on `#{association_chain}` inside iteration",
168
- severity: :warning,
169
168
  suggestion: "This query executes on each iteration. Consider preloading data or restructuring the query."
170
169
  )
171
170
  end
@@ -140,7 +140,6 @@ module EagerEye
140
140
  file_path: @file_path,
141
141
  line_number: node.loc.line,
142
142
  message: "Using plucked/mapped array in `where` causes two queries and holds IDs in memory",
143
- severity: :warning,
144
143
  suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.condition.select(:id))`"
145
144
  )
146
145
  end
@@ -4,7 +4,8 @@ module EagerEye
4
4
  class Issue
5
5
  attr_reader :detector, :file_path, :line_number, :message, :severity, :suggestion
6
6
 
7
- VALID_SEVERITIES = %i[warning error].freeze
7
+ VALID_SEVERITIES = %i[info warning error].freeze
8
+ SEVERITY_ORDER = { info: 0, warning: 1, error: 2 }.freeze
8
9
 
9
10
  def initialize(detector:, file_path:, line_number:, message:, severity: :warning, suggestion: nil)
10
11
  @detector = detector
@@ -15,6 +16,14 @@ module EagerEye
15
16
  @suggestion = suggestion
16
17
  end
17
18
 
19
+ def severity_level
20
+ SEVERITY_ORDER[severity]
21
+ end
22
+
23
+ def meets_minimum_severity?(min_severity)
24
+ severity_level >= SEVERITY_ORDER.fetch(min_severity, 0)
25
+ end
26
+
18
27
  def to_h
19
28
  {
20
29
  detector: detector,
@@ -19,6 +19,10 @@ module EagerEye
19
19
  issues.group_by(&:file_path)
20
20
  end
21
21
 
22
+ def info_count
23
+ issues.count { |i| i.severity == :info }
24
+ end
25
+
22
26
  def warning_count
23
27
  issues.count { |i| i.severity == :warning }
24
28
  end
@@ -61,7 +61,7 @@ module EagerEye
61
61
 
62
62
  def format_issue(issue)
63
63
  detector_label = format_detector(issue.detector)
64
- severity_color = issue.severity == :error ? :red : :yellow
64
+ severity_color = severity_to_color(issue.severity)
65
65
 
66
66
  line = " Line #{issue.line_number}: "
67
67
  line += colorize("[#{detector_label}]", severity_color)
@@ -72,18 +72,24 @@ module EagerEye
72
72
  line
73
73
  end
74
74
 
75
+ def severity_to_color(severity)
76
+ { error: :red, warning: :yellow, info: :cyan }.fetch(severity, :yellow)
77
+ end
78
+
75
79
  def format_detector(detector)
76
80
  detector.to_s.split("_").map(&:capitalize).join
77
81
  end
78
82
 
79
83
  def summary
80
84
  total = issues.size
81
- warnings = warning_count
82
85
  errors = error_count
86
+ warnings = warning_count
87
+ infos = info_count
83
88
 
84
89
  "Total: #{total} issue#{"s" unless total == 1} " \
85
- "(#{warnings} warning#{"s" unless warnings == 1}, " \
86
- "#{errors} error#{"s" unless errors == 1})"
90
+ "(#{errors} error#{"s" unless errors == 1}, " \
91
+ "#{warnings} warning#{"s" unless warnings == 1}, " \
92
+ "#{infos} info)"
87
93
  end
88
94
 
89
95
  def colorize(text, color)
@@ -24,8 +24,9 @@ module EagerEye
24
24
  def summary_hash
25
25
  {
26
26
  total: issues.size,
27
- warnings: warning_count,
28
27
  errors: error_count,
28
+ warnings: warning_count,
29
+ infos: info_count,
29
30
  files_affected: issues_by_file.keys.size
30
31
  }
31
32
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.0.7"
4
+ VERSION = "1.0.8"
5
5
  end
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.0.7
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-24 00:00:00.000000000 Z
11
+ date: 2025-12-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ast
@@ -56,9 +56,7 @@ files:
56
56
  - README.md
57
57
  - Rakefile
58
58
  - SECURITY.md
59
- - examples/github_action.yml
60
59
  - exe/eager_eye
61
- - images/icon.png
62
60
  - lib/eager_eye.rb
63
61
  - lib/eager_eye/analyzer.rb
64
62
  - lib/eager_eye/auto_fixer.rb
@@ -1,75 +0,0 @@
1
- # Example GitHub Action workflow for using EagerEye in your Rails project
2
- # Copy this file to .github/workflows/eager_eye.yml in your repository
3
-
4
- name: EagerEye N+1 Analysis
5
-
6
- on:
7
- pull_request:
8
- paths:
9
- - "app/**/*.rb"
10
- - "lib/**/*.rb"
11
-
12
- jobs:
13
- analyze:
14
- runs-on: ubuntu-latest
15
- name: N+1 Query Detection
16
-
17
- steps:
18
- - name: Checkout code
19
- uses: actions/checkout@v4
20
-
21
- - name: Set up Ruby
22
- uses: ruby/setup-ruby@v1
23
- with:
24
- ruby-version: "3.3"
25
-
26
- - name: Install EagerEye
27
- run: gem install eager_eye
28
-
29
- - name: Run EagerEye analysis
30
- run: eager_eye app/ --format json > eager_eye_report.json
31
- continue-on-error: true
32
-
33
- - name: Upload report artifact
34
- uses: actions/upload-artifact@v4
35
- with:
36
- name: eager-eye-report
37
- path: eager_eye_report.json
38
-
39
- - name: Check for N+1 issues
40
- run: |
41
- if [ -f eager_eye_report.json ]; then
42
- issues=$(cat eager_eye_report.json | ruby -rjson -e 'puts JSON.parse(STDIN.read)["summary"]["total_issues"]')
43
- if [ "$issues" -gt 0 ]; then
44
- echo "::warning::EagerEye detected $issues potential N+1 query issue(s)"
45
- cat eager_eye_report.json | ruby -rjson -e '
46
- data = JSON.parse(STDIN.read)
47
- data["issues"].each do |issue|
48
- puts "::warning file=#{issue["file_path"]},line=#{issue["line_number"]}::#{issue["message"]}"
49
- end
50
- '
51
- else
52
- echo "No N+1 query issues detected!"
53
- fi
54
- fi
55
-
56
- # Alternative: Strict mode that fails the build on issues
57
- analyze-strict:
58
- runs-on: ubuntu-latest
59
- name: N+1 Query Detection (Strict)
60
- if: false # Set to true to enable strict mode
61
-
62
- steps:
63
- - name: Checkout code
64
- uses: actions/checkout@v4
65
-
66
- - name: Set up Ruby
67
- uses: ruby/setup-ruby@v1
68
- with:
69
- ruby-version: "3.3"
70
-
71
- - name: Install EagerEye
72
- run: gem install eager_eye
73
-
74
- - name: Run EagerEye analysis (strict)
75
- run: eager_eye app/ --no-color
data/images/icon.png DELETED
Binary file