rubocop-rails 2.0.0
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 +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +73 -0
- data/bin/setup +7 -0
- data/config/default.yml +466 -0
- data/lib/rubocop-rails.rb +12 -0
- data/lib/rubocop/cop/mixin/target_rails_version.rb +16 -0
- data/lib/rubocop/cop/rails/action_filter.rb +117 -0
- data/lib/rubocop/cop/rails/active_record_aliases.rb +48 -0
- data/lib/rubocop/cop/rails/active_record_override.rb +82 -0
- data/lib/rubocop/cop/rails/active_support_aliases.rb +69 -0
- data/lib/rubocop/cop/rails/application_job.rb +40 -0
- data/lib/rubocop/cop/rails/application_record.rb +40 -0
- data/lib/rubocop/cop/rails/assert_not.rb +44 -0
- data/lib/rubocop/cop/rails/belongs_to.rb +102 -0
- data/lib/rubocop/cop/rails/blank.rb +164 -0
- data/lib/rubocop/cop/rails/bulk_change_table.rb +289 -0
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +91 -0
- data/lib/rubocop/cop/rails/date.rb +161 -0
- data/lib/rubocop/cop/rails/delegate.rb +132 -0
- data/lib/rubocop/cop/rails/delegate_allow_blank.rb +37 -0
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +91 -0
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +45 -0
- data/lib/rubocop/cop/rails/environment_comparison.rb +68 -0
- data/lib/rubocop/cop/rails/exit.rb +67 -0
- data/lib/rubocop/cop/rails/file_path.rb +108 -0
- data/lib/rubocop/cop/rails/find_by.rb +55 -0
- data/lib/rubocop/cop/rails/find_each.rb +51 -0
- data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +25 -0
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +106 -0
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +39 -0
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +117 -0
- data/lib/rubocop/cop/rails/http_status.rb +160 -0
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +94 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +246 -0
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +175 -0
- data/lib/rubocop/cop/rails/link_to_blank.rb +98 -0
- data/lib/rubocop/cop/rails/not_null_column.rb +67 -0
- data/lib/rubocop/cop/rails/output.rb +49 -0
- data/lib/rubocop/cop/rails/output_safety.rb +99 -0
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +107 -0
- data/lib/rubocop/cop/rails/presence.rb +124 -0
- data/lib/rubocop/cop/rails/present.rb +153 -0
- data/lib/rubocop/cop/rails/read_write_attribute.rb +74 -0
- data/lib/rubocop/cop/rails/redundant_allow_nil.rb +111 -0
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +136 -0
- data/lib/rubocop/cop/rails/reflection_class_name.rb +37 -0
- data/lib/rubocop/cop/rails/refute_methods.rb +76 -0
- data/lib/rubocop/cop/rails/relative_date_constant.rb +93 -0
- data/lib/rubocop/cop/rails/request_referer.rb +56 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +286 -0
- data/lib/rubocop/cop/rails/safe_navigation.rb +87 -0
- data/lib/rubocop/cop/rails/save_bang.rb +316 -0
- data/lib/rubocop/cop/rails/scope_args.rb +29 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +87 -0
- data/lib/rubocop/cop/rails/time_zone.rb +238 -0
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +105 -0
- data/lib/rubocop/cop/rails/unknown_env.rb +63 -0
- data/lib/rubocop/cop/rails/validation.rb +109 -0
- data/lib/rubocop/cop/rails_cops.rb +64 -0
- data/lib/rubocop/rails.rb +12 -0
- data/lib/rubocop/rails/inject.rb +18 -0
- data/lib/rubocop/rails/version.rb +10 -0
- metadata +143 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for consistent uses of `request.referer` or
|
7
|
+
# `request.referrer`, depending on the cop's configuration.
|
8
|
+
#
|
9
|
+
# @example EnforcedStyle: referer (default)
|
10
|
+
# # bad
|
11
|
+
# request.referrer
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
# request.referer
|
15
|
+
#
|
16
|
+
# @example EnforcedStyle: referrer
|
17
|
+
# # bad
|
18
|
+
# request.referer
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# request.referrer
|
22
|
+
class RequestReferer < Cop
|
23
|
+
include ConfigurableEnforcedStyle
|
24
|
+
|
25
|
+
MSG = 'Use `request.%<prefer>s` instead of ' \
|
26
|
+
'`request.%<current>s`.'
|
27
|
+
|
28
|
+
def_node_matcher :referer?, <<-PATTERN
|
29
|
+
(send (send nil? :request) {:referer :referrer})
|
30
|
+
PATTERN
|
31
|
+
|
32
|
+
def on_send(node)
|
33
|
+
referer?(node) do
|
34
|
+
return unless node.method?(wrong_method_name)
|
35
|
+
|
36
|
+
add_offense(node.source_range, location: node.source_range)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def autocorrect(node)
|
41
|
+
->(corrector) { corrector.replace(node, "request.#{style}") }
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def message(_node)
|
47
|
+
format(MSG, prefer: style, current: wrong_method_name)
|
48
|
+
end
|
49
|
+
|
50
|
+
def wrong_method_name
|
51
|
+
style == :referer ? :referrer : :referer
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,286 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks whether the change method of the migration file is
|
7
|
+
# reversible.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# def change
|
12
|
+
# change_table :users do |t|
|
13
|
+
# t.remove :name
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# def change
|
19
|
+
# create_table :users do |t|
|
20
|
+
# t.string :name
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
# def change
|
26
|
+
# reversible do |dir|
|
27
|
+
# change_table :users do |t|
|
28
|
+
# dir.up do
|
29
|
+
# t.column :name, :string
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# dir.down do
|
33
|
+
# t.remove :name
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @example
|
40
|
+
# # drop_table
|
41
|
+
#
|
42
|
+
# # bad
|
43
|
+
# def change
|
44
|
+
# drop_table :users
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# # good
|
48
|
+
# def change
|
49
|
+
# drop_table :users do |t|
|
50
|
+
# t.string :name
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# @example
|
55
|
+
# # change_column_default
|
56
|
+
#
|
57
|
+
# # bad
|
58
|
+
# def change
|
59
|
+
# change_column_default(:suppliers, :qualification, 'new')
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# # good
|
63
|
+
# def change
|
64
|
+
# change_column_default(:posts, :state, from: nil, to: "draft")
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# @example
|
68
|
+
# # remove_column
|
69
|
+
#
|
70
|
+
# # bad
|
71
|
+
# def change
|
72
|
+
# remove_column(:suppliers, :qualification)
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# # good
|
76
|
+
# def change
|
77
|
+
# remove_column(:suppliers, :qualification, :string)
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# @example
|
81
|
+
# # remove_foreign_key
|
82
|
+
#
|
83
|
+
# # bad
|
84
|
+
# def change
|
85
|
+
# remove_foreign_key :accounts, column: :owner_id
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# # good
|
89
|
+
# def change
|
90
|
+
# remove_foreign_key :accounts, :branches
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# @example
|
94
|
+
# # change_table
|
95
|
+
#
|
96
|
+
# # bad
|
97
|
+
# def change
|
98
|
+
# change_table :users do |t|
|
99
|
+
# t.remove :name
|
100
|
+
# t.change_default :authorized, 1
|
101
|
+
# t.change :price, :string
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# # good
|
106
|
+
# def change
|
107
|
+
# change_table :users do |t|
|
108
|
+
# t.string :name
|
109
|
+
# end
|
110
|
+
# end
|
111
|
+
#
|
112
|
+
# # good
|
113
|
+
# def change
|
114
|
+
# reversible do |dir|
|
115
|
+
# change_table :users do |t|
|
116
|
+
# dir.up do
|
117
|
+
# t.change :price, :string
|
118
|
+
# end
|
119
|
+
#
|
120
|
+
# dir.down do
|
121
|
+
# t.change :price, :integer
|
122
|
+
# end
|
123
|
+
# end
|
124
|
+
# end
|
125
|
+
# end
|
126
|
+
#
|
127
|
+
# @see https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
|
128
|
+
class ReversibleMigration < Cop
|
129
|
+
MSG = '%<action>s is not reversible.'
|
130
|
+
IRREVERSIBLE_CHANGE_TABLE_CALLS = %i[
|
131
|
+
change change_default remove
|
132
|
+
].freeze
|
133
|
+
|
134
|
+
def_node_matcher :irreversible_schema_statement_call, <<-PATTERN
|
135
|
+
(send nil? ${:change_table_comment :execute :remove_belongs_to} ...)
|
136
|
+
PATTERN
|
137
|
+
|
138
|
+
def_node_matcher :drop_table_call, <<-PATTERN
|
139
|
+
(send nil? :drop_table ...)
|
140
|
+
PATTERN
|
141
|
+
|
142
|
+
def_node_matcher :change_column_default_call, <<-PATTERN
|
143
|
+
(send nil? :change_column_default {[(sym _) (sym _)] (splat _)} $...)
|
144
|
+
PATTERN
|
145
|
+
|
146
|
+
def_node_matcher :remove_column_call, <<-PATTERN
|
147
|
+
(send nil? :remove_column $...)
|
148
|
+
PATTERN
|
149
|
+
|
150
|
+
def_node_matcher :remove_foreign_key_call, <<-PATTERN
|
151
|
+
(send nil? :remove_foreign_key _ $_)
|
152
|
+
PATTERN
|
153
|
+
|
154
|
+
def_node_matcher :change_table_call, <<-PATTERN
|
155
|
+
(send nil? :change_table $_ ...)
|
156
|
+
PATTERN
|
157
|
+
|
158
|
+
def on_send(node)
|
159
|
+
return unless within_change_method?(node)
|
160
|
+
return if within_reversible_or_up_only_block?(node)
|
161
|
+
|
162
|
+
check_irreversible_schema_statement_node(node)
|
163
|
+
check_drop_table_node(node)
|
164
|
+
check_change_column_default_node(node)
|
165
|
+
check_remove_column_node(node)
|
166
|
+
check_remove_foreign_key_node(node)
|
167
|
+
end
|
168
|
+
|
169
|
+
def on_block(node)
|
170
|
+
return unless within_change_method?(node)
|
171
|
+
return if within_reversible_or_up_only_block?(node)
|
172
|
+
return if node.body.nil?
|
173
|
+
|
174
|
+
check_change_table_node(node.send_node, node.body)
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def check_irreversible_schema_statement_node(node)
|
180
|
+
irreversible_schema_statement_call(node) do |method_name|
|
181
|
+
add_offense(node, message: format(MSG, action: method_name))
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def check_drop_table_node(node)
|
186
|
+
drop_table_call(node) do
|
187
|
+
unless node.parent.block_type?
|
188
|
+
add_offense(
|
189
|
+
node,
|
190
|
+
message: format(MSG, action: 'drop_table(without block)')
|
191
|
+
)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def check_change_column_default_node(node)
|
197
|
+
change_column_default_call(node) do |args|
|
198
|
+
unless all_hash_key?(args.last, :from, :to)
|
199
|
+
add_offense(
|
200
|
+
node,
|
201
|
+
message: format(
|
202
|
+
MSG, action: 'change_column_default(without :from and :to)'
|
203
|
+
)
|
204
|
+
)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def check_remove_column_node(node)
|
210
|
+
remove_column_call(node) do |args|
|
211
|
+
if args.to_a.size < 3
|
212
|
+
add_offense(
|
213
|
+
node,
|
214
|
+
message: format(MSG, action: 'remove_column(without type)')
|
215
|
+
)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def check_remove_foreign_key_node(node)
|
221
|
+
remove_foreign_key_call(node) do |arg|
|
222
|
+
if arg.hash_type?
|
223
|
+
add_offense(
|
224
|
+
node,
|
225
|
+
message: format(MSG,
|
226
|
+
action: 'remove_foreign_key(without table)')
|
227
|
+
)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def check_change_table_node(node, block)
|
233
|
+
change_table_call(node) do |arg|
|
234
|
+
if target_rails_version < 4.0
|
235
|
+
add_offense(
|
236
|
+
node,
|
237
|
+
message: format(MSG, action: 'change_table')
|
238
|
+
)
|
239
|
+
elsif block.send_type?
|
240
|
+
check_change_table_offense(arg, block)
|
241
|
+
else
|
242
|
+
block.each_child_node(:send) do |child_node|
|
243
|
+
check_change_table_offense(arg, child_node)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def check_change_table_offense(receiver, node)
|
250
|
+
method_name = node.method_name
|
251
|
+
return if receiver != node.receiver &&
|
252
|
+
!IRREVERSIBLE_CHANGE_TABLE_CALLS.include?(method_name)
|
253
|
+
|
254
|
+
add_offense(
|
255
|
+
node,
|
256
|
+
message: format(MSG, action: "change_table(with #{method_name})")
|
257
|
+
)
|
258
|
+
end
|
259
|
+
|
260
|
+
def within_change_method?(node)
|
261
|
+
node.each_ancestor(:def).any? do |ancestor|
|
262
|
+
ancestor.method?(:change)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def within_reversible_or_up_only_block?(node)
|
267
|
+
node.each_ancestor(:block).any? do |ancestor|
|
268
|
+
ancestor.block_type? &&
|
269
|
+
ancestor.send_node.method?(:reversible) ||
|
270
|
+
ancestor.send_node.method?(:up_only)
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def all_hash_key?(args, *keys)
|
275
|
+
return false unless args&.hash_type?
|
276
|
+
|
277
|
+
hash_keys = args.keys.map do |key|
|
278
|
+
key.children.first.to_sym
|
279
|
+
end
|
280
|
+
|
281
|
+
hash_keys & keys == keys
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop converts usages of `try!` to `&.`. It can also be configured
|
7
|
+
# to convert `try`. It will convert code to use safe navigation.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # ConvertTry: false
|
11
|
+
# # bad
|
12
|
+
# foo.try!(:bar)
|
13
|
+
# foo.try!(:bar, baz)
|
14
|
+
# foo.try!(:bar) { |e| e.baz }
|
15
|
+
#
|
16
|
+
# foo.try!(:[], 0)
|
17
|
+
#
|
18
|
+
# # good
|
19
|
+
# foo.try(:bar)
|
20
|
+
# foo.try(:bar, baz)
|
21
|
+
# foo.try(:bar) { |e| e.baz }
|
22
|
+
#
|
23
|
+
# foo&.bar
|
24
|
+
# foo&.bar(baz)
|
25
|
+
# foo&.bar { |e| e.baz }
|
26
|
+
#
|
27
|
+
#
|
28
|
+
# # ConvertTry: true
|
29
|
+
# # bad
|
30
|
+
# foo.try!(:bar)
|
31
|
+
# foo.try!(:bar, baz)
|
32
|
+
# foo.try!(:bar) { |e| e.baz }
|
33
|
+
# foo.try(:bar)
|
34
|
+
# foo.try(:bar, baz)
|
35
|
+
# foo.try(:bar) { |e| e.baz }
|
36
|
+
#
|
37
|
+
# # good
|
38
|
+
# foo&.bar
|
39
|
+
# foo&.bar(baz)
|
40
|
+
# foo&.bar { |e| e.baz }
|
41
|
+
class SafeNavigation < Cop
|
42
|
+
include RangeHelp
|
43
|
+
|
44
|
+
MSG = 'Use safe navigation (`&.`) instead of `%<try>s`.'
|
45
|
+
|
46
|
+
def_node_matcher :try_call, <<-PATTERN
|
47
|
+
(send !nil? ${:try :try!} $_ ...)
|
48
|
+
PATTERN
|
49
|
+
|
50
|
+
def on_send(node)
|
51
|
+
try_call(node) do |try_method, dispatch|
|
52
|
+
return if try_method == :try && !cop_config['ConvertTry']
|
53
|
+
return unless dispatch.sym_type? && dispatch.value =~ /\w+[=!?]?/
|
54
|
+
|
55
|
+
add_offense(node, message: format(MSG, try: try_method))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def autocorrect(node)
|
60
|
+
method_node, *params = *node.arguments
|
61
|
+
method = method_node.source[1..-1]
|
62
|
+
|
63
|
+
range = range_between(node.loc.dot.begin_pos,
|
64
|
+
node.loc.expression.end_pos)
|
65
|
+
|
66
|
+
lambda do |corrector|
|
67
|
+
corrector.replace(range, replacement(method, params))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def replacement(method, params)
|
74
|
+
new_params = params.map(&:source).join(', ')
|
75
|
+
|
76
|
+
if method.end_with?('=')
|
77
|
+
"&.#{method[0...-1]} = #{new_params}"
|
78
|
+
elsif params.empty?
|
79
|
+
"&.#{method}"
|
80
|
+
else
|
81
|
+
"&.#{method}(#{new_params})"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop identifies possible cases where Active Record save! or related
|
7
|
+
# should be used instead of save because the model might have failed to
|
8
|
+
# save and an exception is better than unhandled failure.
|
9
|
+
#
|
10
|
+
# This will allow:
|
11
|
+
# - update or save calls, assigned to a variable,
|
12
|
+
# or used as a condition in an if/unless/case statement.
|
13
|
+
# - create calls, assigned to a variable that then has a
|
14
|
+
# call to `persisted?`.
|
15
|
+
# - calls if the result is explicitly returned from methods and blocks,
|
16
|
+
# or provided as arguments.
|
17
|
+
# - calls whose signature doesn't look like an ActiveRecord
|
18
|
+
# persistence method.
|
19
|
+
#
|
20
|
+
# By default it will also allow implicit returns from methods and blocks.
|
21
|
+
# that behavior can be turned off with `AllowImplicitReturn: false`.
|
22
|
+
#
|
23
|
+
# You can permit receivers that are giving false positives with
|
24
|
+
# `AllowedReceivers: []`
|
25
|
+
#
|
26
|
+
# @example
|
27
|
+
#
|
28
|
+
# # bad
|
29
|
+
# user.save
|
30
|
+
# user.update(name: 'Joe')
|
31
|
+
# user.find_or_create_by(name: 'Joe')
|
32
|
+
# user.destroy
|
33
|
+
#
|
34
|
+
# # good
|
35
|
+
# unless user.save
|
36
|
+
# # ...
|
37
|
+
# end
|
38
|
+
# user.save!
|
39
|
+
# user.update!(name: 'Joe')
|
40
|
+
# user.find_or_create_by!(name: 'Joe')
|
41
|
+
# user.destroy!
|
42
|
+
#
|
43
|
+
# user = User.find_or_create_by(name: 'Joe')
|
44
|
+
# unless user.persisted?
|
45
|
+
# # ...
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def save_user
|
49
|
+
# return user.save
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
# @example AllowImplicitReturn: true (default)
|
53
|
+
#
|
54
|
+
# # good
|
55
|
+
# users.each { |u| u.save }
|
56
|
+
#
|
57
|
+
# def save_user
|
58
|
+
# user.save
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# @example AllowImplicitReturn: false
|
62
|
+
#
|
63
|
+
# # bad
|
64
|
+
# users.each { |u| u.save }
|
65
|
+
# def save_user
|
66
|
+
# user.save
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# # good
|
70
|
+
# users.each { |u| u.save! }
|
71
|
+
#
|
72
|
+
# def save_user
|
73
|
+
# user.save!
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# def save_user
|
77
|
+
# return user.save
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# @example AllowedReceivers: ['merchant.customers', 'Service::Mailer']
|
81
|
+
#
|
82
|
+
# # bad
|
83
|
+
# merchant.create
|
84
|
+
# customers.builder.save
|
85
|
+
# Mailer.create
|
86
|
+
#
|
87
|
+
# module Service::Mailer
|
88
|
+
# self.create
|
89
|
+
# end
|
90
|
+
#
|
91
|
+
# # good
|
92
|
+
# merchant.customers.create
|
93
|
+
# MerchantService.merchant.customers.destroy
|
94
|
+
# Service::Mailer.update(message: 'Message')
|
95
|
+
# ::Service::Mailer.update
|
96
|
+
# Services::Service::Mailer.update(message: 'Message')
|
97
|
+
# Service::Mailer::update
|
98
|
+
#
|
99
|
+
class SaveBang < Cop
|
100
|
+
include NegativeConditional
|
101
|
+
|
102
|
+
MSG = 'Use `%<prefer>s` instead of `%<current>s` if the return ' \
|
103
|
+
'value is not checked.'
|
104
|
+
CREATE_MSG = (MSG +
|
105
|
+
' Or check `persisted?` on model returned from ' \
|
106
|
+
'`%<current>s`.').freeze
|
107
|
+
CREATE_CONDITIONAL_MSG = '`%<current>s` returns a model which is ' \
|
108
|
+
'always truthy.'
|
109
|
+
|
110
|
+
CREATE_PERSIST_METHODS = %i[create
|
111
|
+
first_or_create find_or_create_by].freeze
|
112
|
+
MODIFY_PERSIST_METHODS = %i[save
|
113
|
+
update update_attributes destroy].freeze
|
114
|
+
PERSIST_METHODS = (CREATE_PERSIST_METHODS +
|
115
|
+
MODIFY_PERSIST_METHODS).freeze
|
116
|
+
|
117
|
+
def join_force?(force_class)
|
118
|
+
force_class == VariableForce
|
119
|
+
end
|
120
|
+
|
121
|
+
def after_leaving_scope(scope, _variable_table)
|
122
|
+
scope.variables.each_value do |variable|
|
123
|
+
variable.assignments.each do |assignment|
|
124
|
+
check_assignment(assignment)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def check_assignment(assignment)
|
130
|
+
node = right_assignment_node(assignment)
|
131
|
+
|
132
|
+
return unless node&.send_type?
|
133
|
+
return unless persist_method?(node, CREATE_PERSIST_METHODS)
|
134
|
+
return if persisted_referenced?(assignment)
|
135
|
+
|
136
|
+
add_offense_for_node(node, CREATE_MSG)
|
137
|
+
end
|
138
|
+
|
139
|
+
def on_send(node) # rubocop:disable Metrics/CyclomaticComplexity
|
140
|
+
return unless persist_method?(node)
|
141
|
+
return if return_value_assigned?(node)
|
142
|
+
return if check_used_in_conditional(node)
|
143
|
+
return if argument?(node)
|
144
|
+
return if implicit_return?(node)
|
145
|
+
return if explicit_return?(node)
|
146
|
+
|
147
|
+
add_offense_for_node(node)
|
148
|
+
end
|
149
|
+
alias on_csend on_send
|
150
|
+
|
151
|
+
def autocorrect(node)
|
152
|
+
save_loc = node.loc.selector
|
153
|
+
new_method = "#{node.method_name}!"
|
154
|
+
|
155
|
+
->(corrector) { corrector.replace(save_loc, new_method) }
|
156
|
+
end
|
157
|
+
|
158
|
+
private
|
159
|
+
|
160
|
+
def add_offense_for_node(node, msg = MSG)
|
161
|
+
name = node.method_name
|
162
|
+
full_message = format(msg, prefer: "#{name}!", current: name.to_s)
|
163
|
+
|
164
|
+
add_offense(node, location: :selector, message: full_message)
|
165
|
+
end
|
166
|
+
|
167
|
+
def right_assignment_node(assignment)
|
168
|
+
node = assignment.node.child_nodes.first
|
169
|
+
|
170
|
+
return node unless node&.block_type?
|
171
|
+
|
172
|
+
node.send_node
|
173
|
+
end
|
174
|
+
|
175
|
+
def persisted_referenced?(assignment)
|
176
|
+
return unless assignment.referenced?
|
177
|
+
|
178
|
+
assignment.variable.references.any? do |reference|
|
179
|
+
call_to_persisted?(reference.node.parent)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def call_to_persisted?(node)
|
184
|
+
node.send_type? && node.method?(:persisted?)
|
185
|
+
end
|
186
|
+
|
187
|
+
def assignable_node(node)
|
188
|
+
assignable = node.block_node || node
|
189
|
+
while node
|
190
|
+
node = hash_parent(node) || array_parent(node)
|
191
|
+
assignable = node if node
|
192
|
+
end
|
193
|
+
assignable
|
194
|
+
end
|
195
|
+
|
196
|
+
def hash_parent(node)
|
197
|
+
pair = node.parent
|
198
|
+
return unless pair&.pair_type?
|
199
|
+
|
200
|
+
hash = pair.parent
|
201
|
+
return unless hash&.hash_type?
|
202
|
+
|
203
|
+
hash
|
204
|
+
end
|
205
|
+
|
206
|
+
def array_parent(node)
|
207
|
+
array = node.parent
|
208
|
+
return unless array&.array_type?
|
209
|
+
|
210
|
+
array
|
211
|
+
end
|
212
|
+
|
213
|
+
def check_used_in_conditional(node)
|
214
|
+
return false unless conditional?(node)
|
215
|
+
|
216
|
+
unless MODIFY_PERSIST_METHODS.include?(node.method_name)
|
217
|
+
add_offense_for_node(node, CREATE_CONDITIONAL_MSG)
|
218
|
+
end
|
219
|
+
|
220
|
+
true
|
221
|
+
end
|
222
|
+
|
223
|
+
def conditional?(node) # rubocop:disable Metrics/CyclomaticComplexity
|
224
|
+
node = node.block_node || node
|
225
|
+
|
226
|
+
condition = node.parent
|
227
|
+
return false unless condition
|
228
|
+
|
229
|
+
condition.if_type? || condition.case_type? ||
|
230
|
+
condition.or_type? || condition.and_type? ||
|
231
|
+
single_negative?(condition)
|
232
|
+
end
|
233
|
+
|
234
|
+
def allowed_receiver?(node)
|
235
|
+
return false unless node.receiver
|
236
|
+
return false unless cop_config['AllowedReceivers']
|
237
|
+
|
238
|
+
cop_config['AllowedReceivers'].any? do |allowed_receiver|
|
239
|
+
receiver_chain_matches?(node, allowed_receiver)
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
def receiver_chain_matches?(node, allowed_receiver)
|
244
|
+
allowed_receiver.split('.').reverse.all? do |receiver_part|
|
245
|
+
node = node.receiver
|
246
|
+
return false unless node
|
247
|
+
|
248
|
+
if node.variable?
|
249
|
+
node.node_parts.first == receiver_part.to_sym
|
250
|
+
elsif node.send_type?
|
251
|
+
node.method_name == receiver_part.to_sym
|
252
|
+
elsif node.const_type?
|
253
|
+
const_matches?(node.const_name, receiver_part)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Const == Const
|
259
|
+
# ::Const == ::Const
|
260
|
+
# ::Const == Const
|
261
|
+
# Const == ::Const
|
262
|
+
# NameSpace::Const == Const
|
263
|
+
# NameSpace::Const == NameSpace::Const
|
264
|
+
# NameSpace::Const != ::Const
|
265
|
+
# Const != NameSpace::Const
|
266
|
+
def const_matches?(const, allowed_const)
|
267
|
+
parts = allowed_const.split('::').reverse.zip(
|
268
|
+
const.split('::').reverse
|
269
|
+
)
|
270
|
+
parts.all? do |(allowed_part, const_part)|
|
271
|
+
allowed_part == const_part.to_s
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def implicit_return?(node)
|
276
|
+
return false unless cop_config['AllowImplicitReturn']
|
277
|
+
|
278
|
+
node = assignable_node(node)
|
279
|
+
method = node.parent
|
280
|
+
return unless method && (method.def_type? || method.block_type?)
|
281
|
+
|
282
|
+
method.children.size == node.sibling_index + 1
|
283
|
+
end
|
284
|
+
|
285
|
+
def argument?(node)
|
286
|
+
assignable_node(node).argument?
|
287
|
+
end
|
288
|
+
|
289
|
+
def explicit_return?(node)
|
290
|
+
ret = assignable_node(node).parent
|
291
|
+
ret && (ret.return_type? || ret.next_type?)
|
292
|
+
end
|
293
|
+
|
294
|
+
def return_value_assigned?(node)
|
295
|
+
assignment = assignable_node(node).parent
|
296
|
+
assignment&.lvasgn_type?
|
297
|
+
end
|
298
|
+
|
299
|
+
def persist_method?(node, methods = PERSIST_METHODS)
|
300
|
+
methods.include?(node.method_name) &&
|
301
|
+
expected_signature?(node) &&
|
302
|
+
!allowed_receiver?(node)
|
303
|
+
end
|
304
|
+
|
305
|
+
# Check argument signature as no arguments or one hash
|
306
|
+
def expected_signature?(node)
|
307
|
+
!node.arguments? ||
|
308
|
+
(node.arguments.one? &&
|
309
|
+
node.method_name != :destroy &&
|
310
|
+
(node.first_argument.hash_type? ||
|
311
|
+
!node.first_argument.literal?))
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|