forked from rubocop/rubocop-rails
-
Notifications
You must be signed in to change notification settings - Fork 0
/
deprecated_active_model_errors_methods.rb
168 lines (142 loc) · 4.85 KB
/
deprecated_active_model_errors_methods.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# frozen_string_literal: true
module RuboCop
module Cop
module Rails
# Checks direct manipulation of ActiveModel#errors as hash.
# These operations are deprecated in Rails 6.1 and will not work in Rails 7.
#
# @safety
# This cop is unsafe because it can report `errors` manipulation on non-ActiveModel,
# which is obviously valid.
# The cop has no way of knowing whether a variable is an ActiveModel or not.
#
# @example
# # bad
# user.errors[:name] << 'msg'
# user.errors.messages[:name] << 'msg'
#
# # good
# user.errors.add(:name, 'msg')
#
# # bad
# user.errors[:name].clear
# user.errors.messages[:name].clear
#
# # good
# user.errors.delete(:name)
#
# # bad
# user.errors.keys.include?(:attr)
#
# # good
# user.errors.attribute_names.include?(:attr)
#
class DeprecatedActiveModelErrorsMethods < Base
include RangeHelp
extend AutoCorrector
MSG = 'Avoid manipulating ActiveModel errors as hash directly.'
AUTOCORRECTABLE_METHODS = %i[<< clear keys].freeze
INCOMPATIBLE_METHODS = %i[keys values to_h to_xml].freeze
MANIPULATIVE_METHODS = Set[
*%i[
<< append clear collect! compact! concat
delete delete_at delete_if drop drop_while fill filter! keep_if
flatten! insert map! pop prepend push reject! replace reverse!
rotate! select! shift shuffle! slice! sort! sort_by! uniq! unshift
]
].freeze
def_node_matcher :receiver_matcher_outside_model, '{send ivar lvar}'
def_node_matcher :receiver_matcher_inside_model, '{nil? send ivar lvar}'
def_node_matcher :any_manipulation?, <<~PATTERN
{
#root_manipulation?
#root_assignment?
#errors_deprecated?
#messages_details_manipulation?
#messages_details_assignment?
}
PATTERN
def_node_matcher :root_manipulation?, <<~PATTERN
(send
(send
(send #receiver_matcher :errors) :[] ...)
MANIPULATIVE_METHODS
...
)
PATTERN
def_node_matcher :root_assignment?, <<~PATTERN
(send
(send #receiver_matcher :errors)
:[]=
...)
PATTERN
def_node_matcher :errors_deprecated?, <<~PATTERN
(send
(send #receiver_matcher :errors)
{:keys :values :to_h :to_xml})
PATTERN
def_node_matcher :messages_details_manipulation?, <<~PATTERN
(send
(send
(send
(send #receiver_matcher :errors)
{:messages :details})
:[]
...)
MANIPULATIVE_METHODS
...)
PATTERN
def_node_matcher :messages_details_assignment?, <<~PATTERN
(send
(send
(send #receiver_matcher :errors)
{:messages :details})
:[]=
...)
PATTERN
def on_send(node)
any_manipulation?(node) do
next if target_rails_version <= 6.0 && INCOMPATIBLE_METHODS.include?(node.method_name)
add_offense(node) do |corrector|
next if skip_autocorrect?(node)
autocorrect(corrector, node)
end
end
end
private
def skip_autocorrect?(node)
return true unless AUTOCORRECTABLE_METHODS.include?(node.method_name)
return false unless (receiver = node.receiver.receiver)
receiver.send_type? && receiver.method?(:details) && node.method?(:<<)
end
def autocorrect(corrector, node)
receiver = node.receiver
range = offense_range(node, receiver)
replacement = replacement(node, receiver)
corrector.replace(range, replacement)
end
def offense_range(node, receiver)
receiver = receiver.receiver while receiver.send_type? && !receiver.method?(:errors) && receiver.receiver
range_between(receiver.source_range.end_pos, node.source_range.end_pos)
end
def replacement(node, receiver)
return '.attribute_names' if node.method?(:keys)
key = receiver.first_argument.source
case node.method_name
when :<<
value = node.first_argument.source
".add(#{key}, #{value})"
when :clear
".delete(#{key})"
end
end
def receiver_matcher(node)
model_file? ? receiver_matcher_inside_model(node) : receiver_matcher_outside_model(node)
end
def model_file?
processed_source.file_path.include?('/models/')
end
end
end
end
end