eager_eye 1.0.10 → 1.1.1
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/.rubocop.yml +3 -3
- data/CHANGELOG.md +20 -10
- data/CONTRIBUTING.md +1 -1
- data/README.md +20 -29
- data/SECURITY.md +1 -0
- data/lib/eager_eye/analyzer.rb +33 -14
- data/lib/eager_eye/association_parser.rb +88 -0
- data/lib/eager_eye/auto_fixer.rb +4 -12
- data/lib/eager_eye/cli.rb +2 -7
- data/lib/eager_eye/comment_parser.rb +0 -5
- data/lib/eager_eye/detectors/base.rb +25 -4
- data/lib/eager_eye/detectors/callback_query.rb +11 -45
- data/lib/eager_eye/detectors/count_in_iteration.rb +16 -54
- data/lib/eager_eye/detectors/custom_method_query.rb +25 -91
- data/lib/eager_eye/detectors/loop_association.rb +77 -89
- data/lib/eager_eye/detectors/missing_counter_cache.rb +16 -47
- data/lib/eager_eye/detectors/pluck_to_array.rb +19 -52
- data/lib/eager_eye/detectors/serializer_nesting.rb +20 -70
- data/lib/eager_eye/fixers/pluck_to_select.rb +2 -9
- data/lib/eager_eye/issue.rb +2 -9
- data/lib/eager_eye/railtie.rb +7 -20
- data/lib/eager_eye/reporters/console.rb +7 -18
- data/lib/eager_eye/rspec/matchers.rb +1 -6
- data/lib/eager_eye/version.rb +1 -1
- data/lib/eager_eye.rb +1 -1
- data/sig/eager_eye.rbs +0 -1
- metadata +3 -2
|
@@ -3,26 +3,10 @@
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
module Detectors
|
|
5
5
|
class SerializerNesting < Base
|
|
6
|
-
|
|
7
|
-
SERIALIZER_PATTERNS = [
|
|
8
|
-
"ActiveModel::Serializer",
|
|
9
|
-
"ActiveModelSerializers::Model",
|
|
10
|
-
"Blueprinter::Base",
|
|
11
|
-
"Alba::Resource"
|
|
12
|
-
].freeze
|
|
13
|
-
|
|
14
|
-
# Method names that define attributes in serializers
|
|
6
|
+
SERIALIZER_PATTERNS = %w[ActiveModel::Serializer ActiveModelSerializers::Model Blueprinter::Base Alba::Resource].freeze
|
|
15
7
|
ATTRIBUTE_METHODS = %i[attribute field attributes].freeze
|
|
16
|
-
|
|
17
|
-
# Object reference names in serializers
|
|
18
8
|
OBJECT_REFS = %i[object record resource].freeze
|
|
19
|
-
|
|
20
|
-
# Common association names (same as LoopAssociation)
|
|
21
|
-
ASSOCIATION_NAMES = %w[
|
|
22
|
-
author user owner creator admin member customer client
|
|
23
|
-
post article comment category tag parent company organization
|
|
24
|
-
project task item order product account profile setting
|
|
25
|
-
image avatar photo attachment document
|
|
9
|
+
HAS_MANY_ASSOCIATIONS = %w[
|
|
26
10
|
authors users owners creators admins members customers clients
|
|
27
11
|
posts articles comments categories tags children companies organizations
|
|
28
12
|
projects tasks items orders products accounts profiles settings
|
|
@@ -52,21 +36,16 @@ module EagerEye
|
|
|
52
36
|
def serializer_class?(node)
|
|
53
37
|
return false unless node.type == :class
|
|
54
38
|
|
|
55
|
-
# Check class name ends with Serializer, Blueprint, or Resource
|
|
56
39
|
class_name = extract_class_name(node)
|
|
57
40
|
return false unless class_name
|
|
58
41
|
|
|
59
42
|
class_name.end_with?("Serializer", "Blueprint", "Resource") ||
|
|
60
|
-
inherits_from_serializer?(node) ||
|
|
61
|
-
includes_serializer_module?(node)
|
|
43
|
+
inherits_from_serializer?(node) || includes_serializer_module?(node)
|
|
62
44
|
end
|
|
63
45
|
|
|
64
46
|
def extract_class_name(class_node)
|
|
65
47
|
name_node = class_node.children[0]
|
|
66
|
-
|
|
67
|
-
return nil unless name_node.type == :const
|
|
68
|
-
|
|
69
|
-
name_node.children[1].to_s
|
|
48
|
+
name_node.children[1].to_s if name_node&.type == :const
|
|
70
49
|
end
|
|
71
50
|
|
|
72
51
|
def inherits_from_serializer?(class_node)
|
|
@@ -74,7 +53,7 @@ module EagerEye
|
|
|
74
53
|
return false unless parent_node
|
|
75
54
|
|
|
76
55
|
parent_name = const_to_string(parent_node)
|
|
77
|
-
SERIALIZER_PATTERNS.any? { |
|
|
56
|
+
SERIALIZER_PATTERNS.any? { |p| parent_name&.include?(p.split("::").last) }
|
|
78
57
|
end
|
|
79
58
|
|
|
80
59
|
def includes_serializer_module?(class_node)
|
|
@@ -82,20 +61,14 @@ module EagerEye
|
|
|
82
61
|
return false unless body
|
|
83
62
|
|
|
84
63
|
traverse_ast(body) do |node|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
method = node.children[1]
|
|
88
|
-
return true if method == :include && alba_resource?(node)
|
|
64
|
+
return true if node.type == :send && node.children[1] == :include && alba_resource?(node)
|
|
89
65
|
end
|
|
90
|
-
|
|
91
66
|
false
|
|
92
67
|
end
|
|
93
68
|
|
|
94
69
|
def alba_resource?(include_node)
|
|
95
70
|
arg = include_node.children[2]
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const_to_string(arg)&.include?("Alba")
|
|
71
|
+
arg && const_to_string(arg)&.include?("Alba")
|
|
99
72
|
end
|
|
100
73
|
|
|
101
74
|
def const_to_string(node)
|
|
@@ -103,12 +76,10 @@ module EagerEye
|
|
|
103
76
|
|
|
104
77
|
parts = []
|
|
105
78
|
current = node
|
|
106
|
-
|
|
107
79
|
while current&.type == :const
|
|
108
80
|
parts.unshift(current.children[1].to_s)
|
|
109
81
|
current = current.children[0]
|
|
110
82
|
end
|
|
111
|
-
|
|
112
83
|
parts.join("::")
|
|
113
84
|
end
|
|
114
85
|
|
|
@@ -117,34 +88,24 @@ module EagerEye
|
|
|
117
88
|
return unless body
|
|
118
89
|
|
|
119
90
|
traverse_ast(body) do |node|
|
|
120
|
-
next unless attribute_block?(node)
|
|
121
|
-
|
|
122
|
-
block_body = node.children[2]
|
|
123
|
-
next unless block_body
|
|
91
|
+
next unless attribute_block?(node) && node.children[2]
|
|
124
92
|
|
|
125
|
-
find_association_in_block(
|
|
93
|
+
find_association_in_block(node.children[2], file_path, issues)
|
|
126
94
|
end
|
|
127
95
|
end
|
|
128
96
|
|
|
129
97
|
def attribute_block?(node)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
send_node = node.children[0]
|
|
133
|
-
return false unless send_node&.type == :send
|
|
134
|
-
|
|
135
|
-
method_name = send_node.children[1]
|
|
136
|
-
ATTRIBUTE_METHODS.include?(method_name)
|
|
98
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
99
|
+
ATTRIBUTE_METHODS.include?(node.children[0].children[1])
|
|
137
100
|
end
|
|
138
101
|
|
|
139
|
-
def find_association_in_block(block_body,
|
|
102
|
+
def find_association_in_block(block_body, file_path, issues)
|
|
140
103
|
traverse_ast(block_body) do |node|
|
|
141
104
|
next unless node.type == :send
|
|
142
105
|
|
|
143
106
|
receiver = node.children[0]
|
|
144
107
|
method_name = node.children[1]
|
|
145
|
-
|
|
146
|
-
next unless object_reference?(receiver)
|
|
147
|
-
next unless likely_association?(method_name)
|
|
108
|
+
next unless object_reference?(receiver) && likely_association?(method_name)
|
|
148
109
|
|
|
149
110
|
issues << create_issue(
|
|
150
111
|
file_path: file_path,
|
|
@@ -159,33 +120,22 @@ module EagerEye
|
|
|
159
120
|
return false unless node
|
|
160
121
|
|
|
161
122
|
case node.type
|
|
162
|
-
when :send
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
method = node.children[1]
|
|
166
|
-
|
|
167
|
-
receiver.nil? && OBJECT_REFS.include?(method)
|
|
168
|
-
when :lvar
|
|
169
|
-
# Block variable like |post|
|
|
170
|
-
true
|
|
171
|
-
else
|
|
172
|
-
false
|
|
123
|
+
when :send then node.children[0].nil? && OBJECT_REFS.include?(node.children[1])
|
|
124
|
+
when :lvar then true
|
|
125
|
+
else false
|
|
173
126
|
end
|
|
174
127
|
end
|
|
175
128
|
|
|
176
129
|
def receiver_name(node)
|
|
177
130
|
case node.type
|
|
178
|
-
when :send
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
node.children[0].to_s
|
|
182
|
-
else
|
|
183
|
-
"object"
|
|
131
|
+
when :send then node.children[1].to_s
|
|
132
|
+
when :lvar then node.children[0].to_s
|
|
133
|
+
else "object"
|
|
184
134
|
end
|
|
185
135
|
end
|
|
186
136
|
|
|
187
137
|
def likely_association?(method_name)
|
|
188
|
-
|
|
138
|
+
HAS_MANY_ASSOCIATIONS.include?(method_name.to_s)
|
|
189
139
|
end
|
|
190
140
|
end
|
|
191
141
|
end
|
|
@@ -3,27 +3,20 @@
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
module Fixers
|
|
5
5
|
class PluckToSelect < Base
|
|
6
|
-
# This fixer only works for single-line pluck + where patterns
|
|
7
|
-
# Two-line patterns are too complex to fix automatically
|
|
8
|
-
|
|
9
6
|
def fixable?
|
|
10
|
-
issue.detector == :pluck_to_array &&
|
|
11
|
-
single_line_pattern?
|
|
7
|
+
issue.detector == :pluck_to_array && single_line_pattern?
|
|
12
8
|
end
|
|
13
9
|
|
|
14
10
|
protected
|
|
15
11
|
|
|
16
12
|
def fixed_content
|
|
17
|
-
# Model.where(col: OtherModel.pluck(:id)) -> Model.where(col: OtherModel.select(:id))
|
|
18
13
|
line_content.gsub(/\.pluck\((:\w+)\)/, '.select(\1)')
|
|
19
14
|
end
|
|
20
15
|
|
|
21
16
|
private
|
|
22
17
|
|
|
23
18
|
def single_line_pattern?
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
line_content.include?(".pluck(") && line_content.include?(".where(")
|
|
19
|
+
line_content&.include?(".pluck(") && line_content.include?(".where(")
|
|
27
20
|
end
|
|
28
21
|
end
|
|
29
22
|
end
|
data/lib/eager_eye/issue.rb
CHANGED
|
@@ -40,20 +40,13 @@ module EagerEye
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
def ==(other)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
detector == other.detector &&
|
|
46
|
-
file_path == other.file_path &&
|
|
47
|
-
line_number == other.line_number &&
|
|
48
|
-
message == other.message &&
|
|
49
|
-
severity == other.severity &&
|
|
50
|
-
suggestion == other.suggestion
|
|
43
|
+
other.is_a?(Issue) && to_h == other.to_h
|
|
51
44
|
end
|
|
52
45
|
|
|
53
46
|
alias eql? ==
|
|
54
47
|
|
|
55
48
|
def hash
|
|
56
|
-
|
|
49
|
+
to_h.hash
|
|
57
50
|
end
|
|
58
51
|
|
|
59
52
|
private
|
data/lib/eager_eye/railtie.rb
CHANGED
|
@@ -10,32 +10,20 @@ module EagerEye
|
|
|
10
10
|
namespace :eager_eye do
|
|
11
11
|
desc "Analyze Rails application for N+1 query issues"
|
|
12
12
|
task analyze: :environment do
|
|
13
|
-
|
|
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
|
|
13
|
+
puts run_analysis(EagerEye::Reporters::Console)
|
|
24
14
|
end
|
|
25
15
|
|
|
26
16
|
desc "Analyze and output results as JSON"
|
|
27
17
|
task json: :environment do
|
|
28
|
-
|
|
18
|
+
puts run_analysis(EagerEye::Reporters::Json, pretty: true)
|
|
19
|
+
end
|
|
29
20
|
|
|
21
|
+
def run_analysis(reporter_class, **opts)
|
|
22
|
+
require "eager_eye"
|
|
30
23
|
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
|
-
|
|
24
|
+
issues = EagerEye::Analyzer.new.run
|
|
38
25
|
exit 1 if issues.any? && EagerEye.configuration.fail_on_issues
|
|
26
|
+
reporter_class.new(issues, **opts).report
|
|
39
27
|
end
|
|
40
28
|
|
|
41
29
|
def load_config_file
|
|
@@ -55,7 +43,6 @@ module EagerEye
|
|
|
55
43
|
end
|
|
56
44
|
end
|
|
57
45
|
|
|
58
|
-
# Generate initializer for configuration
|
|
59
46
|
generators do
|
|
60
47
|
require_relative "generators/install_generator"
|
|
61
48
|
end
|
|
@@ -48,15 +48,7 @@ module EagerEye
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def file_section(file_path, file_issues)
|
|
51
|
-
|
|
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")
|
|
51
|
+
[colorize(file_path, :cyan), *file_issues.map { |i| format_issue(i) }, ""].join("\n")
|
|
60
52
|
end
|
|
61
53
|
|
|
62
54
|
def format_issue(issue)
|
|
@@ -81,15 +73,12 @@ module EagerEye
|
|
|
81
73
|
end
|
|
82
74
|
|
|
83
75
|
def summary
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"(#{errors} error#{"s" unless errors == 1}, " \
|
|
91
|
-
"#{warnings} warning#{"s" unless warnings == 1}, " \
|
|
92
|
-
"#{infos} info)"
|
|
76
|
+
t = issues.size
|
|
77
|
+
e = error_count
|
|
78
|
+
w = warning_count
|
|
79
|
+
i = info_count
|
|
80
|
+
"Total: #{t} issue#{"s" unless t == 1} " \
|
|
81
|
+
"(#{e} error#{"s" unless e == 1}, #{w} warning#{"s" unless w == 1}, #{i} info)"
|
|
93
82
|
end
|
|
94
83
|
|
|
95
84
|
def colorize(text, color)
|
|
@@ -19,8 +19,7 @@ module EagerEye
|
|
|
19
19
|
def matches?(path)
|
|
20
20
|
@path = path
|
|
21
21
|
configure_eager_eye
|
|
22
|
-
|
|
23
|
-
@issues = analyzer.run
|
|
22
|
+
@issues = EagerEye::Analyzer.new(paths: [@path]).run
|
|
24
23
|
@issues.count <= @max_issues
|
|
25
24
|
end
|
|
26
25
|
|
|
@@ -61,10 +60,6 @@ module EagerEye
|
|
|
61
60
|
config.fail_on_issues = false
|
|
62
61
|
end
|
|
63
62
|
end
|
|
64
|
-
|
|
65
|
-
def build_analyzer
|
|
66
|
-
EagerEye::Analyzer.new(paths: [@path])
|
|
67
|
-
end
|
|
68
63
|
end
|
|
69
64
|
end
|
|
70
65
|
end
|
data/lib/eager_eye/version.rb
CHANGED
data/lib/eager_eye.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "eager_eye/version"
|
|
4
4
|
require_relative "eager_eye/configuration"
|
|
5
5
|
require_relative "eager_eye/issue"
|
|
6
|
+
require_relative "eager_eye/association_parser"
|
|
6
7
|
require_relative "eager_eye/detectors/base"
|
|
7
8
|
require_relative "eager_eye/detectors/loop_association"
|
|
8
9
|
require_relative "eager_eye/detectors/serializer_nesting"
|
|
@@ -40,5 +41,4 @@ module EagerEye
|
|
|
40
41
|
end
|
|
41
42
|
end
|
|
42
43
|
|
|
43
|
-
# Load Railtie only if Rails is defined
|
|
44
44
|
require_relative "eager_eye/railtie" if defined?(Rails::Railtie)
|
data/sig/eager_eye.rbs
CHANGED
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.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- exe/eager_eye
|
|
60
60
|
- lib/eager_eye.rb
|
|
61
61
|
- lib/eager_eye/analyzer.rb
|
|
62
|
+
- lib/eager_eye/association_parser.rb
|
|
62
63
|
- lib/eager_eye/auto_fixer.rb
|
|
63
64
|
- lib/eager_eye/cli.rb
|
|
64
65
|
- lib/eager_eye/comment_parser.rb
|