rubocop-rails 2.5.2 → 2.8.1

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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +2 -2
  4. data/config/default.yml +192 -11
  5. data/lib/rubocop/cop/mixin/active_record_helper.rb +9 -3
  6. data/lib/rubocop/cop/mixin/index_method.rb +25 -1
  7. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +143 -0
  8. data/lib/rubocop/cop/rails/after_commit_override.rb +91 -0
  9. data/lib/rubocop/cop/rails/content_tag.rb +69 -0
  10. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +1 -3
  11. data/lib/rubocop/cop/rails/default_scope.rb +54 -0
  12. data/lib/rubocop/cop/rails/delegate.rb +2 -4
  13. data/lib/rubocop/cop/rails/dynamic_find_by.rb +41 -15
  14. data/lib/rubocop/cop/rails/exit.rb +2 -2
  15. data/lib/rubocop/cop/rails/file_path.rb +2 -1
  16. data/lib/rubocop/cop/rails/find_by_id.rb +103 -0
  17. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +1 -5
  18. data/lib/rubocop/cop/rails/helper_instance_variable.rb +2 -0
  19. data/lib/rubocop/cop/rails/http_positional_arguments.rb +7 -1
  20. data/lib/rubocop/cop/rails/http_status.rb +2 -0
  21. data/lib/rubocop/cop/rails/index_by.rb +9 -1
  22. data/lib/rubocop/cop/rails/index_with.rb +9 -1
  23. data/lib/rubocop/cop/rails/inquiry.rb +38 -0
  24. data/lib/rubocop/cop/rails/inverse_of.rb +0 -4
  25. data/lib/rubocop/cop/rails/link_to_blank.rb +3 -3
  26. data/lib/rubocop/cop/rails/mailer_name.rb +80 -0
  27. data/lib/rubocop/cop/rails/match_route.rb +119 -0
  28. data/lib/rubocop/cop/rails/negate_include.rb +39 -0
  29. data/lib/rubocop/cop/rails/order_by_id.rb +53 -0
  30. data/lib/rubocop/cop/rails/pick.rb +55 -0
  31. data/lib/rubocop/cop/rails/pluck.rb +59 -0
  32. data/lib/rubocop/cop/rails/pluck_id.rb +58 -0
  33. data/lib/rubocop/cop/rails/pluck_in_where.rb +70 -0
  34. data/lib/rubocop/cop/rails/presence.rb +2 -6
  35. data/lib/rubocop/cop/rails/rake_environment.rb +17 -0
  36. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +80 -0
  37. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +0 -3
  38. data/lib/rubocop/cop/rails/reflection_class_name.rb +1 -1
  39. data/lib/rubocop/cop/rails/relative_date_constant.rb +5 -2
  40. data/lib/rubocop/cop/rails/render_inline.rb +40 -0
  41. data/lib/rubocop/cop/rails/render_plain_text.rb +76 -0
  42. data/lib/rubocop/cop/rails/reversible_migration.rb +80 -1
  43. data/lib/rubocop/cop/rails/safe_navigation.rb +1 -1
  44. data/lib/rubocop/cop/rails/save_bang.rb +8 -9
  45. data/lib/rubocop/cop/rails/short_i18n.rb +76 -0
  46. data/lib/rubocop/cop/rails/skips_model_validations.rb +46 -8
  47. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +83 -0
  48. data/lib/rubocop/cop/rails/time_zone.rb +1 -3
  49. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +14 -12
  50. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +16 -6
  51. data/lib/rubocop/cop/rails/unknown_env.rb +18 -6
  52. data/lib/rubocop/cop/rails/where_exists.rb +131 -0
  53. data/lib/rubocop/cop/rails/where_not.rb +108 -0
  54. data/lib/rubocop/cop/rails_cops.rb +21 -0
  55. data/lib/rubocop/rails/schema_loader/schema.rb +4 -4
  56. data/lib/rubocop/rails/version.rb +1 -1
  57. metadata +31 -10
@@ -55,11 +55,8 @@ module RuboCop
55
55
  # end
56
56
  # end
57
57
  class RedundantReceiverInWithOptions < Cop
58
- extend TargetRailsVersion
59
58
  include RangeHelp
60
59
 
61
- minimum_target_rails_version 4.2
62
-
63
60
  MSG = 'Redundant receiver in `with_options`.'
64
61
 
65
62
  def_node_matcher :with_options?, <<~PATTERN
@@ -17,7 +17,7 @@ module RuboCop
17
17
  MSG = 'Use a string value for `class_name`.'
18
18
 
19
19
  def_node_matcher :association_with_reflection, <<~PATTERN
20
- (send nil? {:has_many :has_one :belongs_to} _
20
+ (send nil? {:has_many :has_one :belongs_to} _ _ ?
21
21
  (hash <$#reflection_class_name ...>)
22
22
  )
23
23
  PATTERN
@@ -49,8 +49,7 @@ module RuboCop
49
49
 
50
50
  relative_date?(value) do |method_name|
51
51
  add_offense(node,
52
- location: range_between(name.loc.expression.begin_pos,
53
- value.loc.expression.end_pos),
52
+ location: offense_range(name, value),
54
53
  message: format(MSG, method_name: method_name))
55
54
  end
56
55
  end
@@ -77,6 +76,10 @@ module RuboCop
77
76
 
78
77
  private
79
78
 
79
+ def offense_range(name, value)
80
+ range_between(name.loc.expression.begin_pos, value.loc.expression.end_pos)
81
+ end
82
+
80
83
  def_node_matcher :relative_date_assignment?, <<~PATTERN
81
84
  {
82
85
  (casgn _ _ (send _ ${:since :from_now :after :ago :until :before}))
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop looks for inline rendering within controller actions.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # class ProductsController < ApplicationController
11
+ # def index
12
+ # render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>", type: :erb
13
+ # end
14
+ # end
15
+ #
16
+ # # good
17
+ # # app/views/products/index.html.erb
18
+ # # <% products.each do |p| %>
19
+ # # <p><%= p.name %></p>
20
+ # # <% end %>
21
+ #
22
+ # class ProductsController < ApplicationController
23
+ # def index
24
+ # end
25
+ # end
26
+ #
27
+ class RenderInline < Cop
28
+ MSG = 'Prefer using a template over inline rendering.'
29
+
30
+ def_node_matcher :render_with_inline_option?, <<~PATTERN
31
+ (send nil? :render (hash <(pair {(sym :inline) (str "inline")} _) ...>))
32
+ PATTERN
33
+
34
+ def on_send(node)
35
+ add_offense(node) if render_with_inline_option?(node)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop identifies places where `render text:` can be
7
+ # replaced with `render plain:`.
8
+ #
9
+ # @example
10
+ # # bad - explicit MIME type to `text/plain`
11
+ # render text: 'Ruby!', content_type: 'text/plain'
12
+ #
13
+ # # good - short and precise
14
+ # render plain: 'Ruby!'
15
+ #
16
+ # # good - explicit MIME type not to `text/plain`
17
+ # render text: 'Ruby!', content_type: 'text/html'
18
+ #
19
+ # @example ContentTypeCompatibility: true (default)
20
+ # # good - sets MIME type to `text/html`
21
+ # render text: 'Ruby!'
22
+ #
23
+ # @example ContentTypeCompatibility: false
24
+ # # bad - sets MIME type to `text/html`
25
+ # render text: 'Ruby!'
26
+ #
27
+ class RenderPlainText < Cop
28
+ MSG = 'Prefer `render plain:` over `render text:`.'
29
+
30
+ def_node_matcher :render_plain_text?, <<~PATTERN
31
+ (send nil? :render $(hash <$(pair (sym :text) $_) ...>))
32
+ PATTERN
33
+
34
+ def on_send(node)
35
+ render_plain_text?(node) do |options_node, _option_node, _option_value|
36
+ content_type_node = find_content_type(options_node)
37
+ add_offense(node) if compatible_content_type?(content_type_node)
38
+ end
39
+ end
40
+
41
+ def autocorrect(node)
42
+ render_plain_text?(node) do |options_node, option_node, option_value|
43
+ content_type_node = find_content_type(options_node)
44
+ rest_options = options_node.pairs - [option_node, content_type_node].compact
45
+
46
+ lambda do |corrector|
47
+ corrector.replace(
48
+ node,
49
+ replacement(rest_options, option_value)
50
+ )
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def find_content_type(node)
58
+ node.pairs.find { |p| p.key.value.to_sym == :content_type }
59
+ end
60
+
61
+ def compatible_content_type?(node)
62
+ (node && node.value.value == 'text/plain') ||
63
+ (!node && !cop_config['ContentTypeCompatibility'])
64
+ end
65
+
66
+ def replacement(rest_options, option_value)
67
+ if rest_options.any?
68
+ "render plain: #{option_value.source}, #{rest_options.map(&:source).join(', ')}"
69
+ else
70
+ "render plain: #{option_value.source}"
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -129,6 +129,51 @@ module RuboCop
129
129
  # end
130
130
  # end
131
131
  #
132
+ # @example
133
+ # # remove_columns
134
+ #
135
+ # # bad
136
+ # def change
137
+ # remove_columns :users, :name, :email
138
+ # end
139
+ #
140
+ # # good
141
+ # def change
142
+ # reversible do |dir|
143
+ # dir.up do
144
+ # remove_columns :users, :name, :email
145
+ # end
146
+ #
147
+ # dir.down do
148
+ # add_column :users, :name, :string
149
+ # add_column :users, :email, :string
150
+ # end
151
+ # end
152
+ # end
153
+ #
154
+ # # good (Rails >= 6.1, see https://github.com/rails/rails/pull/36589)
155
+ # def change
156
+ # remove_columns :users, :name, :email, type: :string
157
+ # end
158
+ #
159
+ # @example
160
+ # # remove_index
161
+ #
162
+ # # bad
163
+ # def change
164
+ # remove_index :users, name: :index_users_on_email
165
+ # end
166
+ #
167
+ # # good
168
+ # def change
169
+ # remove_index :users, :email
170
+ # end
171
+ #
172
+ # # good
173
+ # def change
174
+ # remove_index :users, column: :email
175
+ # end
176
+ #
132
177
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
133
178
  class ReversibleMigration < Cop
134
179
  MSG = '%<action>s is not reversible.'
@@ -153,6 +198,14 @@ module RuboCop
153
198
  (send nil? :change_table $_ ...)
154
199
  PATTERN
155
200
 
201
+ def_node_matcher :remove_columns_call, <<~PATTERN
202
+ (send nil? :remove_columns ... $_)
203
+ PATTERN
204
+
205
+ def_node_matcher :remove_index_call, <<~PATTERN
206
+ (send nil? :remove_index _ $_)
207
+ PATTERN
208
+
156
209
  def on_send(node)
157
210
  return unless within_change_method?(node)
158
211
  return if within_reversible_or_up_only_block?(node)
@@ -162,6 +215,8 @@ module RuboCop
162
215
  check_reversible_hash_node(node)
163
216
  check_remove_column_node(node)
164
217
  check_remove_foreign_key_node(node)
218
+ check_remove_columns_node(node)
219
+ check_remove_index_node(node)
165
220
  end
166
221
 
167
222
  def on_block(node)
@@ -237,6 +292,30 @@ module RuboCop
237
292
  end
238
293
  end
239
294
 
295
+ def check_remove_columns_node(node)
296
+ remove_columns_call(node) do |args|
297
+ unless all_hash_key?(args, :type) && target_rails_version >= 6.1
298
+ action = target_rails_version >= 6.1 ? 'remove_columns(without type)' : 'remove_columns'
299
+
300
+ add_offense(
301
+ node,
302
+ message: format(MSG, action: action)
303
+ )
304
+ end
305
+ end
306
+ end
307
+
308
+ def check_remove_index_node(node)
309
+ remove_index_call(node) do |args|
310
+ if args.hash_type? && !all_hash_key?(args, :column)
311
+ add_offense(
312
+ node,
313
+ message: format(MSG, action: 'remove_index(without column)')
314
+ )
315
+ end
316
+ end
317
+ end
318
+
240
319
  def check_change_table_offense(receiver, node)
241
320
  method_name = node.method_name
242
321
  return if receiver != node.receiver &&
@@ -281,7 +360,7 @@ module RuboCop
281
360
  key.children.first.to_sym
282
361
  end
283
362
 
284
- hash_keys & keys == keys
363
+ (hash_keys & keys).sort == keys
285
364
  end
286
365
  end
287
366
  end
@@ -48,7 +48,7 @@ module RuboCop
48
48
  def on_send(node)
49
49
  try_call(node) do |try_method, dispatch|
50
50
  return if try_method == :try && !cop_config['ConvertTry']
51
- return unless dispatch.sym_type? && dispatch.value =~ /\w+[=!?]?/
51
+ return unless dispatch.sym_type? && dispatch.value.match?(/\w+[=!?]?/)
52
52
 
53
53
  add_offense(node, message: format(MSG, try: try_method))
54
54
  end
@@ -9,14 +9,14 @@ module RuboCop
9
9
  #
10
10
  # This will allow:
11
11
  #
12
- # - update or save calls, assigned to a variable,
12
+ # * update or save calls, assigned to a variable,
13
13
  # or used as a condition in an if/unless/case statement.
14
- # - create calls, assigned to a variable that then has a
14
+ # * create calls, assigned to a variable that then has a
15
15
  # call to `persisted?`, or whose return value is checked by
16
16
  # `persisted?` immediately
17
- # - calls if the result is explicitly returned from methods and blocks,
17
+ # * calls if the result is explicitly returned from methods and blocks,
18
18
  # or provided as arguments.
19
- # - calls whose signature doesn't look like an ActiveRecord
19
+ # * calls whose signature doesn't look like an ActiveRecord
20
20
  # persistence method.
21
21
  #
22
22
  # By default it will also allow implicit returns from methods and blocks.
@@ -138,7 +138,7 @@ module RuboCop
138
138
  add_offense_for_node(node, CREATE_MSG)
139
139
  end
140
140
 
141
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
141
+ # rubocop:disable Metrics/CyclomaticComplexity
142
142
  def on_send(node)
143
143
  return unless persist_method?(node)
144
144
  return if return_value_assigned?(node)
@@ -150,7 +150,7 @@ module RuboCop
150
150
 
151
151
  add_offense_for_node(node)
152
152
  end
153
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
153
+ # rubocop:enable Metrics/CyclomaticComplexity
154
154
  alias on_csend on_send
155
155
 
156
156
  def autocorrect(node)
@@ -218,9 +218,7 @@ module RuboCop
218
218
  def check_used_in_condition_or_compound_boolean(node)
219
219
  return false unless in_condition_or_compound_boolean?(node)
220
220
 
221
- unless MODIFY_PERSIST_METHODS.include?(node.method_name)
222
- add_offense_for_node(node, CREATE_CONDITIONAL_MSG)
223
- end
221
+ add_offense_for_node(node, CREATE_CONDITIONAL_MSG) unless MODIFY_PERSIST_METHODS.include?(node.method_name)
224
222
 
225
223
  true
226
224
  end
@@ -248,6 +246,7 @@ module RuboCop
248
246
 
249
247
  def allowed_receiver?(node)
250
248
  return false unless node.receiver
249
+ return true if node.receiver.const_name == 'ENV'
251
250
  return false unless cop_config['AllowedReceivers']
252
251
 
253
252
  cop_config['AllowedReceivers'].any? do |allowed_receiver|
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop enforces that short forms of `I18n` methods are used:
7
+ # `t` instead of `translate` and `l` instead of `localize`.
8
+ #
9
+ # This cop has two different enforcement modes. When the EnforcedStyle
10
+ # is conservative (the default) then only `I18n.translate` and `I18n.localize`
11
+ # calls are added as offenses.
12
+ #
13
+ # When the EnforcedStyle is aggressive then all `translate` and `localize` calls
14
+ # without a receiver are added as offenses.
15
+ #
16
+ # @example
17
+ # # bad
18
+ # I18n.translate :key
19
+ # I18n.localize Time.now
20
+ #
21
+ # # good
22
+ # I18n.t :key
23
+ # I18n.l Time.now
24
+ #
25
+ # @example EnforcedStyle: conservative (default)
26
+ # # good
27
+ # translate :key
28
+ # localize Time.now
29
+ # t :key
30
+ # l Time.now
31
+ #
32
+ # @example EnforcedStyle: aggressive
33
+ # # bad
34
+ # translate :key
35
+ # localize Time.now
36
+ #
37
+ # # good
38
+ # t :key
39
+ # l Time.now
40
+ #
41
+ class ShortI18n < Cop
42
+ include ConfigurableEnforcedStyle
43
+
44
+ MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
45
+
46
+ PREFERRED_METHODS = {
47
+ translate: :t,
48
+ localize: :l
49
+ }.freeze
50
+
51
+ def_node_matcher :long_i18n?, <<~PATTERN
52
+ (send {nil? (const nil? :I18n)} ${:translate :localize} ...)
53
+ PATTERN
54
+
55
+ def on_send(node)
56
+ return if style == :conservative && !node.receiver
57
+
58
+ long_i18n?(node) do |method_name|
59
+ good_method = PREFERRED_METHODS[method_name]
60
+ message = format(MSG, good_method: good_method, bad_method: method_name)
61
+
62
+ add_offense(node, location: :selector, message: message)
63
+ end
64
+ end
65
+
66
+ def autocorrect(node)
67
+ long_i18n?(node) do |method_name|
68
+ lambda do |corrector|
69
+ corrector.replace(node.loc.selector, PREFERRED_METHODS[method_name])
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -42,27 +42,50 @@ module RuboCop
42
42
  decrement_counter
43
43
  increment!
44
44
  increment_counter
45
+ insert
46
+ insert!
47
+ insert_all
48
+ insert_all!
45
49
  toggle!
46
50
  update_all
47
51
  update_attribute
48
52
  update_column
49
53
  update_columns
50
- update_counters].freeze
54
+ update_counters
55
+ upsert
56
+ upsert_all].freeze
51
57
 
52
58
  def_node_matcher :good_touch?, <<~PATTERN
53
- (send (const nil? :FileUtils) :touch ...)
59
+ {
60
+ (send (const nil? :FileUtils) :touch ...)
61
+ (send _ :touch {true false})
62
+ }
63
+ PATTERN
64
+
65
+ def_node_matcher :good_insert?, <<~PATTERN
66
+ (send _ {:insert :insert!} _ {
67
+ !(hash ...)
68
+ (hash <(pair (sym !{:returning :unique_by}) _) ...>)
69
+ } ...)
54
70
  PATTERN
55
71
 
56
72
  def on_send(node)
57
- return if whitelist.include?(node.method_name.to_s)
58
- return unless blacklist.include?(node.method_name.to_s)
73
+ return if allowed_methods.include?(node.method_name.to_s)
74
+ return unless forbidden_methods.include?(node.method_name.to_s)
59
75
  return if allowed_method?(node)
60
76
  return if good_touch?(node)
77
+ return if good_insert?(node)
61
78
 
62
79
  add_offense(node, location: :selector)
63
80
  end
64
81
  alias on_csend on_send
65
82
 
83
+ def initialize(*)
84
+ super
85
+ @displayed_allowed_warning = false
86
+ @displayed_forbidden_warning = false
87
+ end
88
+
66
89
  private
67
90
 
68
91
  def message(node)
@@ -74,12 +97,27 @@ module RuboCop
74
97
  !node.arguments?
75
98
  end
76
99
 
77
- def blacklist
78
- cop_config['Blacklist'] || []
100
+ def forbidden_methods
101
+ obsolete_result = cop_config['Blacklist']
102
+ if obsolete_result
103
+ warn '`Blacklist` has been renamed to `ForbiddenMethods`.' unless @displayed_forbidden_warning
104
+ @displayed_forbidden_warning = true
105
+ return obsolete_result
106
+ end
107
+
108
+ cop_config['ForbiddenMethods'] || []
79
109
  end
80
110
 
81
- def whitelist
82
- cop_config['Whitelist'] || []
111
+ def allowed_methods
112
+ obsolete_result = cop_config['Whitelist']
113
+ if obsolete_result
114
+ warn '`Whitelist` has been renamed to `AllowedMethods`.' unless @displayed_allowed_warning
115
+ @displayed_allowed_warning = true
116
+
117
+ return obsolete_result
118
+ end
119
+
120
+ cop_config['AllowedMethods'] || []
83
121
  end
84
122
  end
85
123
  end