eager_eye 1.2.0 → 1.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 +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +28 -0
- data/README.md +1 -1
- data/lib/eager_eye/detectors/concerns/non_ar_source_detector.rb +56 -0
- data/lib/eager_eye/detectors/custom_method_query.rb +8 -1
- data/lib/eager_eye/detectors/loop_association.rb +4 -1
- data/lib/eager_eye/detectors/pluck_to_array.rb +130 -84
- data/lib/eager_eye/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a432b16aece37497880140ff64a88cd3dabe6ae603846a513d21f71a8b11b5a1
|
|
4
|
+
data.tar.gz: 330aac1ef792a5ca24597dcd4128329e20f05959ea45437e244788b94f12026d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 192aa6bb71494f09091ac64a44de11526db09ec389cf41db80229457d59d52f5e81c9b9ae05cc6892ec11c646078a12520c657940a986c47f6519af6b956526f
|
|
7
|
+
data.tar.gz: 3e18421c967675ea0c10ab19ff7f0426d2a2d8e2fcf969a20b6bc0571f0f32a40c8f515b16c464df57c67b447fe0e9c3b3087b22cee37bbda3bee89b1dd3d418
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.2] - 2026-01-31
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **CustomMethodQuery False Positive** - Skip PostgreSQL array column methods
|
|
15
|
+
- Methods ending with `_ids`, `_tags`, `_types`, `_codes`, `_names`, `_values` now recognized as array attributes
|
|
16
|
+
- `sector_subcategory_ids.first` no longer flagged (Ruby Array#first, not AR query)
|
|
17
|
+
|
|
18
|
+
- **LoopAssociation False Positive** - Skip common non-association attribute methods
|
|
19
|
+
- Added `origin`, `priority`, `level`, `kind`, `label`, `code`, `reason`, `amount`, `price`, `quantity`, `url`, `path`, `email`, `phone`, `address`, `notes`, `memo`, `data`, `metadata`, `position`, `rank`, `score`, `rating`, `enabled`, `disabled`, `active`, `published`, `draft`, `archived`, `locked`, `visible`, `hidden` to excluded methods
|
|
20
|
+
- Note: `category` and `tag` remain detectable as they're common association names - use inline suppression if they're string attributes in your codebase
|
|
21
|
+
|
|
22
|
+
- **PluckToArray False Positive** - Skip params-originated values
|
|
23
|
+
- `params[:ids].split(',').map(&:to_i)` no longer flagged
|
|
24
|
+
- `params` method now recognized as non-ActiveRecord source
|
|
25
|
+
|
|
26
|
+
## [1.2.1] - 2026-01-31
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- **PluckToArray False Positives** - Major improvements to reduce false positives:
|
|
31
|
+
- Skip when variable is used in multiple places (e.g., for ordering with `array_position`)
|
|
32
|
+
- Skip non-ActiveRecord sources: Sidekiq, Redis, Resque, DelayedJob
|
|
33
|
+
- Skip `.to_sql` usage patterns (UNION queries can't use subqueries)
|
|
34
|
+
- Skip non-AR `.where` receivers (Sidekiq::Queue, Redis, etc.)
|
|
35
|
+
- Differentiate message between `.pluck()` and `.map(&:id)` patterns
|
|
36
|
+
- Skip block maps like `.map { |u| u[:id] }` (likely Hash/Array access)
|
|
37
|
+
|
|
10
38
|
## [1.2.0] - 2026-01-18
|
|
11
39
|
|
|
12
40
|
### Features
|
data/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://github.com/hamzagedikkaya/eager_eye/actions/workflows/main.yml"><img src="https://github.com/hamzagedikkaya/eager_eye/actions/workflows/main.yml/badge.svg" alt="CI"></a>
|
|
13
|
-
<a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.2.
|
|
13
|
+
<a href="https://rubygems.org/gems/eager_eye"><img src="https://img.shields.io/badge/gem-v1.2.2-red.svg" alt="Gem Version"></a>
|
|
14
14
|
<a href="https://github.com/hamzagedikkaya/eager_eye"><img src="https://img.shields.io/badge/coverage-95%25-brightgreen.svg" alt="Coverage"></a>
|
|
15
15
|
<a href="https://www.ruby-lang.org/"><img src="https://img.shields.io/badge/ruby-%3E%3D%203.1-ruby.svg" alt="Ruby"></a>
|
|
16
16
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module EagerEye
|
|
4
|
+
module Detectors
|
|
5
|
+
module Concerns
|
|
6
|
+
module NonArSourceDetector
|
|
7
|
+
NON_AR_RECEIVERS = %w[Sidekiq Redis Resque DelayedJob Queue Job Hash Array Set].freeze
|
|
8
|
+
NON_DB_SOURCE_METHODS = %i[smembers sinter sunion sdiff zrange zrangebyscore lrange hkeys hvals hgetall
|
|
9
|
+
keys values entries args params].freeze
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def ar_receiver?(node)
|
|
14
|
+
receiver = node.children[0]
|
|
15
|
+
return true unless receiver.is_a?(Parser::AST::Node)
|
|
16
|
+
|
|
17
|
+
!non_ar_class?(receiver)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def non_ar_class?(node)
|
|
21
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
22
|
+
return NON_AR_RECEIVERS.any? { |r| extract_const_name(node).include?(r) } if node.type == :const
|
|
23
|
+
return non_ar_class?(node.children[0]) if node.type == :send && node.children[0].is_a?(Parser::AST::Node)
|
|
24
|
+
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def extract_const_name(node)
|
|
29
|
+
return "" unless node.is_a?(Parser::AST::Node) && node.type == :const
|
|
30
|
+
|
|
31
|
+
parent_name = extract_const_name(node.children[0])
|
|
32
|
+
name = node.children[1].to_s
|
|
33
|
+
parent_name.empty? ? name : "#{parent_name}::#{name}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def non_db_source?(node)
|
|
37
|
+
return false unless node.is_a?(Parser::AST::Node)
|
|
38
|
+
return false unless %i[send block].include?(node.type)
|
|
39
|
+
|
|
40
|
+
send_node = node.type == :block ? node.children[0] : node
|
|
41
|
+
send_node.is_a?(Parser::AST::Node) && non_db_method_chain?(send_node)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def non_db_method_chain?(node)
|
|
45
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :send
|
|
46
|
+
return true if NON_DB_SOURCE_METHODS.include?(node.children[1])
|
|
47
|
+
|
|
48
|
+
receiver = node.children[0]
|
|
49
|
+
return non_ar_class?(receiver) || non_db_method_chain?(receiver) if receiver.is_a?(Parser::AST::Node)
|
|
50
|
+
|
|
51
|
+
false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -7,6 +7,7 @@ module EagerEye
|
|
|
7
7
|
maximum].freeze
|
|
8
8
|
SAFE_QUERY_METHODS = %i[first last take count sum find size length ids].freeze
|
|
9
9
|
SAFE_TRANSFORM_METHODS = %i[keys values split [] params sort pluck ids to_s to_a to_i chars bytes].freeze
|
|
10
|
+
ARRAY_COLUMN_SUFFIXES = %w[_ids _tags _types _codes _names _values].freeze
|
|
10
11
|
ITERATION_METHODS = %i[each map select find_all reject collect detect find_index flat_map].freeze
|
|
11
12
|
|
|
12
13
|
def self.detector_name
|
|
@@ -124,7 +125,13 @@ module EagerEye
|
|
|
124
125
|
def receiver_ends_with_safe_transform_method?(node)
|
|
125
126
|
return false unless node.is_a?(Parser::AST::Node) && node.type == :send
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
method_name = node.children[1]
|
|
129
|
+
SAFE_TRANSFORM_METHODS.include?(method_name) || array_column_method?(method_name)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def array_column_method?(method_name)
|
|
133
|
+
method_str = method_name.to_s
|
|
134
|
+
ARRAY_COLUMN_SUFFIXES.any? { |suffix| method_str.end_with?(suffix) }
|
|
128
135
|
end
|
|
129
136
|
|
|
130
137
|
def add_issue(node)
|
|
@@ -19,7 +19,10 @@ module EagerEye
|
|
|
19
19
|
id to_s to_h to_a to_json to_xml inspect class object_id nil? blank? present? empty?
|
|
20
20
|
any? none? size count length save save! update update! destroy destroy! delete delete!
|
|
21
21
|
valid? invalid? errors new? persisted? changed? frozen? name title body content text
|
|
22
|
-
description value key type status state created_at updated_at deleted_at
|
|
22
|
+
description value key type status state created_at updated_at deleted_at origin
|
|
23
|
+
priority level kind label code reason amount price quantity url path email phone
|
|
24
|
+
address notes memo data metadata position rank score rating enabled disabled active
|
|
25
|
+
published draft archived locked visible hidden
|
|
23
26
|
].freeze
|
|
24
27
|
|
|
25
28
|
def self.detector_name
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "concerns/non_ar_source_detector"
|
|
4
|
+
|
|
3
5
|
module EagerEye
|
|
4
6
|
module Detectors
|
|
5
7
|
class PluckToArray < Base
|
|
8
|
+
include Concerns::NonArSourceDetector
|
|
9
|
+
|
|
6
10
|
SMALL_COLLECTIONS = %w[tags settings options categories roles permissions statuses types priorities].freeze
|
|
7
11
|
|
|
8
12
|
def self.detector_name
|
|
@@ -12,53 +16,100 @@ module EagerEye
|
|
|
12
16
|
def detect(ast, file_path)
|
|
13
17
|
@issues = []
|
|
14
18
|
@file_path = file_path
|
|
15
|
-
|
|
16
|
-
@map_id_variables = {}
|
|
17
|
-
@critical_pluck_variables = {}
|
|
18
|
-
@small_collection_variables = {}
|
|
19
|
+
reset_tracking_variables
|
|
19
20
|
|
|
20
21
|
return @issues unless ast
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
collect_all_info(ast)
|
|
24
|
+
check_ast(ast)
|
|
25
|
+
|
|
23
26
|
@issues
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
private
|
|
27
30
|
|
|
28
|
-
def
|
|
31
|
+
def reset_tracking_variables
|
|
32
|
+
@pluck_variables = {}
|
|
33
|
+
@map_id_variables = {}
|
|
34
|
+
@critical_pluck_variables = {}
|
|
35
|
+
@small_collection_variables = {}
|
|
36
|
+
@variable_usages = Hash.new(0)
|
|
37
|
+
@to_sql_variables = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def collect_all_info(node)
|
|
29
41
|
return unless node.is_a?(Parser::AST::Node)
|
|
30
42
|
|
|
31
43
|
collect_assignments(node)
|
|
32
|
-
|
|
44
|
+
collect_variable_usage(node)
|
|
45
|
+
collect_to_sql_usage(node)
|
|
33
46
|
|
|
34
|
-
node.children.each { |child|
|
|
47
|
+
node.children.each { |child| collect_all_info(child) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def check_ast(node)
|
|
51
|
+
return unless node.is_a?(Parser::AST::Node)
|
|
52
|
+
|
|
53
|
+
check_where_calls(node)
|
|
54
|
+
node.children.each { |child| check_ast(child) }
|
|
35
55
|
end
|
|
36
56
|
|
|
37
57
|
def collect_assignments(node)
|
|
38
|
-
return unless
|
|
58
|
+
return unless node.type == :lvasgn
|
|
39
59
|
|
|
40
60
|
var_name = node.children[0]
|
|
41
61
|
value = node.children[1]
|
|
42
62
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
63
|
+
return if non_db_source?(value)
|
|
64
|
+
|
|
65
|
+
track_variable_type(var_name, value, node.loc.line)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def track_variable_type(var_name, value, line)
|
|
69
|
+
@critical_pluck_variables[var_name] = line if all_pluck_call?(value)
|
|
70
|
+
@small_collection_variables[var_name] = line if small_collection_pluck?(value)
|
|
71
|
+
@pluck_variables[var_name] = line if pluck_call?(value)
|
|
72
|
+
@map_id_variables[var_name] = line if map_id_call?(value)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def collect_variable_usage(node)
|
|
76
|
+
return unless node.is_a?(Parser::AST::Node) && node.type == :lvar
|
|
77
|
+
|
|
78
|
+
@variable_usages[node.children[0]] += 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def collect_to_sql_usage(node)
|
|
82
|
+
return unless node.is_a?(Parser::AST::Node) && node.type == :send && node.children[1] == :to_sql
|
|
83
|
+
|
|
84
|
+
find_variables_in_chain(node).each { |var_name| @to_sql_variables[var_name] = true }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def find_variables_in_chain(node)
|
|
88
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
|
89
|
+
return [node.children[0]] if node.type == :lvar
|
|
90
|
+
|
|
91
|
+
node.children.flat_map { |child| find_variables_in_chain(child) }
|
|
47
92
|
end
|
|
48
93
|
|
|
49
94
|
def check_where_calls(node)
|
|
50
|
-
return unless where_call?(node)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
95
|
+
return unless where_call?(node) && ar_receiver?(node)
|
|
96
|
+
|
|
97
|
+
process_where_call(node)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def process_where_call(node)
|
|
101
|
+
if critical_pluck?(node) then add_critical_issue(node)
|
|
102
|
+
elsif small_collection?(node) then add_info_issue(node)
|
|
103
|
+
elsif regular_pluck?(node) then check_regular_pluck(node)
|
|
58
104
|
end
|
|
59
105
|
end
|
|
60
106
|
|
|
61
|
-
def
|
|
107
|
+
def check_regular_pluck(node)
|
|
108
|
+
var_name = find_pluck_var_in_where(node)
|
|
109
|
+
return if var_name && (multi_use_variable?(var_name) || @to_sql_variables[var_name])
|
|
110
|
+
|
|
111
|
+
add_issue(node, var_name)
|
|
112
|
+
end
|
|
62
113
|
|
|
63
114
|
def where_call?(node) = node.type == :send && node.children[1] == :where
|
|
64
115
|
|
|
@@ -67,108 +118,103 @@ module EagerEye
|
|
|
67
118
|
end
|
|
68
119
|
|
|
69
120
|
def all_pluck_call?(node)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
receiver = node.children[0]
|
|
73
|
-
receiver.is_a?(Parser::AST::Node) && receiver.type == :send && receiver.children[1] == :all
|
|
121
|
+
pluck_call?(node) && node.children[0].is_a?(Parser::AST::Node) &&
|
|
122
|
+
node.children[0].type == :send && node.children[0].children[1] == :all
|
|
74
123
|
end
|
|
75
124
|
|
|
76
125
|
def small_collection_pluck?(node)
|
|
77
126
|
return false unless pluck_call?(node)
|
|
78
127
|
|
|
79
128
|
receiver = node.children[0]
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
method_name = receiver.children[1].to_s
|
|
83
|
-
SMALL_COLLECTIONS.any? { |c| method_name.include?(c) }
|
|
129
|
+
receiver.is_a?(Parser::AST::Node) && receiver.type == :send &&
|
|
130
|
+
SMALL_COLLECTIONS.any? { |c| receiver.children[1].to_s.include?(c) }
|
|
84
131
|
end
|
|
85
132
|
|
|
86
133
|
def map_id_call?(node)
|
|
87
|
-
node.is_a?(Parser::AST::Node) &&
|
|
134
|
+
node.is_a?(Parser::AST::Node) && node.type == :send && %i[map collect].include?(node.children[1]) &&
|
|
135
|
+
node.children[2..].any? { |arg| symbol_to_proc_id?(arg) }
|
|
88
136
|
end
|
|
89
137
|
|
|
90
|
-
def
|
|
91
|
-
node.
|
|
92
|
-
%i[
|
|
138
|
+
def symbol_to_proc_id?(node)
|
|
139
|
+
node.is_a?(Parser::AST::Node) && node.type == :block_pass &&
|
|
140
|
+
node.children[0]&.type == :sym && %i[id to_i].include?(node.children[0].children[0])
|
|
93
141
|
end
|
|
94
142
|
|
|
95
|
-
def
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
end
|
|
143
|
+
def regular_pluck?(node) = node.children[2..].any? { |arg| pluck_var_in_hash?(arg) }
|
|
144
|
+
def critical_pluck?(node) = node.children[2..].any? { |arg| critical_pluck_in_hash?(arg) }
|
|
145
|
+
def small_collection?(node) = node.children[2..].any? { |arg| small_collection_in_hash?(arg) }
|
|
99
146
|
|
|
100
|
-
def
|
|
101
|
-
|
|
147
|
+
def pluck_var_in_hash?(node) = hash_with_value?(node) { |v| pluck_value?(v) }
|
|
148
|
+
def critical_pluck_in_hash?(node) = hash_with_value?(node) { |v| critical_value?(v) }
|
|
149
|
+
def small_collection_in_hash?(node) = hash_with_value?(node) { |v| small_collection_value?(v) }
|
|
102
150
|
|
|
103
|
-
|
|
104
|
-
|
|
151
|
+
def hash_with_value?(node, &block)
|
|
152
|
+
return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
|
|
105
153
|
|
|
106
|
-
|
|
107
|
-
node.children[2..].any? { |arg| pluck_var_in_hash?(arg) }
|
|
154
|
+
node.children.any? { |pair| pair.type == :pair && block.call(pair.children[1]) }
|
|
108
155
|
end
|
|
109
156
|
|
|
110
|
-
def
|
|
111
|
-
|
|
157
|
+
def pluck_value?(val)
|
|
158
|
+
val.type == :lvar && (@pluck_variables.key?(val.children[0]) || @map_id_variables.key?(val.children[0]))
|
|
112
159
|
end
|
|
113
160
|
|
|
114
|
-
def
|
|
115
|
-
|
|
161
|
+
def critical_value?(val)
|
|
162
|
+
val.type == :lvar ? @critical_pluck_variables.key?(val.children[0]) : all_pluck_call?(val)
|
|
116
163
|
end
|
|
117
164
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
node.children.any? { |pair| pair.type == :pair && pluck_value?(pair.children[1]) }
|
|
165
|
+
def small_collection_value?(val)
|
|
166
|
+
val.type == :lvar && @small_collection_variables.key?(val.children[0])
|
|
122
167
|
end
|
|
123
168
|
|
|
124
|
-
def
|
|
125
|
-
|
|
169
|
+
def find_pluck_var_in_where(node)
|
|
170
|
+
node.children[2..].each do |arg|
|
|
171
|
+
next unless arg.is_a?(Parser::AST::Node) && arg.type == :hash
|
|
126
172
|
|
|
127
|
-
|
|
173
|
+
var = find_pluck_var_in_hash(arg)
|
|
174
|
+
return var if var
|
|
175
|
+
end
|
|
176
|
+
nil
|
|
128
177
|
end
|
|
129
178
|
|
|
130
|
-
def
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
node.children.any? { |pair| pair.type == :pair && small_collection_value?(pair.children[1]) }
|
|
134
|
-
end
|
|
179
|
+
def find_pluck_var_in_hash(hash_node)
|
|
180
|
+
hash_node.children.each do |pair|
|
|
181
|
+
next unless pair.type == :pair && pair.children[1].type == :lvar
|
|
135
182
|
|
|
136
|
-
|
|
137
|
-
|
|
183
|
+
var_name = pair.children[1].children[0]
|
|
184
|
+
return var_name if @pluck_variables.key?(var_name) || @map_id_variables.key?(var_name)
|
|
185
|
+
end
|
|
186
|
+
nil
|
|
138
187
|
end
|
|
139
188
|
|
|
140
|
-
def
|
|
141
|
-
|
|
142
|
-
end
|
|
189
|
+
def multi_use_variable?(var_name) = @variable_usages[var_name] > 1
|
|
190
|
+
def map_variable?(var_name) = @map_id_variables.key?(var_name)
|
|
143
191
|
|
|
144
|
-
def
|
|
145
|
-
|
|
192
|
+
def add_issue(node, var_name = nil)
|
|
193
|
+
message, suggestion = issue_content(var_name)
|
|
194
|
+
@issues << create_issue(file_path: @file_path, line_number: node.loc.line,
|
|
195
|
+
message: message, suggestion: suggestion, severity: :warning)
|
|
146
196
|
end
|
|
147
197
|
|
|
148
|
-
def
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
198
|
+
def issue_content(var_name)
|
|
199
|
+
if var_name && map_variable?(var_name)
|
|
200
|
+
["Using ID array from `.map(&:id)` in `where` causes two queries",
|
|
201
|
+
"If source is ActiveRecord, use `.select(:id)` subquery instead"]
|
|
202
|
+
else
|
|
203
|
+
["Using plucked array in `where` causes two queries and memory overhead",
|
|
204
|
+
"Use `.select(:id)` subquery instead: `Model.where(col: OtherModel.select(:id))`"]
|
|
205
|
+
end
|
|
156
206
|
end
|
|
157
207
|
|
|
158
208
|
def add_critical_issue(node)
|
|
159
|
-
@issues << create_issue(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.select(:id))`",
|
|
164
|
-
severity: :error
|
|
165
|
-
)
|
|
209
|
+
@issues << create_issue(file_path: @file_path, line_number: node.loc.line,
|
|
210
|
+
message: "Using `.all.pluck(:id)` loads entire table into memory - highly inefficient",
|
|
211
|
+
suggestion: "Use `.select(:id)` subquery: `Model.where(col: OtherModel.select(:id))`",
|
|
212
|
+
severity: :error)
|
|
166
213
|
end
|
|
167
214
|
|
|
168
215
|
def add_info_issue(node)
|
|
169
216
|
@issues << create_issue(
|
|
170
|
-
file_path: @file_path,
|
|
171
|
-
line_number: node.loc.line,
|
|
217
|
+
file_path: @file_path, line_number: node.loc.line,
|
|
172
218
|
message: "Small collection pluck may be acceptable for few records",
|
|
173
219
|
suggestion: "Consider `.select(:id)` for consistency, but pluck is fine for small collections",
|
|
174
220
|
severity: :info
|
data/lib/eager_eye/version.rb
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.2.
|
|
4
|
+
version: 1.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- hamzagedikkaya
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-01
|
|
11
|
+
date: 2026-02-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ast
|
|
@@ -66,6 +66,7 @@ files:
|
|
|
66
66
|
- lib/eager_eye/configuration.rb
|
|
67
67
|
- lib/eager_eye/detectors/base.rb
|
|
68
68
|
- lib/eager_eye/detectors/callback_query.rb
|
|
69
|
+
- lib/eager_eye/detectors/concerns/non_ar_source_detector.rb
|
|
69
70
|
- lib/eager_eye/detectors/count_in_iteration.rb
|
|
70
71
|
- lib/eager_eye/detectors/custom_method_query.rb
|
|
71
72
|
- lib/eager_eye/detectors/loop_association.rb
|
|
@@ -90,7 +91,6 @@ licenses:
|
|
|
90
91
|
- MIT
|
|
91
92
|
metadata:
|
|
92
93
|
allowed_push_host: https://rubygems.org
|
|
93
|
-
homepage_uri: https://github.com/hamzagedikkaya/eager_eye
|
|
94
94
|
source_code_uri: https://github.com/hamzagedikkaya/eager_eye
|
|
95
95
|
changelog_uri: https://github.com/hamzagedikkaya/eager_eye/blob/master/CHANGELOG.md
|
|
96
96
|
rubygems_mfa_required: 'true'
|