rubocop-rails 2.5.0 → 2.7.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +2 -2
  4. data/config/default.yml +156 -9
  5. data/lib/rubocop/cop/mixin/active_record_helper.rb +22 -0
  6. data/lib/rubocop/cop/mixin/index_method.rb +8 -1
  7. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +145 -0
  8. data/lib/rubocop/cop/rails/content_tag.rb +69 -0
  9. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +1 -3
  10. data/lib/rubocop/cop/rails/default_scope.rb +54 -0
  11. data/lib/rubocop/cop/rails/delegate.rb +2 -4
  12. data/lib/rubocop/cop/rails/dynamic_find_by.rb +40 -15
  13. data/lib/rubocop/cop/rails/exit.rb +2 -2
  14. data/lib/rubocop/cop/rails/file_path.rb +2 -1
  15. data/lib/rubocop/cop/rails/find_by_id.rb +103 -0
  16. data/lib/rubocop/cop/rails/http_positional_arguments.rb +1 -1
  17. data/lib/rubocop/cop/rails/http_status.rb +2 -0
  18. data/lib/rubocop/cop/rails/inquiry.rb +34 -0
  19. data/lib/rubocop/cop/rails/inverse_of.rb +0 -4
  20. data/lib/rubocop/cop/rails/link_to_blank.rb +3 -3
  21. data/lib/rubocop/cop/rails/mailer_name.rb +80 -0
  22. data/lib/rubocop/cop/rails/match_route.rb +119 -0
  23. data/lib/rubocop/cop/rails/negate_include.rb +39 -0
  24. data/lib/rubocop/cop/rails/pick.rb +55 -0
  25. data/lib/rubocop/cop/rails/pluck.rb +59 -0
  26. data/lib/rubocop/cop/rails/pluck_id.rb +58 -0
  27. data/lib/rubocop/cop/rails/pluck_in_where.rb +36 -0
  28. data/lib/rubocop/cop/rails/presence.rb +2 -6
  29. data/lib/rubocop/cop/rails/rake_environment.rb +17 -0
  30. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +80 -0
  31. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +0 -3
  32. data/lib/rubocop/cop/rails/render_inline.rb +40 -0
  33. data/lib/rubocop/cop/rails/render_plain_text.rb +76 -0
  34. data/lib/rubocop/cop/rails/safe_navigation.rb +1 -1
  35. data/lib/rubocop/cop/rails/save_bang.rb +6 -7
  36. data/lib/rubocop/cop/rails/short_i18n.rb +76 -0
  37. data/lib/rubocop/cop/rails/skips_model_validations.rb +46 -8
  38. data/lib/rubocop/cop/rails/time_zone.rb +1 -3
  39. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +12 -12
  40. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +28 -6
  41. data/lib/rubocop/cop/rails/unknown_env.rb +18 -6
  42. data/lib/rubocop/cop/rails/where_exists.rb +68 -0
  43. data/lib/rubocop/cop/rails_cops.rb +17 -0
  44. data/lib/rubocop/rails/schema_loader.rb +10 -10
  45. data/lib/rubocop/rails/schema_loader/schema.rb +48 -14
  46. data/lib/rubocop/rails/version.rb +1 -1
  47. metadata +27 -10
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop identifies places where `pluck` is used in `where` query methods
7
+ # and can be replaced with `select`.
8
+ #
9
+ # Since `pluck` is an eager method and hits the database immediately,
10
+ # using `select` helps to avoid additional database queries.
11
+ #
12
+ # @example
13
+ # # bad
14
+ # Post.where(user_id: User.active.pluck(:id))
15
+ #
16
+ # # good
17
+ # Post.where(user_id: User.active.select(:id))
18
+ #
19
+ class PluckInWhere < Cop
20
+ include ActiveRecordHelper
21
+
22
+ MSG = 'Use `select` instead of `pluck` within `where` query method.'
23
+
24
+ def on_send(node)
25
+ add_offense(node, location: :selector) if node.method?(:pluck) && in_where?(node)
26
+ end
27
+
28
+ def autocorrect(node)
29
+ lambda do |corrector|
30
+ corrector.replace(node.loc.selector, 'select')
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -76,15 +76,11 @@ module RuboCop
76
76
  return if ignore_if_node?(node)
77
77
 
78
78
  redundant_receiver_and_other(node) do |receiver, other|
79
- unless ignore_other_node?(other) || receiver.nil?
80
- add_offense(node, message: message(node, receiver, other))
81
- end
79
+ add_offense(node, message: message(node, receiver, other)) unless ignore_other_node?(other) || receiver.nil?
82
80
  end
83
81
 
84
82
  redundant_negative_receiver_and_other(node) do |receiver, other|
85
- unless ignore_other_node?(other) || receiver.nil?
86
- add_offense(node, message: message(node, receiver, other))
87
- end
83
+ add_offense(node, message: message(node, receiver, other)) unless ignore_other_node?(other) || receiver.nil?
88
84
  end
89
85
  end
90
86
 
@@ -41,8 +41,25 @@ module RuboCop
41
41
  end
42
42
  end
43
43
 
44
+ def autocorrect(node)
45
+ lambda do |corrector|
46
+ task_name = node.arguments[0]
47
+ task_dependency = correct_task_dependency(task_name)
48
+
49
+ corrector.replace(task_name.loc.expression, task_dependency)
50
+ end
51
+ end
52
+
44
53
  private
45
54
 
55
+ def correct_task_dependency(task_name)
56
+ if task_name.sym_type?
57
+ "#{task_name.source.delete(':|\'|"')}: :environment"
58
+ else
59
+ "#{task_name.source} => :environment"
60
+ end
61
+ end
62
+
46
63
  def task_name(node)
47
64
  first_arg = node.arguments[0]
48
65
  case first_arg&.type
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop detects cases where the `:foreign_key` option on associations
7
+ # is redundant.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # class Post
12
+ # has_many :comments, foreign_key: 'post_id'
13
+ # end
14
+ #
15
+ # class Comment
16
+ # belongs_to :post, foreign_key: 'post_id'
17
+ # end
18
+ #
19
+ # # good
20
+ # class Post
21
+ # has_many :comments
22
+ # end
23
+ #
24
+ # class Comment
25
+ # belongs_to :author, foreign_key: 'user_id'
26
+ # end
27
+ class RedundantForeignKey < Cop
28
+ include RangeHelp
29
+
30
+ MSG = 'Specifying the default value for `foreign_key` is redundant.'
31
+
32
+ def_node_matcher :association_with_foreign_key, <<~PATTERN
33
+ (send nil? ${:belongs_to :has_one :has_many :has_and_belongs_to_many} ({sym str} $_)
34
+ $(hash <$(pair (sym :foreign_key) ({sym str} $_)) ...>)
35
+ )
36
+ PATTERN
37
+
38
+ def on_send(node)
39
+ association_with_foreign_key(node) do |type, name, options, foreign_key_pair, foreign_key|
40
+ if redundant?(node, type, name, options, foreign_key)
41
+ add_offense(node, location: foreign_key_pair.loc.expression)
42
+ end
43
+ end
44
+ end
45
+
46
+ def autocorrect(node)
47
+ _type, _name, _options, foreign_key_pair, _foreign_key = association_with_foreign_key(node)
48
+ range = range_with_surrounding_space(range: foreign_key_pair.source_range, side: :left)
49
+ range = range_with_surrounding_comma(range, :left)
50
+
51
+ lambda do |corrector|
52
+ corrector.remove(range)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def redundant?(node, association_type, association_name, options, foreign_key)
59
+ foreign_key.to_s == default_foreign_key(node, association_type, association_name, options)
60
+ end
61
+
62
+ def default_foreign_key(node, association_type, association_name, options)
63
+ if association_type == :belongs_to
64
+ "#{association_name}_id"
65
+ elsif (as = find_as_option(options))
66
+ "#{as}_id"
67
+ else
68
+ node.parent_module_name&.foreign_key
69
+ end
70
+ end
71
+
72
+ def find_as_option(options)
73
+ options.pairs.find do |pair|
74
+ pair.key.sym_type? && pair.key.value == :as
75
+ end&.value&.value
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -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
@@ -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
@@ -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.
@@ -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