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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +35 -0
- data/CHANGELOG.md +45 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +310 -0
- data/Rakefile +12 -0
- data/examples/github_action.yml +75 -0
- data/exe/eager_eye +6 -0
- data/lib/eager_eye/analyzer.rb +78 -0
- data/lib/eager_eye/cli.rb +120 -0
- data/lib/eager_eye/configuration.rb +16 -0
- data/lib/eager_eye/detectors/base.rb +48 -0
- data/lib/eager_eye/detectors/loop_association.rb +124 -0
- data/lib/eager_eye/detectors/missing_counter_cache.rb +73 -0
- data/lib/eager_eye/detectors/serializer_nesting.rb +192 -0
- data/lib/eager_eye/generators/install_generator.rb +41 -0
- data/lib/eager_eye/issue.rb +58 -0
- data/lib/eager_eye/railtie.rb +63 -0
- data/lib/eager_eye/reporters/base.rb +31 -0
- data/lib/eager_eye/reporters/console.rb +96 -0
- data/lib/eager_eye/reporters/json.rb +34 -0
- data/lib/eager_eye/version.rb +5 -0
- data/lib/eager_eye.rb +35 -0
- data/sig/eager_eye.rbs +4 -0
- metadata +103 -0
|
@@ -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
|
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
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: []
|