eager_eye 0.2.0 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4adfe62f9c9082d1c0f1e9b149af5f2cc09db5a455af6c0262687a5770b2c50b
4
- data.tar.gz: 5284a262cf22fc9e7191dafa0a45383b3f073565313e02d202fb902bc250e7bb
3
+ metadata.gz: 6329285c3bdd39b8c99bbf665ffef121ac0dc5530b26b8e125d71ca04239a7a4
4
+ data.tar.gz: 78a608b043a06a58b22a54553d0c0b4ee459c2ee921405f01c5a216a77e801fb
5
5
  SHA512:
6
- metadata.gz: e40d5802cd52757817d3c6f071abb7203d05ca5b127a72ee5534066b59b4addb2fdec1a592f703864710eec81fa571ef774ada0cd2381995b3819b711366d939
7
- data.tar.gz: 50d3bcfa2029032200cae2b4e05ba5ef319f1a981acfc1b8b2969e0dcd977d849d01c2edab51f42808d0bdda538a63be280ad2063f5927907b64e579185c223e
6
+ metadata.gz: 5d985342e6d3bc4a09dab284b153807b4cda7a7dce898c4dca9ca3e0223e6fb2f575b369426a2a04c30f4d04b557749be8091ee4ed23de4c4a86522d410867ec
7
+ data.tar.gz: 18ba07590dce273befec6295f0ce05702241ee46a727c05825dd573d44e3019839655f0da14dc9674f5ded7d56b7ee21d802b6264a2b2a85c5f6859928c887da
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EagerEye
4
+ module Detectors
5
+ class CustomMethodQuery < Base
6
+ QUERY_METHODS = %i[
7
+ where
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
+ ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map].freeze
25
+
26
+ def self.detector_name
27
+ :custom_method_query
28
+ end
29
+
30
+ def detect(ast, file_path)
31
+ return [] unless ast
32
+
33
+ @issues = []
34
+ @file_path = file_path
35
+
36
+ find_iteration_blocks(ast) do |block_body, block_var|
37
+ check_block_for_query_methods(block_body, block_var)
38
+ end
39
+
40
+ @issues
41
+ end
42
+
43
+ private
44
+
45
+ def find_iteration_blocks(node, &block)
46
+ return unless node.is_a?(Parser::AST::Node)
47
+
48
+ if iteration_block?(node)
49
+ block_var = extract_block_variable(node)
50
+ block_body = extract_block_body(node)
51
+ yield(block_body, block_var) if block_var && block_body
52
+ end
53
+
54
+ node.children.each do |child|
55
+ find_iteration_blocks(child, &block)
56
+ end
57
+ end
58
+
59
+ def iteration_block?(node)
60
+ return false unless node.type == :block
61
+
62
+ send_node = node.children[0]
63
+ return false unless send_node&.type == :send
64
+
65
+ method_name = send_node.children[1]
66
+ ITERATION_METHODS.include?(method_name)
67
+ end
68
+
69
+ def check_block_for_query_methods(node, block_var)
70
+ return unless node.is_a?(Parser::AST::Node)
71
+
72
+ add_issue(node) if query_chain_on_association?(node, block_var)
73
+
74
+ node.children.each do |child|
75
+ check_block_for_query_methods(child, block_var)
76
+ end
77
+ end
78
+
79
+ def query_chain_on_association?(node, block_var)
80
+ return false unless node.type == :send
81
+
82
+ method_name = node.children[1]
83
+ return false unless QUERY_METHODS.include?(method_name)
84
+
85
+ receiver = node.children[0]
86
+ receiver_chain_starts_with?(receiver, block_var)
87
+ end
88
+
89
+ def receiver_chain_starts_with?(node, block_var)
90
+ return false unless node.is_a?(Parser::AST::Node)
91
+
92
+ case node.type
93
+ when :lvar
94
+ node.children[0] == block_var
95
+ when :send
96
+ receiver_chain_starts_with?(node.children[0], block_var)
97
+ else
98
+ false
99
+ end
100
+ end
101
+
102
+ def extract_block_variable(block_node)
103
+ args_node = block_node.children[1]
104
+ return nil unless args_node&.type == :args
105
+
106
+ first_arg = args_node.children[0]
107
+ return nil unless first_arg&.type == :arg
108
+
109
+ first_arg.children[0]
110
+ end
111
+
112
+ def extract_block_body(block_node)
113
+ block_node.children[2]
114
+ end
115
+
116
+ def add_issue(node)
117
+ method_name = node.children[1]
118
+ association_chain = reconstruct_chain(node.children[0])
119
+
120
+ @issues << create_issue(
121
+ file_path: @file_path,
122
+ line_number: node.loc.line,
123
+ message: "Query method `.#{method_name}` called on `#{association_chain}` inside iteration",
124
+ severity: :warning,
125
+ suggestion: "This query executes on each iteration. Consider preloading data or restructuring the query."
126
+ )
127
+ end
128
+
129
+ def reconstruct_chain(node)
130
+ return "" unless node.is_a?(Parser::AST::Node)
131
+
132
+ case node.type
133
+ when :lvar
134
+ node.children[0].to_s
135
+ when :send
136
+ receiver_str = reconstruct_chain(node.children[0])
137
+ method = node.children[1]
138
+ receiver_str.empty? ? method.to_s : "#{receiver_str}.#{method}"
139
+ else
140
+ ""
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -49,7 +49,11 @@ module EagerEye
49
49
  block_body = node.children[2]
50
50
  next unless block_body
51
51
 
52
- find_association_calls(block_body, block_var, file_path, issues)
52
+ # Check if the collection already has includes
53
+ collection_node = node.children[0]
54
+ included_associations = extract_included_associations(collection_node)
55
+
56
+ find_association_calls(block_body, block_var, file_path, issues, included_associations)
53
57
  end
54
58
 
55
59
  issues
@@ -78,7 +82,44 @@ module EagerEye
78
82
  first_arg.children[0]
79
83
  end
80
84
 
81
- def find_association_calls(node, block_var, file_path, issues)
85
+ def extract_included_associations(collection_node)
86
+ included = Set.new
87
+ return included unless collection_node&.type == :send
88
+
89
+ # Traverse through chained method calls to find includes()
90
+ current = collection_node
91
+ while current&.type == :send
92
+ method_name = current.children[1]
93
+ extract_includes_from_method(current, included) if method_name == :includes
94
+
95
+ current = current.children[0]
96
+ end
97
+
98
+ included
99
+ end
100
+
101
+ def extract_includes_from_method(method_node, included_set)
102
+ args = method_node.children[2..]
103
+ args&.each do |arg|
104
+ case arg&.type
105
+ when :sym
106
+ # includes(:product)
107
+ included_set.add(arg.children[0])
108
+ when :hash
109
+ # includes(product: :manufacturer)
110
+ extract_from_hash(arg, included_set)
111
+ end
112
+ end
113
+ end
114
+
115
+ def extract_from_hash(hash_node, included_set)
116
+ hash_node.children.each do |pair|
117
+ key = pair.children[0]
118
+ included_set.add(key.children[0]) if key&.type == :sym
119
+ end
120
+ end
121
+
122
+ def find_association_calls(node, block_var, file_path, issues, included_associations = Set.new)
82
123
  reported_associations = Set.new
83
124
 
84
125
  traverse_ast(node) do |child|
@@ -91,6 +132,9 @@ module EagerEye
91
132
  next unless direct_call_on_block_var?(receiver, block_var)
92
133
  next unless likely_association?(method_name)
93
134
 
135
+ # Skip if association is already included
136
+ next if included_associations.include?(method_name)
137
+
94
138
  # Avoid duplicate reports for same association on same line
95
139
  report_key = "#{child.loc.line}:#{method_name}"
96
140
  next if reported_associations.include?(report_key)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eager_eye
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - hamzagedikkaya
@@ -61,6 +61,7 @@ files:
61
61
  - lib/eager_eye/cli.rb
62
62
  - lib/eager_eye/configuration.rb
63
63
  - lib/eager_eye/detectors/base.rb
64
+ - lib/eager_eye/detectors/custom_method_query.rb
64
65
  - lib/eager_eye/detectors/loop_association.rb
65
66
  - lib/eager_eye/detectors/missing_counter_cache.rb
66
67
  - lib/eager_eye/detectors/serializer_nesting.rb