rubocop-rails 2.5.1 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) 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 +22 -0
  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 +84 -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 +40 -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 +1 -1
  20. data/lib/rubocop/cop/rails/http_status.rb +2 -0
  21. data/lib/rubocop/cop/rails/index_by.rb +8 -0
  22. data/lib/rubocop/cop/rails/index_with.rb +8 -0
  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 +79 -0
  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 +15 -11
  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 +106 -0
  54. data/lib/rubocop/cop/rails_cops.rb +21 -0
  55. data/lib/rubocop/rails/schema_loader.rb +10 -10
  56. data/lib/rubocop/rails/schema_loader/schema.rb +4 -4
  57. data/lib/rubocop/rails/version.rb +1 -1
  58. 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 &&
@@ -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