@@ -32,7 +32,7 @@ class RedundantPresenceValidationOnBelongsTo < Base
32
32
extend AutoCorrector
33
33
extend TargetRailsVersion
34
34
35
- MSG = 'Remove explicit presence validation for ` %<association>s` .'
35
+ MSG = 'Remove explicit presence validation for %<association>s.'
36
36
RESTRICT_ON_SEND = %i[ validates ] . freeze
37
37
38
38
minimum_target_rails_version 5.0
@@ -43,28 +43,30 @@ class RedundantPresenceValidationOnBelongsTo < Base
43
43
# @example source that matches - by association
44
44
# validates :user, presence: true
45
45
#
46
+ # @example source that matches - by association
47
+ # validates :name, :user, presence: true
48
+ #
46
49
# @example source that matches - with presence options
47
50
# validates :user, presence: { message: 'duplicate' }
48
51
#
49
52
# @example source that matches - by a foreign key
50
53
# validates :user_id, presence: true
51
54
def_node_matcher :presence_validation? , <<~PATTERN
52
- $ (
55
+ (
53
56
send nil? :validates
54
- (sym $_)
55
- ...
57
+ (sym $_)+
56
58
$(hash <$(pair (sym :presence) {true hash}) ...>)
57
59
)
58
60
PATTERN
59
61
60
- # @!method optional_option ?(node)
61
- # Match a `belongs_to` association with an optional option in a hash
62
+ # @!method optional ?(node)
63
+ # Match a `belongs_to` association with an optional option in a hash
62
64
def_node_matcher :optional? , <<~PATTERN
63
65
(send nil? :belongs_to _ ... #optional_option?)
64
66
PATTERN
65
67
66
68
# @!method optional_option?(node)
67
- # Match an optional option in a hash
69
+ # Match an optional option in a hash
68
70
def_node_matcher :optional_option? , <<~PATTERN
69
71
{
70
72
(hash <(pair (sym :optional) true) ...>) # optional: true
@@ -122,7 +124,7 @@ class RedundantPresenceValidationOnBelongsTo < Base
122
124
)
123
125
PATTERN
124
126
125
- # @!method belongs_to_without_fk?(node, fk )
127
+ # @!method belongs_to_without_fk?(node, key )
126
128
# Match a matching `belongs_to` association, without an explicit `foreign_key` option
127
129
#
128
130
# @param node [RuboCop::AST::Node]
@@ -150,21 +152,43 @@ class RedundantPresenceValidationOnBelongsTo < Base
150
152
PATTERN
151
153
152
154
def on_send ( node )
153
- validation , key , options , presence = presence_validation? ( node )
154
- 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?
155
158
156
- belongs_to = belongs_to_for ( node . parent , key )
157
- return unless belongs_to
158
- return if optional? ( belongs_to )
159
+ add_offense_and_correct ( node , all_keys , keys , options , presence )
160
+ end
161
+ end
159
162
160
- message = format ( MSG , association : key . to_s )
163
+ private
161
164
162
- add_offense ( presence , message : message ) do |corrector |
163
- remove_presence_validation ( corrector , node , options , presence )
165
+ def add_offense_and_correct ( node , all_keys , keys , options , presence )
166
+ add_offense ( presence , message : message_for ( keys ) ) do |corrector |
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
164
178
end
165
179
end
166
180
167
- private
181
+ def message_for ( keys )
182
+ display_keys = keys . map { |key | "`#{ key } `" } . join ( '/' )
183
+ format ( MSG , association : display_keys )
184
+ end
185
+
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
168
192
169
193
def belongs_to_for ( model_class_node , key )
170
194
if key . to_s . end_with? ( '_id' )
@@ -175,17 +199,48 @@ def belongs_to_for(model_class_node, key)
175
199
end
176
200
end
177
201
178
- def remove_presence_validation ( corrector , node , options , presence )
179
- if options . children . one?
180
- corrector . remove ( range_by_whole_lines ( node . source_range , include_final_newline : true ) )
181
- else
182
- range = range_with_surrounding_comma (
183
- range_with_surrounding_space ( range : presence . source_range , side : :left ) ,
184
- :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
185
212
)
186
- corrector . remove ( range )
213
+ corrector . remove ( key_range )
187
214
end
188
215
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
189
244
end
190
245
end
191
246
end
0 commit comments