eager_eye 1.1.0 → 1.1.2
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 +1 -1
- data/CHANGELOG.md +16 -0
- data/README.md +1 -1
- data/SECURITY.md +1 -0
- data/lib/eager_eye/analyzer.rb +5 -14
- data/lib/eager_eye/association_parser.rb +6 -28
- 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 +3 -10
- data/lib/eager_eye/detectors/callback_query.rb +22 -47
- 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 +7 -30
- data/lib/eager_eye/detectors/missing_counter_cache.rb +16 -47
- data/lib/eager_eye/detectors/pluck_to_array.rb +15 -48
- 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 +0 -1
- data/sig/eager_eye.rbs +0 -1
- metadata +2 -2
|
@@ -3,27 +3,9 @@
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
module Detectors
|
|
5
5
|
class CustomMethodQuery < Base
|
|
6
|
-
QUERY_METHODS = %i[
|
|
7
|
-
|
|
8
|
-
find_by
|
|
9
|
-
find_by!
|
|
10
|
-
exists?
|
|
11
|
-
find
|
|
12
|
-
first
|
|
13
|
-
last
|
|
14
|
-
take
|
|
15
|
-
pluck
|
|
16
|
-
ids
|
|
17
|
-
count
|
|
18
|
-
sum
|
|
19
|
-
average
|
|
20
|
-
minimum
|
|
21
|
-
maximum
|
|
22
|
-
].freeze
|
|
23
|
-
|
|
24
|
-
# Array-only methods that should not be flagged when collection is clearly an array
|
|
6
|
+
QUERY_METHODS = %i[where find_by find_by! exists? find first last take pluck ids count sum average minimum
|
|
7
|
+
maximum].freeze
|
|
25
8
|
ARRAY_METHODS = %i[first last take].freeze
|
|
26
|
-
|
|
27
9
|
ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map].freeze
|
|
28
10
|
|
|
29
11
|
def self.detector_name
|
|
@@ -51,34 +33,22 @@ module EagerEye
|
|
|
51
33
|
|
|
52
34
|
if iteration_block?(node)
|
|
53
35
|
block_var = extract_block_variable(node)
|
|
54
|
-
block_body =
|
|
55
|
-
|
|
56
|
-
yield(block_body, block_var, collection) if block_var && block_body
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
node.children.each do |child|
|
|
60
|
-
find_iteration_blocks(child, &block)
|
|
36
|
+
block_body = node.children[2]
|
|
37
|
+
yield(block_body, block_var, node.children[0]) if block_var && block_body
|
|
61
38
|
end
|
|
39
|
+
node.children.each { |child| find_iteration_blocks(child, &block) }
|
|
62
40
|
end
|
|
63
41
|
|
|
64
42
|
def iteration_block?(node)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
send_node = node.children[0]
|
|
68
|
-
return false unless send_node&.type == :send
|
|
69
|
-
|
|
70
|
-
method_name = send_node.children[1]
|
|
71
|
-
ITERATION_METHODS.include?(method_name)
|
|
43
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
44
|
+
ITERATION_METHODS.include?(node.children[0].children[1])
|
|
72
45
|
end
|
|
73
46
|
|
|
74
47
|
def check_block_for_query_methods(node, block_var, is_array_collection = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
75
48
|
return unless node.is_a?(Parser::AST::Node)
|
|
76
49
|
|
|
77
50
|
add_issue(node) if query_chain_on_association?(node, block_var, is_array_collection)
|
|
78
|
-
|
|
79
|
-
node.children.each do |child|
|
|
80
|
-
check_block_for_query_methods(child, block_var, is_array_collection)
|
|
81
|
-
end
|
|
51
|
+
node.children.each { |child| check_block_for_query_methods(child, block_var, is_array_collection) }
|
|
82
52
|
end
|
|
83
53
|
|
|
84
54
|
def query_chain_on_association?(node, block_var, is_array_collection = false) # rubocop:disable Style/OptionalBooleanParameter
|
|
@@ -86,35 +56,23 @@ module EagerEye
|
|
|
86
56
|
|
|
87
57
|
method_name = node.children[1]
|
|
88
58
|
return false unless QUERY_METHODS.include?(method_name)
|
|
59
|
+
return false if is_array_collection && ARRAY_METHODS.include?(method_name) &&
|
|
60
|
+
receiver_is_only_block_var?(node.children[0], block_var)
|
|
89
61
|
|
|
90
|
-
|
|
91
|
-
# AND the receiver is only the block variable (not chained)
|
|
92
|
-
if is_array_collection && ARRAY_METHODS.include?(method_name) &&
|
|
93
|
-
receiver_is_only_block_var?(node.children[0], block_var)
|
|
94
|
-
return false
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
receiver = node.children[0]
|
|
98
|
-
receiver_chain_starts_with?(receiver, block_var)
|
|
62
|
+
receiver_chain_starts_with?(node.children[0], block_var)
|
|
99
63
|
end
|
|
100
64
|
|
|
101
65
|
def receiver_is_only_block_var?(node, block_var)
|
|
102
|
-
|
|
103
|
-
node.is_a?(Parser::AST::Node) &&
|
|
104
|
-
node.type == :lvar &&
|
|
105
|
-
node.children[0] == block_var
|
|
66
|
+
node.is_a?(Parser::AST::Node) && node.type == :lvar && node.children[0] == block_var
|
|
106
67
|
end
|
|
107
68
|
|
|
108
69
|
def receiver_chain_starts_with?(node, block_var)
|
|
109
70
|
return false unless node.is_a?(Parser::AST::Node)
|
|
110
71
|
|
|
111
72
|
case node.type
|
|
112
|
-
when :lvar
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
receiver_chain_starts_with?(node.children[0], block_var)
|
|
116
|
-
else
|
|
117
|
-
false
|
|
73
|
+
when :lvar then node.children[0] == block_var
|
|
74
|
+
when :send then receiver_chain_starts_with?(node.children[0], block_var)
|
|
75
|
+
else false
|
|
118
76
|
end
|
|
119
77
|
end
|
|
120
78
|
|
|
@@ -123,37 +81,16 @@ module EagerEye
|
|
|
123
81
|
return nil unless args_node&.type == :args
|
|
124
82
|
|
|
125
83
|
first_arg = args_node.children[0]
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
first_arg.children[0]
|
|
84
|
+
first_arg&.type == :arg ? first_arg.children[0] : nil
|
|
129
85
|
end
|
|
130
86
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def extract_collection(block_node)
|
|
136
|
-
# Extract the collection being iterated on
|
|
137
|
-
# For: collection.each { |item| ... }
|
|
138
|
-
# Returns: the send node representing the collection method call
|
|
139
|
-
block_node.children[0]
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def collection_is_array?(collection_node)
|
|
143
|
-
return false unless collection_node.is_a?(Parser::AST::Node)
|
|
87
|
+
def collection_is_array?(node)
|
|
88
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
144
89
|
|
|
145
|
-
case
|
|
146
|
-
when :array
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
when :send
|
|
150
|
-
# Only consider these methods as definitely returning arrays when iterating
|
|
151
|
-
method_name = collection_node.children[1]
|
|
152
|
-
# map, select, collect, etc. on anything return arrays for iteration
|
|
153
|
-
%i[map select collect flat_map to_a uniq compact].include?(method_name)
|
|
154
|
-
else
|
|
155
|
-
# Block variable itself won't tell us if it's an array
|
|
156
|
-
false
|
|
90
|
+
case node.type
|
|
91
|
+
when :array then true
|
|
92
|
+
when :send then %i[map select collect flat_map to_a uniq compact].include?(node.children[1])
|
|
93
|
+
else false
|
|
157
94
|
end
|
|
158
95
|
end
|
|
159
96
|
|
|
@@ -173,14 +110,11 @@ module EagerEye
|
|
|
173
110
|
return "" unless node.is_a?(Parser::AST::Node)
|
|
174
111
|
|
|
175
112
|
case node.type
|
|
176
|
-
when :lvar
|
|
177
|
-
node.children[0].to_s
|
|
113
|
+
when :lvar then node.children[0].to_s
|
|
178
114
|
when :send
|
|
179
115
|
receiver_str = reconstruct_chain(node.children[0])
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
else
|
|
183
|
-
""
|
|
116
|
+
receiver_str.empty? ? node.children[1].to_s : "#{receiver_str}.#{node.children[1]}"
|
|
117
|
+
else ""
|
|
184
118
|
end
|
|
185
119
|
end
|
|
186
120
|
end
|
|
@@ -5,11 +5,8 @@ module EagerEye
|
|
|
5
5
|
class LoopAssociation < Base
|
|
6
6
|
ITERATION_METHODS = %i[each map collect select find find_all reject filter filter_map flat_map].freeze
|
|
7
7
|
PRELOAD_METHODS = %i[includes preload eager_load].freeze
|
|
8
|
-
# Methods that return a single record (not a collection)
|
|
9
8
|
SINGLE_RECORD_METHODS = %i[find find_by find_by! first first! last last! take take! second third fourth fifth
|
|
10
9
|
forty_two sole find_sole_by].freeze
|
|
11
|
-
|
|
12
|
-
# Common association names (belongs_to = singular, has_many = plural)
|
|
13
10
|
ASSOCIATION_NAMES = Set.new(%w[
|
|
14
11
|
author user owner creator admin member customer client post article comment category tag
|
|
15
12
|
parent company organization project task item order product account profile setting image
|
|
@@ -18,8 +15,6 @@ module EagerEye
|
|
|
18
15
|
tasks items orders products accounts profiles settings images avatars photos attachments
|
|
19
16
|
documents
|
|
20
17
|
]).freeze
|
|
21
|
-
|
|
22
|
-
# Methods that should NOT be treated as associations
|
|
23
18
|
EXCLUDED_METHODS = %i[
|
|
24
19
|
id to_s to_h to_a to_json to_xml inspect class object_id nil? blank? present? empty?
|
|
25
20
|
any? none? size count length save save! update update! destroy destroy! delete delete!
|
|
@@ -73,46 +68,32 @@ module EagerEye
|
|
|
73
68
|
end
|
|
74
69
|
|
|
75
70
|
def infer_model_name_from_collection(node)
|
|
76
|
-
# Infer model name from collection (posts -> Post, users -> User)
|
|
77
71
|
return nil unless node&.type == :send
|
|
78
72
|
|
|
79
|
-
# Handle: Model.includes, @var.method calls
|
|
80
73
|
receiver = node.children[0]
|
|
81
|
-
|
|
82
|
-
when :const
|
|
83
|
-
receiver.children[1].to_s
|
|
84
|
-
end
|
|
74
|
+
receiver.children[1].to_s if receiver&.type == :const
|
|
85
75
|
end
|
|
86
76
|
|
|
87
77
|
def iteration_block?(node)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
send_node = node.children[0]
|
|
91
|
-
return false unless send_node&.type == :send
|
|
92
|
-
|
|
93
|
-
method_name = send_node.children[1]
|
|
94
|
-
ITERATION_METHODS.include?(method_name)
|
|
78
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
79
|
+
ITERATION_METHODS.include?(node.children[0].children[1])
|
|
95
80
|
end
|
|
96
81
|
|
|
97
82
|
def extract_block_variable(block_node)
|
|
98
|
-
args = block_node&.children&.
|
|
99
|
-
|
|
100
|
-
|
|
83
|
+
args = block_node&.children&.[](1)
|
|
84
|
+
first_arg = args&.children&.first
|
|
85
|
+
first_arg&.type == :arg ? first_arg.children[0] : nil
|
|
101
86
|
end
|
|
102
87
|
|
|
103
88
|
def extract_included_associations(collection_node)
|
|
104
89
|
included = Set.new
|
|
105
90
|
return included unless collection_node&.type == :send
|
|
106
91
|
|
|
107
|
-
# Traverse through chained method calls to find includes/preload/eager_load
|
|
108
92
|
current = collection_node
|
|
109
93
|
while current&.type == :send
|
|
110
|
-
|
|
111
|
-
extract_includes_from_method(current, included) if PRELOAD_METHODS.include?(method_name)
|
|
112
|
-
|
|
94
|
+
extract_includes_from_method(current, included) if PRELOAD_METHODS.include?(current.children[1])
|
|
113
95
|
current = current.children[0]
|
|
114
96
|
end
|
|
115
|
-
|
|
116
97
|
included
|
|
117
98
|
end
|
|
118
99
|
|
|
@@ -177,10 +158,6 @@ module EagerEye
|
|
|
177
158
|
included_set.merge(extract_symbols_from_args(args))
|
|
178
159
|
end
|
|
179
160
|
|
|
180
|
-
def extract_from_hash(hash_node, included_set)
|
|
181
|
-
extract_symbols_from_hash(hash_node, included_set)
|
|
182
|
-
end
|
|
183
|
-
|
|
184
161
|
def find_association_calls(node, block_var, file_path, issues, included_associations = Set.new)
|
|
185
162
|
reported = Set.new
|
|
186
163
|
traverse_ast(node) do |child|
|
|
@@ -3,22 +3,14 @@
|
|
|
3
3
|
module EagerEye
|
|
4
4
|
module Detectors
|
|
5
5
|
class MissingCounterCache < Base
|
|
6
|
-
# Methods that trigger COUNT queries
|
|
7
6
|
COUNT_METHODS = %i[count size length].freeze
|
|
8
|
-
|
|
9
|
-
# Common has_many association names (plural)
|
|
10
7
|
PLURAL_ASSOCIATIONS = %w[
|
|
11
|
-
posts comments tags categories articles users members
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
followers followings likes favorites bookmarks votes
|
|
15
|
-
children replies responses answers questions
|
|
8
|
+
posts comments tags categories articles users members items orders products tasks projects
|
|
9
|
+
images attachments documents files messages notifications reviews ratings followers followings
|
|
10
|
+
likes favorites bookmarks votes children replies responses answers questions
|
|
16
11
|
].freeze
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
ITERATION_METHODS = %i[each map collect select reject find_all
|
|
20
|
-
filter filter_map flat_map each_with_index
|
|
21
|
-
each_with_object reduce inject sum].freeze
|
|
12
|
+
ITERATION_METHODS = %i[each map collect select reject find_all filter filter_map flat_map
|
|
13
|
+
each_with_index each_with_object reduce inject sum].freeze
|
|
22
14
|
|
|
23
15
|
def self.detector_name
|
|
24
16
|
:missing_counter_cache
|
|
@@ -51,68 +43,45 @@ module EagerEye
|
|
|
51
43
|
|
|
52
44
|
def count_on_association?(node)
|
|
53
45
|
return false unless node.type == :send
|
|
54
|
-
|
|
55
|
-
method_name = node.children[1]
|
|
56
|
-
return false unless COUNT_METHODS.include?(method_name)
|
|
46
|
+
return false unless COUNT_METHODS.include?(node.children[1])
|
|
57
47
|
|
|
58
48
|
receiver = node.children[0]
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
likely_association_receiver?(receiver)
|
|
49
|
+
receiver && likely_association_receiver?(receiver)
|
|
62
50
|
end
|
|
63
51
|
|
|
64
52
|
def likely_association_receiver?(node)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
method_name = node.children[1]
|
|
68
|
-
PLURAL_ASSOCIATIONS.include?(method_name.to_s)
|
|
53
|
+
node.type == :send && PLURAL_ASSOCIATIONS.include?(node.children[1].to_s)
|
|
69
54
|
end
|
|
70
55
|
|
|
71
56
|
def extract_association_name(node)
|
|
72
57
|
receiver = node.children[0]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
receiver.children[1].to_s
|
|
58
|
+
receiver.children[1].to_s if receiver&.type == :send
|
|
76
59
|
end
|
|
77
60
|
|
|
78
|
-
# Check if the node is inside an iteration block
|
|
79
61
|
def inside_iteration?(node)
|
|
80
62
|
parent = node
|
|
81
|
-
while (parent =
|
|
63
|
+
while (parent = @parent_map[parent])
|
|
82
64
|
return true if iteration_block?(parent)
|
|
83
65
|
end
|
|
84
66
|
false
|
|
85
67
|
end
|
|
86
68
|
|
|
87
|
-
def find_parent(node)
|
|
88
|
-
@parent_map ||= {}
|
|
89
|
-
@parent_map[node]
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
# Override traverse_ast to build parent map
|
|
93
69
|
def traverse_ast(node, &block)
|
|
94
70
|
return unless node.is_a?(Parser::AST::Node)
|
|
95
71
|
|
|
96
72
|
@parent_map ||= {}
|
|
97
|
-
|
|
98
73
|
yield node
|
|
99
|
-
|
|
100
74
|
node.children.each do |child|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
75
|
+
next unless child.is_a?(Parser::AST::Node)
|
|
76
|
+
|
|
77
|
+
@parent_map[child] = node
|
|
78
|
+
traverse_ast(child, &block)
|
|
105
79
|
end
|
|
106
80
|
end
|
|
107
81
|
|
|
108
82
|
def iteration_block?(node)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
send_node = node.children[0]
|
|
112
|
-
return false unless send_node&.type == :send
|
|
113
|
-
|
|
114
|
-
method_name = send_node.children[1]
|
|
115
|
-
ITERATION_METHODS.include?(method_name)
|
|
83
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
84
|
+
ITERATION_METHODS.include?(node.children[0].children[1])
|
|
116
85
|
end
|
|
117
86
|
end
|
|
118
87
|
end
|
|
@@ -49,100 +49,67 @@ module EagerEye
|
|
|
49
49
|
add_issue(node) if regular_pluck?(node)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
def local_variable_assignment?(node)
|
|
53
|
-
node.type == :lvasgn
|
|
54
|
-
end
|
|
52
|
+
def local_variable_assignment?(node) = node.type == :lvasgn
|
|
55
53
|
|
|
56
|
-
def where_call?(node)
|
|
57
|
-
node.type == :send && node.children[1] == :where
|
|
58
|
-
end
|
|
54
|
+
def where_call?(node) = node.type == :send && node.children[1] == :where
|
|
59
55
|
|
|
60
56
|
def pluck_call?(node)
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
method = node.children[1]
|
|
64
|
-
%i[pluck ids].include?(method)
|
|
57
|
+
node.is_a?(Parser::AST::Node) && node.type == :send && %i[pluck ids].include?(node.children[1])
|
|
65
58
|
end
|
|
66
59
|
|
|
67
60
|
def all_pluck_call?(node)
|
|
68
61
|
return false unless pluck_call?(node)
|
|
69
62
|
|
|
70
63
|
receiver = node.children[0]
|
|
71
|
-
receiver.is_a?(Parser::AST::Node) && receiver.type == :send &&
|
|
72
|
-
receiver.children[1] == :all
|
|
64
|
+
receiver.is_a?(Parser::AST::Node) && receiver.type == :send && receiver.children[1] == :all
|
|
73
65
|
end
|
|
74
66
|
|
|
75
67
|
def map_id_call?(node)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
block_map?(node) || send_map?(node)
|
|
68
|
+
node.is_a?(Parser::AST::Node) && (block_map?(node) || send_map?(node))
|
|
79
69
|
end
|
|
80
70
|
|
|
81
71
|
def block_map?(node)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
send_node = node.children[0]
|
|
85
|
-
send_node&.type == :send && %i[map collect].include?(send_node.children[1])
|
|
72
|
+
node.type == :block && node.children[0]&.type == :send &&
|
|
73
|
+
%i[map collect].include?(node.children[0].children[1])
|
|
86
74
|
end
|
|
87
75
|
|
|
88
76
|
def send_map?(node)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
method = node.children[1]
|
|
92
|
-
%i[map collect].include?(method) &&
|
|
77
|
+
node.type == :send && %i[map collect].include?(node.children[1]) &&
|
|
93
78
|
node.children[2..].any? { |arg| symbol_to_proc_id?(arg) }
|
|
94
79
|
end
|
|
95
80
|
|
|
96
81
|
def symbol_to_proc_id?(node)
|
|
97
82
|
return false unless node.is_a?(Parser::AST::Node) && node.type == :block_pass
|
|
98
83
|
|
|
99
|
-
sym
|
|
100
|
-
sym&.type == :sym && %i[id to_i].include?(sym.children[0])
|
|
84
|
+
node.children[0]&.type == :sym && %i[id to_i].include?(node.children[0].children[0])
|
|
101
85
|
end
|
|
102
86
|
|
|
103
87
|
def regular_pluck?(node)
|
|
104
|
-
|
|
105
|
-
where_args.any? { |arg| pluck_var_in_hash?(arg) }
|
|
88
|
+
node.children[2..].any? { |arg| pluck_var_in_hash?(arg) }
|
|
106
89
|
end
|
|
107
90
|
|
|
108
91
|
def critical_pluck?(node)
|
|
109
|
-
|
|
110
|
-
where_args.any? { |arg| critical_pluck_in_hash?(arg) }
|
|
92
|
+
node.children[2..].any? { |arg| critical_pluck_in_hash?(arg) }
|
|
111
93
|
end
|
|
112
94
|
|
|
113
95
|
def pluck_var_in_hash?(node)
|
|
114
96
|
return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
|
|
115
97
|
|
|
116
|
-
node.children.any?
|
|
117
|
-
next false unless pair.type == :pair
|
|
118
|
-
|
|
119
|
-
pluck_value?(pair.children[1])
|
|
120
|
-
end
|
|
98
|
+
node.children.any? { |pair| pair.type == :pair && pluck_value?(pair.children[1]) }
|
|
121
99
|
end
|
|
122
100
|
|
|
123
101
|
def critical_pluck_in_hash?(node)
|
|
124
102
|
return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
|
|
125
103
|
|
|
126
|
-
node.children.any?
|
|
127
|
-
next false unless pair.type == :pair
|
|
128
|
-
|
|
129
|
-
critical_value?(pair.children[1])
|
|
130
|
-
end
|
|
104
|
+
node.children.any? { |pair| pair.type == :pair && critical_value?(pair.children[1]) }
|
|
131
105
|
end
|
|
132
106
|
|
|
133
107
|
def pluck_value?(value)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
var_name = value.children[0]
|
|
137
|
-
@pluck_variables.key?(var_name) || @map_id_variables.key?(var_name)
|
|
108
|
+
value.type == :lvar && (@pluck_variables.key?(value.children[0]) || @map_id_variables.key?(value.children[0]))
|
|
138
109
|
end
|
|
139
110
|
|
|
140
111
|
def critical_value?(value)
|
|
141
|
-
|
|
142
|
-
@critical_pluck_variables.key?(value.children[0])
|
|
143
|
-
else
|
|
144
|
-
all_pluck_call?(value)
|
|
145
|
-
end
|
|
112
|
+
value.type == :lvar ? @critical_pluck_variables.key?(value.children[0]) : all_pluck_call?(value)
|
|
146
113
|
end
|
|
147
114
|
|
|
148
115
|
def add_issue(node)
|
|
@@ -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
|