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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 717dbf77ddadff7da379108a74205cabaed1cdb2796ff8af8dce681fca07b1c9
4
- data.tar.gz: 424b8b40da2e4bb69f54d58a5d2923346b016db3d1e69220c06e8bbde0dfa7c6
3
+ metadata.gz: a432b16aece37497880140ff64a88cd3dabe6ae603846a513d21f71a8b11b5a1
4
+ data.tar.gz: 330aac1ef792a5ca24597dcd4128329e20f05959ea45437e244788b94f12026d
5
5
  SHA512:
6
- metadata.gz: efd5e760d3ea54457eb837011b538424553c532ca38a29660a3865c952a32a42ff82bbaeca5925fd423c56510800fc6bc961483a6c167e5a7b132eb06b2f93d3
7
- data.tar.gz: 5d80c29a7a1648541377e10e990869293e9f1ba3c99490fec4aa4efab16b99ad56f7ac00b8ab5df9763c70dc41d3dbe2ee9984212f43e2d261f599c15aca8919
6
+ metadata.gz: 192aa6bb71494f09091ac64a44de11526db09ec389cf41db80229457d59d52f5e81c9b9ae05cc6892ec11c646078a12520c657940a986c47f6519af6b956526f
7
+ data.tar.gz: 3e18421c967675ea0c10ab19ff7f0426d2a2d8e2fcf969a20b6bc0571f0f32a40c8f515b16c464df57c67b447fe0e9c3b3087b22cee37bbda3bee89b1dd3d418
data/.rubocop.yml CHANGED
@@ -20,7 +20,7 @@ Metrics/BlockLength:
20
20
  - "lib/eager_eye/railtie.rb"
21
21
 
22
22
  Metrics/ClassLength:
23
- Max: 155
23
+ Max: 165
24
24
 
25
25
  Metrics/ParameterLists:
26
26
  Max: 6
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.0-red.svg" alt="Gem Version"></a>
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
- SAFE_TRANSFORM_METHODS.include?(node.children[1])
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
- @pluck_variables = {}
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
- visit(ast)
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 visit(node)
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
- check_where_calls(node)
44
+ collect_variable_usage(node)
45
+ collect_to_sql_usage(node)
33
46
 
34
- node.children.each { |child| visit(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 local_variable_assignment?(node)
58
+ return unless node.type == :lvasgn
39
59
 
40
60
  var_name = node.children[0]
41
61
  value = node.children[1]
42
62
 
43
- @critical_pluck_variables[var_name] = node.loc.line if all_pluck_call?(value)
44
- @small_collection_variables[var_name] = node.loc.line if small_collection_pluck?(value)
45
- @pluck_variables[var_name] = node.loc.line if pluck_call?(value)
46
- @map_id_variables[var_name] = node.loc.line if map_id_call?(value)
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
- if critical_pluck?(node)
53
- add_critical_issue(node)
54
- elsif small_collection?(node)
55
- add_info_issue(node)
56
- elsif regular_pluck?(node)
57
- add_issue(node)
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 local_variable_assignment?(node) = node.type == :lvasgn
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
- return false unless pluck_call?(node)
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
- return false unless receiver.is_a?(Parser::AST::Node) && receiver.type == :send
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) && (block_map?(node) || send_map?(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 block_map?(node)
91
- node.type == :block && node.children[0]&.type == :send &&
92
- %i[map collect].include?(node.children[0].children[1])
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 send_map?(node)
96
- node.type == :send && %i[map collect].include?(node.children[1]) &&
97
- node.children[2..].any? { |arg| symbol_to_proc_id?(arg) }
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 symbol_to_proc_id?(node)
101
- return false unless node.is_a?(Parser::AST::Node) && node.type == :block_pass
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
- node.children[0]&.type == :sym && %i[id to_i].include?(node.children[0].children[0])
104
- end
151
+ def hash_with_value?(node, &block)
152
+ return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
105
153
 
106
- def regular_pluck?(node)
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 critical_pluck?(node)
111
- node.children[2..].any? { |arg| critical_pluck_in_hash?(arg) }
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 small_collection?(node)
115
- node.children[2..].any? { |arg| small_collection_in_hash?(arg) }
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 pluck_var_in_hash?(node)
119
- return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
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 critical_pluck_in_hash?(node)
125
- return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
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
- node.children.any? { |pair| pair.type == :pair && critical_value?(pair.children[1]) }
173
+ var = find_pluck_var_in_hash(arg)
174
+ return var if var
175
+ end
176
+ nil
128
177
  end
129
178
 
130
- def small_collection_in_hash?(node)
131
- return false unless node.is_a?(Parser::AST::Node) && node.type == :hash
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
- def pluck_value?(value)
137
- value.type == :lvar && (@pluck_variables.key?(value.children[0]) || @map_id_variables.key?(value.children[0]))
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 critical_value?(value)
141
- value.type == :lvar ? @critical_pluck_variables.key?(value.children[0]) : all_pluck_call?(value)
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 small_collection_value?(value)
145
- value.type == :lvar && @small_collection_variables.key?(value.children[0])
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 add_issue(node)
149
- @issues << create_issue(
150
- file_path: @file_path,
151
- line_number: node.loc.line,
152
- message: "Using plucked array in `where` causes two queries and memory overhead",
153
- suggestion: "Use `.select(:id)` subquery instead: `Model.where(col: OtherModel.select(:id))`",
154
- severity: :warning
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
- file_path: @file_path,
161
- line_number: node.loc.line,
162
- message: "Using `.all.pluck(:id)` loads entire table into memory - highly inefficient",
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EagerEye
4
- VERSION = "1.2.0"
4
+ VERSION = "1.2.2"
5
5
  end
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.0
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-18 00:00:00.000000000 Z
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'