eager_eye 0.1.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.
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ class Issue
5
+ attr_reader :detector, :file_path, :line_number, :message, :severity, :suggestion
6
+
7
+ VALID_SEVERITIES = %i[warning error].freeze
8
+
9
+ def initialize(detector:, file_path:, line_number:, message:, severity: :warning, suggestion: nil)
10
+ @detector = detector
11
+ @file_path = file_path
12
+ @line_number = line_number
13
+ @message = message
14
+ @severity = validate_severity(severity)
15
+ @suggestion = suggestion
16
+ end
17
+
18
+ def to_h
19
+ {
20
+ detector: detector,
21
+ file_path: file_path,
22
+ line_number: line_number,
23
+ message: message,
24
+ severity: severity,
25
+ suggestion: suggestion
26
+ }
27
+ end
28
+
29
+ def to_json(*args)
30
+ to_h.to_json(*args)
31
+ end
32
+
33
+ def ==(other)
34
+ return false unless other.is_a?(Issue)
35
+
36
+ detector == other.detector &&
37
+ file_path == other.file_path &&
38
+ line_number == other.line_number &&
39
+ message == other.message &&
40
+ severity == other.severity &&
41
+ suggestion == other.suggestion
42
+ end
43
+
44
+ alias eql? ==
45
+
46
+ def hash
47
+ [detector, file_path, line_number, message, severity, suggestion].hash
48
+ end
49
+
50
+ private
51
+
52
+ def validate_severity(severity)
53
+ return severity if VALID_SEVERITIES.include?(severity)
54
+
55
+ raise ArgumentError, "Invalid severity: #{severity}. Must be one of: #{VALID_SEVERITIES.join(", ")}"
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module EagerEye
6
+ class Railtie < Rails::Railtie
7
+ railtie_name :eager_eye
8
+
9
+ rake_tasks do
10
+ namespace :eager_eye do
11
+ desc "Analyze Rails application for N+1 query issues"
12
+ task analyze: :environment do
13
+ require "eager_eye"
14
+
15
+ load_config_file
16
+
17
+ analyzer = EagerEye::Analyzer.new
18
+ issues = analyzer.run
19
+
20
+ reporter = EagerEye::Reporters::Console.new(issues)
21
+ puts reporter.report
22
+
23
+ exit 1 if issues.any? && EagerEye.configuration.fail_on_issues
24
+ end
25
+
26
+ desc "Analyze and output results as JSON"
27
+ task json: :environment do
28
+ require "eager_eye"
29
+
30
+ load_config_file
31
+
32
+ analyzer = EagerEye::Analyzer.new
33
+ issues = analyzer.run
34
+
35
+ reporter = EagerEye::Reporters::Json.new(issues, pretty: true)
36
+ puts reporter.report
37
+
38
+ exit 1 if issues.any? && EagerEye.configuration.fail_on_issues
39
+ end
40
+
41
+ def load_config_file
42
+ config_file = Rails.root.join(".eager_eye.yml")
43
+ return unless File.exist?(config_file)
44
+
45
+ require "yaml"
46
+ config = YAML.load_file(config_file, symbolize_names: true)
47
+
48
+ EagerEye.configure do |c|
49
+ c.excluded_paths = config[:excluded_paths] if config[:excluded_paths]
50
+ c.enabled_detectors = config[:enabled_detectors].map(&:to_sym) if config[:enabled_detectors]
51
+ c.app_path = config[:app_path] if config[:app_path]
52
+ c.fail_on_issues = config[:fail_on_issues] if config.key?(:fail_on_issues)
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ # Generate initializer for configuration
59
+ generators do
60
+ require_relative "generators/install_generator"
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ module Reporters
5
+ class Base
6
+ attr_reader :issues
7
+
8
+ def initialize(issues)
9
+ @issues = issues
10
+ end
11
+
12
+ def report
13
+ raise NotImplementedError, "Subclasses must implement #report"
14
+ end
15
+
16
+ protected
17
+
18
+ def issues_by_file
19
+ issues.group_by(&:file_path)
20
+ end
21
+
22
+ def warning_count
23
+ issues.count { |i| i.severity == :warning }
24
+ end
25
+
26
+ def error_count
27
+ issues.count { |i| i.severity == :error }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ module Reporters
5
+ class Console < Base
6
+ COLORS = {
7
+ red: "\e[31m",
8
+ yellow: "\e[33m",
9
+ green: "\e[32m",
10
+ cyan: "\e[36m",
11
+ reset: "\e[0m",
12
+ bold: "\e[1m"
13
+ }.freeze
14
+
15
+ def initialize(issues, colorize: true)
16
+ super(issues)
17
+ @colorize = colorize
18
+ end
19
+
20
+ def report
21
+ return no_issues_message if issues.empty?
22
+
23
+ output = []
24
+ output << header
25
+ output << ""
26
+
27
+ issues_by_file.each do |file_path, file_issues|
28
+ output << file_section(file_path, file_issues)
29
+ end
30
+
31
+ output << separator
32
+ output << summary
33
+ output.join("\n")
34
+ end
35
+
36
+ private
37
+
38
+ def no_issues_message
39
+ colorize("No issues detected!", :green)
40
+ end
41
+
42
+ def header
43
+ "#{colorize("EagerEye Analysis Results", :bold)}\n#{"=" * 25}"
44
+ end
45
+
46
+ def separator
47
+ "-" * 40
48
+ end
49
+
50
+ def file_section(file_path, file_issues)
51
+ lines = []
52
+ lines << colorize(file_path, :cyan)
53
+
54
+ file_issues.each do |issue|
55
+ lines << format_issue(issue)
56
+ end
57
+
58
+ lines << ""
59
+ lines.join("\n")
60
+ end
61
+
62
+ def format_issue(issue)
63
+ detector_label = format_detector(issue.detector)
64
+ severity_color = issue.severity == :error ? :red : :yellow
65
+
66
+ line = " Line #{issue.line_number}: "
67
+ line += colorize("[#{detector_label}]", severity_color)
68
+ line += " #{issue.message}"
69
+
70
+ line += "\n #{colorize("Suggestion:", :green)} #{issue.suggestion}" if issue.suggestion
71
+
72
+ line
73
+ end
74
+
75
+ def format_detector(detector)
76
+ detector.to_s.split("_").map(&:capitalize).join
77
+ end
78
+
79
+ def summary
80
+ total = issues.size
81
+ warnings = warning_count
82
+ errors = error_count
83
+
84
+ "Total: #{total} issue#{"s" unless total == 1} " \
85
+ "(#{warnings} warning#{"s" unless warnings == 1}, " \
86
+ "#{errors} error#{"s" unless errors == 1})"
87
+ end
88
+
89
+ def colorize(text, color)
90
+ return text unless @colorize
91
+
92
+ "#{COLORS[color]}#{text}#{COLORS[:reset]}"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module EagerEye
6
+ module Reporters
7
+ class Json < Base
8
+ def initialize(issues, pretty: false)
9
+ super(issues)
10
+ @pretty = pretty
11
+ end
12
+
13
+ def report
14
+ result = {
15
+ summary: summary_hash,
16
+ issues: issues.map(&:to_h)
17
+ }
18
+
19
+ @pretty ? JSON.pretty_generate(result) : JSON.generate(result)
20
+ end
21
+
22
+ private
23
+
24
+ def summary_hash
25
+ {
26
+ total: issues.size,
27
+ warnings: warning_count,
28
+ errors: error_count,
29
+ files_affected: issues_by_file.keys.size
30
+ }
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ VERSION = "0.1.0"
5
+ end
data/lib/eager_eye.rb ADDED
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "eager_eye/version"
4
+ require_relative "eager_eye/configuration"
5
+ require_relative "eager_eye/issue"
6
+ require_relative "eager_eye/detectors/base"
7
+ require_relative "eager_eye/detectors/loop_association"
8
+ require_relative "eager_eye/detectors/serializer_nesting"
9
+ require_relative "eager_eye/detectors/missing_counter_cache"
10
+ require_relative "eager_eye/analyzer"
11
+ require_relative "eager_eye/reporters/base"
12
+ require_relative "eager_eye/reporters/console"
13
+ require_relative "eager_eye/reporters/json"
14
+ require_relative "eager_eye/cli"
15
+
16
+ module EagerEye
17
+ class Error < StandardError; end
18
+
19
+ class << self
20
+ def configuration
21
+ @configuration ||= Configuration.new
22
+ end
23
+
24
+ def configure
25
+ yield(configuration)
26
+ end
27
+
28
+ def reset_configuration!
29
+ @configuration = Configuration.new
30
+ end
31
+ end
32
+ end
33
+
34
+ # Load Railtie only if Rails is defined
35
+ require_relative "eager_eye/railtie" if defined?(Rails::Railtie)
data/sig/eager_eye.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module EagerEye
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eager_eye
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - hamzagedikkaya
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-12-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ast
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.4'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.4'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parser
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.3'
41
+ description: EagerEye detects N+1 query problems using AST analysis without running
42
+ your code.
43
+ email:
44
+ - gedikkayahamza@gmail.com
45
+ executables:
46
+ - eager_eye
47
+ extensions: []
48
+ extra_rdoc_files: []
49
+ files:
50
+ - ".rspec"
51
+ - ".rubocop.yml"
52
+ - CHANGELOG.md
53
+ - CODE_OF_CONDUCT.md
54
+ - LICENSE.txt
55
+ - README.md
56
+ - Rakefile
57
+ - examples/github_action.yml
58
+ - exe/eager_eye
59
+ - lib/eager_eye.rb
60
+ - lib/eager_eye/analyzer.rb
61
+ - lib/eager_eye/cli.rb
62
+ - lib/eager_eye/configuration.rb
63
+ - lib/eager_eye/detectors/base.rb
64
+ - lib/eager_eye/detectors/loop_association.rb
65
+ - lib/eager_eye/detectors/missing_counter_cache.rb
66
+ - lib/eager_eye/detectors/serializer_nesting.rb
67
+ - lib/eager_eye/generators/install_generator.rb
68
+ - lib/eager_eye/issue.rb
69
+ - lib/eager_eye/railtie.rb
70
+ - lib/eager_eye/reporters/base.rb
71
+ - lib/eager_eye/reporters/console.rb
72
+ - lib/eager_eye/reporters/json.rb
73
+ - lib/eager_eye/version.rb
74
+ - sig/eager_eye.rbs
75
+ homepage: https://github.com/hamzagedikkaya/eager_eye
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ allowed_push_host: https://rubygems.org
80
+ homepage_uri: https://github.com/hamzagedikkaya/eager_eye
81
+ source_code_uri: https://github.com/hamzagedikkaya/eager_eye
82
+ changelog_uri: https://github.com/hamzagedikkaya/eager_eye/blob/master/CHANGELOG.md
83
+ rubygems_mfa_required: 'true'
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 3.1.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.5.9
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Static analysis tool for detecting N+1 queries in Rails applications
103
+ test_files: []