Skip to content

Commit 4bd8199

Browse files
committed
[Fix #603] Auto-correct with multiple validation attributes
1 parent 636e58c commit 4bd8199

File tree

3 files changed

+125
-18
lines changed

3 files changed

+125
-18
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* [#603](https://github.com/rubocop/rubocop-rails/issues/603): Fix autocorrection of multiple attributes for `Rails/RedundantPresenceValidationOnBelongsTo` cop. ([@pirj][])

lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class RedundantPresenceValidationOnBelongsTo < Base
5252
# @example source that matches - by a foreign key
5353
# validates :user_id, presence: true
5454
def_node_matcher :presence_validation?, <<~PATTERN
55-
$(
55+
(
5656
send nil? :validates
5757
(sym $_)+
5858
$(hash <$(pair (sym :presence) {true hash}) ...>)
@@ -152,27 +152,44 @@ class RedundantPresenceValidationOnBelongsTo < Base
152152
PATTERN
153153

154154
def on_send(node)
155-
validation, all_keys, options, presence = presence_validation?(node)
156-
return unless validation
155+
presence_validation?(node) do |all_keys, options, presence|
156+
keys = non_optional_belongs_to(node.parent, all_keys)
157+
return if keys.none?
157158

158-
keys = all_keys.select do |key|
159-
belongs_to = belongs_to_for(node.parent, key)
160-
belongs_to && !optional?(belongs_to)
159+
add_offense_and_correct(node, all_keys, keys, options, presence)
161160
end
162-
return if keys.none?
161+
end
163162

163+
private
164+
165+
def add_offense_and_correct(node, all_keys, keys, options, presence)
164166
add_offense(presence, message: message_for(keys)) do |corrector|
165-
remove_presence_validation(corrector, node, options, presence)
167+
if options.children.one? # `presence: true` is the only option
168+
if keys == all_keys
169+
remove_validation(corrector, node)
170+
else
171+
remove_keys_from_validation(corrector, node, keys)
172+
end
173+
elsif keys == all_keys
174+
remove_presence_option(corrector, presence)
175+
else
176+
extract_validation_for_keys(corrector, node, keys, options)
177+
end
166178
end
167179
end
168180

169-
private
170-
171181
def message_for(keys)
172182
display_keys = keys.map { |key| "`#{key}`" }.join('/')
173183
format(MSG, association: display_keys)
174184
end
175185

186+
def non_optional_belongs_to(node, keys)
187+
keys.select do |key|
188+
belongs_to = belongs_to_for(node, key)
189+
belongs_to && !optional?(belongs_to)
190+
end
191+
end
192+
176193
def belongs_to_for(model_class_node, key)
177194
if key.to_s.end_with?('_id')
178195
normalized_key = key.to_s.delete_suffix('_id').to_sym
@@ -182,17 +199,48 @@ def belongs_to_for(model_class_node, key)
182199
end
183200
end
184201

185-
def remove_presence_validation(corrector, node, options, presence)
186-
if options.children.one?
187-
corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
188-
else
189-
range = range_with_surrounding_comma(
190-
range_with_surrounding_space(range: presence.source_range, side: :left),
191-
:left
202+
def remove_validation(corrector, node)
203+
corrector.remove(validation_range(node))
204+
end
205+
206+
def remove_keys_from_validation(corrector, node, keys)
207+
keys.each do |key|
208+
key_node = node.arguments.find { |arg| arg.value == key }
209+
key_range = range_with_surrounding_space(
210+
range: range_with_surrounding_comma(key_node.source_range, :right),
211+
side: :right
192212
)
193-
corrector.remove(range)
213+
corrector.remove(key_range)
194214
end
195215
end
216+
217+
def remove_presence_option(corrector, presence)
218+
range = range_with_surrounding_comma(
219+
range_with_surrounding_space(range: presence.source_range, side: :left),
220+
:left
221+
)
222+
corrector.remove(range)
223+
end
224+
225+
def extract_validation_for_keys(corrector, node, keys, options)
226+
indentation = ' ' * node.source_range.column
227+
options_without_presence = options.children.reject { |pair| pair.key.value == :presence }
228+
source = [
229+
indentation,
230+
'validates ',
231+
keys.map(&:inspect).join(', '),
232+
', ',
233+
options_without_presence.map(&:source).join(', '),
234+
"\n"
235+
].join
236+
237+
remove_keys_from_validation(corrector, node, keys)
238+
corrector.insert_after(validation_range(node), source)
239+
end
240+
241+
def validation_range(node)
242+
range_by_whole_lines(node.source_range, include_final_newline: true)
243+
end
196244
end
197245
end
198246
end

spec/rubocop/cop/rails/redundant_presence_validation_on_belongs_to_spec.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,64 @@
115115
RUBY
116116
end
117117

118+
it 'registers an offense for multiple attributes when not all are associations' do
119+
expect_offense(<<~RUBY)
120+
belongs_to :user
121+
validates :user, :name, presence: true
122+
^^^^^^^^^^^^^^ Remove explicit presence validation for `user`.
123+
RUBY
124+
125+
expect_correction(<<~RUBY)
126+
belongs_to :user
127+
validates :name, presence: true
128+
RUBY
129+
end
130+
131+
it 'registers an offense for a secondary attribute' do
132+
expect_offense(<<~RUBY)
133+
belongs_to :user
134+
validates :name, :user, presence: true
135+
^^^^^^^^^^^^^^ Remove explicit presence validation for `user`.
136+
RUBY
137+
138+
expect_correction(<<~RUBY)
139+
belongs_to :user
140+
validates :name, presence: true
141+
RUBY
142+
end
143+
144+
it 'registers an offense for multiple attributes and options' do
145+
expect_offense(<<~RUBY)
146+
belongs_to :user
147+
validates :user, :name, presence: true, uniqueness: true
148+
^^^^^^^^^^^^^^ Remove explicit presence validation for `user`.
149+
RUBY
150+
151+
expect_correction(<<~RUBY)
152+
belongs_to :user
153+
validates :name, presence: true, uniqueness: true
154+
validates :user, uniqueness: true
155+
RUBY
156+
end
157+
158+
it 'preserves indentation for the extracted validation line' do
159+
expect_offense(<<~RUBY)
160+
class Profile
161+
belongs_to :user
162+
validates :user, :name, presence: true, uniqueness: true
163+
^^^^^^^^^^^^^^ Remove explicit presence validation for `user`.
164+
end
165+
RUBY
166+
167+
expect_correction(<<~RUBY)
168+
class Profile
169+
belongs_to :user
170+
validates :name, presence: true, uniqueness: true
171+
validates :user, uniqueness: true
172+
end
173+
RUBY
174+
end
175+
118176
it 'registers an offense for presence with a message' do
119177
expect_offense(<<~RUBY)
120178
belongs_to :user

0 commit comments

Comments
 (0)