rubocop-rails 2.9.0 → 2.11.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +4 -4
  4. data/config/default.yml +111 -5
  5. data/config/obsoletion.yml +7 -0
  6. data/lib/rubocop/cop/mixin/active_record_helper.rb +11 -0
  7. data/lib/rubocop/cop/rails/add_column_index.rb +64 -0
  8. data/lib/rubocop/cop/rails/attribute_default_block_value.rb +1 -1
  9. data/lib/rubocop/cop/rails/belongs_to.rb +1 -1
  10. data/lib/rubocop/cop/rails/blank.rb +4 -0
  11. data/lib/rubocop/cop/rails/content_tag.rb +18 -3
  12. data/lib/rubocop/cop/rails/date.rb +17 -6
  13. data/lib/rubocop/cop/rails/dynamic_find_by.rb +4 -2
  14. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +78 -0
  15. data/lib/rubocop/cop/rails/environment_comparison.rb +1 -2
  16. data/lib/rubocop/cop/rails/environment_variable_access.rb +67 -0
  17. data/lib/rubocop/cop/rails/expanded_date_range.rb +86 -0
  18. data/lib/rubocop/cop/rails/file_path.rb +2 -4
  19. data/lib/rubocop/cop/rails/find_by.rb +26 -11
  20. data/lib/rubocop/cop/rails/find_each.rb +6 -7
  21. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +34 -4
  22. data/lib/rubocop/cop/rails/helper_instance_variable.rb +27 -1
  23. data/lib/rubocop/cop/rails/http_positional_arguments.rb +8 -1
  24. data/lib/rubocop/cop/rails/http_status.rb +12 -3
  25. data/lib/rubocop/cop/rails/i18n_locale_assignment.rb +37 -0
  26. data/lib/rubocop/cop/rails/inverse_of.rb +1 -2
  27. data/lib/rubocop/cop/rails/link_to_blank.rb +5 -1
  28. data/lib/rubocop/cop/rails/reflection_class_name.rb +14 -1
  29. data/lib/rubocop/cop/rails/relative_date_constant.rb +18 -19
  30. data/lib/rubocop/cop/rails/require_dependency.rb +38 -0
  31. data/lib/rubocop/cop/rails/reversible_migration.rb +1 -1
  32. data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +75 -0
  33. data/lib/rubocop/cop/rails/safe_navigation.rb +20 -2
  34. data/lib/rubocop/cop/rails/time_zone.rb +22 -22
  35. data/lib/rubocop/cop/rails/time_zone_assignment.rb +37 -0
  36. data/lib/rubocop/cop/rails/unknown_env.rb +1 -1
  37. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +69 -0
  38. data/lib/rubocop/cop/rails/where_equals.rb +6 -2
  39. data/lib/rubocop/cop/rails/where_exists.rb +11 -0
  40. data/lib/rubocop/cop/rails/where_not.rb +5 -1
  41. data/lib/rubocop/cop/rails_cops.rb +9 -0
  42. data/lib/rubocop/rails.rb +2 -0
  43. data/lib/rubocop/rails/schema_loader/schema.rb +2 -4
  44. data/lib/rubocop/rails/version.rb +1 -1
  45. metadata +21 -11
@@ -5,7 +5,9 @@ module RuboCop
5
5
  module Rails
6
6
  # This cop looks for `has_many` or `has_one` associations that don't
7
7
  # specify a `:dependent` option.
8
- # It doesn't register an offense if `:through` option was specified.
8
+ #
9
+ # It doesn't register an offense if `:through` or `dependent: nil`
10
+ # is specified, or if the model is read-only.
9
11
  #
10
12
  # @example
11
13
  # # bad
@@ -18,8 +20,18 @@ module RuboCop
18
20
  # class User < ActiveRecord::Base
19
21
  # has_many :comments, dependent: :restrict_with_exception
20
22
  # has_one :avatar, dependent: :destroy
23
+ # has_many :articles, dependent: nil
21
24
  # has_many :patients, through: :appointments
22
25
  # end
26
+ #
27
+ # class User < ActiveRecord::Base
28
+ # has_many :comments
29
+ # has_one :avatar
30
+ #
31
+ # def readonly?
32
+ # true
33
+ # end
34
+ # end
23
35
  class HasManyOrHasOneDependent < Base
24
36
  MSG = 'Specify a `:dependent` option.'
25
37
  RESTRICT_ON_SEND = %i[has_many has_one].freeze
@@ -37,7 +49,7 @@ module RuboCop
37
49
  PATTERN
38
50
 
39
51
  def_node_matcher :dependent_option?, <<~PATTERN
40
- (pair (sym :dependent) !nil)
52
+ (pair (sym :dependent) {!nil (nil)})
41
53
  PATTERN
42
54
 
43
55
  def_node_matcher :present_option?, <<~PATTERN
@@ -51,8 +63,20 @@ module RuboCop
51
63
  (args) ...)
52
64
  PATTERN
53
65
 
66
+ def_node_matcher :association_extension_block?, <<~PATTERN
67
+ (block
68
+ (send nil? :has_many _)
69
+ (args) ...)
70
+ PATTERN
71
+
72
+ def_node_matcher :readonly?, <<~PATTERN
73
+ (def :readonly?
74
+ (args)
75
+ (true))
76
+ PATTERN
77
+
54
78
  def on_send(node)
55
- return if active_resource?(node.parent)
79
+ return if active_resource?(node.parent) || readonly_model?(node)
56
80
  return if !association_without_options?(node) && valid_options?(association_with_options?(node))
57
81
  return if valid_options_in_with_options_block?(node)
58
82
 
@@ -61,10 +85,16 @@ module RuboCop
61
85
 
62
86
  private
63
87
 
88
+ def readonly_model?(node)
89
+ return false unless (parent = node.parent)
90
+
91
+ parent.each_descendant(:def).any? { |def_node| readonly?(def_node) }
92
+ end
93
+
64
94
  def valid_options_in_with_options_block?(node)
65
95
  return true unless node.parent
66
96
 
67
- n = node.parent.begin_type? ? node.parent.parent : node.parent
97
+ n = node.parent.begin_type? || association_extension_block?(node.parent) ? node.parent.parent : node.parent
68
98
 
69
99
  contain_valid_options_in_with_options_block?(n)
70
100
  end
@@ -13,6 +13,9 @@ module RuboCop
13
13
  # variable, consider moving the behaviour elsewhere, for
14
14
  # example to a model, decorator or presenter.
15
15
  #
16
+ # Provided that a class inherits `ActionView::Helpers::FormBuilder`,
17
+ # an offense will not be registered.
18
+ #
16
19
  # @example
17
20
  # # bad
18
21
  # def welcome_message
@@ -23,18 +26,41 @@ module RuboCop
23
26
  # def welcome_message(user)
24
27
  # "Hello #{user.name}"
25
28
  # end
29
+ #
30
+ # # good
31
+ # class MyFormBuilder < ActionView::Helpers::FormBuilder
32
+ # @template.do_something
33
+ # end
26
34
  class HelperInstanceVariable < Base
27
35
  MSG = 'Do not use instance variables in helpers.'
28
36
 
37
+ def_node_matcher :form_builder_class?, <<~PATTERN
38
+ (const
39
+ (const
40
+ (const nil? :ActionView) :Helpers) :FormBuilder)
41
+ PATTERN
42
+
29
43
  def on_ivar(node)
44
+ return if inherit_form_builder?(node)
45
+
30
46
  add_offense(node)
31
47
  end
32
48
 
33
49
  def on_ivasgn(node)
34
- return if node.parent.or_asgn_type?
50
+ return if node.parent.or_asgn_type? || inherit_form_builder?(node)
35
51
 
36
52
  add_offense(node.loc.name)
37
53
  end
54
+
55
+ private
56
+
57
+ def inherit_form_builder?(node)
58
+ node.each_ancestor(:class) do |class_node|
59
+ return true if form_builder_class?(class_node.parent_class)
60
+ end
61
+
62
+ false
63
+ end
38
64
  end
39
65
  end
40
66
  end
@@ -18,6 +18,7 @@ module RuboCop
18
18
  # get :new, params: { user_id: 1 }
19
19
  # get :new, **options
20
20
  class HttpPositionalArguments < Base
21
+ include RangeHelp
21
22
  extend AutoCorrector
22
23
  extend TargetRailsVersion
23
24
 
@@ -44,7 +45,7 @@ module RuboCop
44
45
 
45
46
  message = format(MSG, verb: node.method_name)
46
47
 
47
- add_offense(node.loc.selector, message: message) do |corrector|
48
+ add_offense(highlight_range(node), message: message) do |corrector|
48
49
  # given a pre Rails 5 method: get :new, {user_id: @user.id}, {}
49
50
  #
50
51
  # @return lambda of auto correct procedure
@@ -80,6 +81,12 @@ module RuboCop
80
81
  node.sym_type? && node.value == :format
81
82
  end
82
83
 
84
+ def highlight_range(node)
85
+ _http_path, *data = *node.arguments
86
+
87
+ range_between(data.first.source_range.begin_pos, data.last.source_range.end_pos)
88
+ end
89
+
83
90
  def convert_hash_data(data, type)
84
91
  return '' if data.hash_type? && data.empty?
85
92
 
@@ -11,12 +11,14 @@ module RuboCop
11
11
  # render json: { foo: 'bar' }, status: 200
12
12
  # render plain: 'foo/bar', status: 304
13
13
  # redirect_to root_url, status: 301
14
+ # head 200
14
15
  #
15
16
  # # good
16
17
  # render :foo, status: :ok
17
18
  # render json: { foo: 'bar' }, status: :ok
18
19
  # render plain: 'foo/bar', status: :not_modified
19
20
  # redirect_to root_url, status: :moved_permanently
21
+ # head :ok
20
22
  #
21
23
  # @example EnforcedStyle: numeric
22
24
  # # bad
@@ -24,23 +26,26 @@ module RuboCop
24
26
  # render json: { foo: 'bar' }, status: :not_found
25
27
  # render plain: 'foo/bar', status: :not_modified
26
28
  # redirect_to root_url, status: :moved_permanently
29
+ # head :ok
27
30
  #
28
31
  # # good
29
32
  # render :foo, status: 200
30
33
  # render json: { foo: 'bar' }, status: 404
31
34
  # render plain: 'foo/bar', status: 304
32
35
  # redirect_to root_url, status: 301
36
+ # head 200
33
37
  #
34
38
  class HttpStatus < Base
35
39
  include ConfigurableEnforcedStyle
36
40
  extend AutoCorrector
37
41
 
38
- RESTRICT_ON_SEND = %i[render redirect_to].freeze
42
+ RESTRICT_ON_SEND = %i[render redirect_to head].freeze
39
43
 
40
44
  def_node_matcher :http_status, <<~PATTERN
41
45
  {
42
46
  (send nil? {:render :redirect_to} _ $hash)
43
47
  (send nil? {:render :redirect_to} $hash)
48
+ (send nil? :head ${int sym} ...)
44
49
  }
45
50
  PATTERN
46
51
 
@@ -49,8 +54,12 @@ module RuboCop
49
54
  PATTERN
50
55
 
51
56
  def on_send(node)
52
- http_status(node) do |hash_node|
53
- status = status_code(hash_node)
57
+ http_status(node) do |hash_node_or_status_code|
58
+ status = if hash_node_or_status_code.hash_type?
59
+ status_code(hash_node_or_status_code)
60
+ else
61
+ hash_node_or_status_code
62
+ end
54
63
  return unless status
55
64
 
56
65
  checker = checker_class.new(status)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for the use of `I18n.locale=` method.
7
+ #
8
+ # The `locale` attribute persists for the rest of the Ruby runtime, potentially causing
9
+ # unexpected behavior at a later time.
10
+ # Using `I18n.with_locale` ensures the code passed in the block is the only place `I18n.locale` is affected.
11
+ # It eliminates the possibility of a `locale` sticking around longer than intended.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # I18n.locale = :fr
16
+ #
17
+ # # good
18
+ # I18n.with_locale(:fr) do
19
+ # end
20
+ #
21
+ class I18nLocaleAssignment < Base
22
+ MSG = 'Use `I18n.with_locale` with block instead of `I18n.locale=`.'
23
+ RESTRICT_ON_SEND = %i[locale=].freeze
24
+
25
+ def_node_matcher :i18n_locale_assignment?, <<~PATTERN
26
+ (send (const {nil? cbase} :I18n) :locale= ...)
27
+ PATTERN
28
+
29
+ def on_send(node)
30
+ return unless i18n_locale_assignment?(node)
31
+
32
+ add_offense(node)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -130,8 +130,7 @@ module RuboCop
130
130
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Setting+Inverses
131
131
  class InverseOf < Base
132
132
  SPECIFY_MSG = 'Specify an `:inverse_of` option.'
133
- NIL_MSG = 'You specified `inverse_of: nil`, you probably meant to ' \
134
- 'use `inverse_of: false`.'
133
+ NIL_MSG = 'You specified `inverse_of: nil`, you probably meant to use `inverse_of: false`.'
135
134
  RESTRICT_ON_SEND = %i[has_many has_one belongs_to].freeze
136
135
 
137
136
  def_node_matcher :association_recv_arguments, <<~PATTERN
@@ -79,7 +79,11 @@ module RuboCop
79
79
  opening_quote = offence_node.children.last.source[0]
80
80
  closing_quote = opening_quote == ':' ? '' : opening_quote
81
81
  new_rel_exp = ", rel: #{opening_quote}noopener#{closing_quote}"
82
- range = send_node.arguments.last.source_range
82
+ range = if (last_argument = send_node.last_argument).hash_type?
83
+ last_argument.pairs.last.source_range
84
+ else
85
+ last_argument.source_range
86
+ end
83
87
 
84
88
  corrector.insert_after(range, new_rel_exp)
85
89
  end
@@ -5,6 +5,8 @@ module RuboCop
5
5
  module Rails
6
6
  # This cop checks if the value of the option `class_name`, in
7
7
  # the definition of a reflection is a string.
8
+ # It is marked as unsafe because it cannot be determined whether
9
+ # constant or method return value specified to `class_name` is a string.
8
10
  #
9
11
  # @example
10
12
  # # bad
@@ -16,6 +18,7 @@ module RuboCop
16
18
  class ReflectionClassName < Base
17
19
  MSG = 'Use a string value for `class_name`.'
18
20
  RESTRICT_ON_SEND = %i[has_many has_one belongs_to].freeze
21
+ ALLOWED_REFLECTION_CLASS_TYPES = %i[dstr str sym].freeze
19
22
 
20
23
  def_node_matcher :association_with_reflection, <<~PATTERN
21
24
  (send nil? {:has_many :has_one :belongs_to} _ _ ?
@@ -24,7 +27,7 @@ module RuboCop
24
27
  PATTERN
25
28
 
26
29
  def_node_matcher :reflection_class_name, <<~PATTERN
27
- (pair (sym :class_name) [!dstr !str !sym])
30
+ (pair (sym :class_name) #reflection_class_value?)
28
31
  PATTERN
29
32
 
30
33
  def on_send(node)
@@ -32,6 +35,16 @@ module RuboCop
32
35
  add_offense(reflection_class_name.loc.expression)
33
36
  end
34
37
  end
38
+
39
+ private
40
+
41
+ def reflection_class_value?(class_value)
42
+ if class_value.send_type?
43
+ !class_value.method?(:to_s) || class_value.receiver&.const_type?
44
+ else
45
+ !ALLOWED_REFLECTION_CLASS_TYPES.include?(class_value.type)
46
+ end
47
+ end
35
48
  end
36
49
  end
37
50
  end
@@ -31,11 +31,12 @@ module RuboCop
31
31
  include RangeHelp
32
32
  extend AutoCorrector
33
33
 
34
- MSG = 'Do not assign %<method_name>s to constants as it ' \
34
+ MSG = 'Do not assign `%<method_name>s` to constants as it ' \
35
35
  'will be evaluated only once.'
36
+ RELATIVE_DATE_METHODS = %i[since from_now after ago until before yesterday tomorrow].to_set.freeze
36
37
 
37
38
  def on_casgn(node)
38
- relative_date_assignment?(node) do |method_name|
39
+ nested_relative_date(node) do |method_name|
39
40
  add_offense(node, message: message(method_name)) do |corrector|
40
41
  autocorrect(corrector, node)
41
42
  end
@@ -50,7 +51,7 @@ module RuboCop
50
51
  lhs.children.zip(rhs.children).each do |(name, value)|
51
52
  next unless name.casgn_type?
52
53
 
53
- relative_date?(value) do |method_name|
54
+ nested_relative_date(value) do |method_name|
54
55
  add_offense(offense_range(name, value), message: message(method_name)) do |corrector|
55
56
  autocorrect(corrector, node)
56
57
  end
@@ -59,7 +60,7 @@ module RuboCop
59
60
  end
60
61
 
61
62
  def on_or_asgn(node)
62
- relative_date_or_assignment?(node) do |method_name|
63
+ relative_date_or_assignment(node) do |method_name|
63
64
  add_offense(node, message: format(MSG, method_name: method_name))
64
65
  end
65
66
  end
@@ -88,24 +89,22 @@ module RuboCop
88
89
  range_between(name.loc.expression.begin_pos, value.loc.expression.end_pos)
89
90
  end
90
91
 
91
- def_node_matcher :relative_date_assignment?, <<~PATTERN
92
- {
93
- (casgn _ _ (send _ ${:since :from_now :after :ago :until :before}))
94
- (casgn _ _ ({erange irange} _ (send _ ${:since :from_now :after :ago :until :before})))
95
- (casgn _ _ ({erange irange} (send _ ${:since :from_now :after :ago :until :before}) _))
96
- }
97
- PATTERN
92
+ def nested_relative_date(node, &callback)
93
+ return if node.block_type?
94
+
95
+ node.each_child_node do |child|
96
+ nested_relative_date(child, &callback)
97
+ end
98
+
99
+ relative_date(node, &callback)
100
+ end
98
101
 
99
- def_node_matcher :relative_date_or_assignment?, <<~PATTERN
100
- (:or_asgn (casgn _ _) (send _ ${:since :from_now :after :ago :until :before}))
102
+ def_node_matcher :relative_date_or_assignment, <<~PATTERN
103
+ (:or_asgn (casgn _ _) (send _ $RELATIVE_DATE_METHODS))
101
104
  PATTERN
102
105
 
103
- def_node_matcher :relative_date?, <<~PATTERN
104
- {
105
- ({erange irange} _ (send _ ${:since :from_now :after :ago :until :before}))
106
- ({erange irange} (send _ ${:since :from_now :after :ago :until :before}) _)
107
- (send _ ${:since :from_now :after :ago :until :before})
108
- }
106
+ def_node_matcher :relative_date, <<~PATTERN
107
+ (send _ $RELATIVE_DATE_METHODS)
109
108
  PATTERN
110
109
  end
111
110
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for the usage of `require_dependency`.
7
+ #
8
+ # `require_dependency` is an obsolete method for Rails applications running in Zeitwerk mode.
9
+ # In Zeitwerk mode, the semantics should match Ruby's and no need to be defensive with load order,
10
+ # just refer to classes and modules normally.
11
+ # If the constant name is dynamic, camelize if needed, and constantize.
12
+ #
13
+ # Applications running in Zeitwerk mode should not use `require_dependency`.
14
+ #
15
+ # NOTE: This cop is disabled by default. Please enable it if you are using Zeitwerk mode.
16
+ #
17
+ # @example
18
+ # # bad
19
+ # require_dependency 'some_lib'
20
+ class RequireDependency < Base
21
+ extend TargetRailsVersion
22
+
23
+ minimum_target_rails_version 6.0
24
+
25
+ MSG = 'Do not use `require_dependency` with Zeitwerk mode.'
26
+ RESTRICT_ON_SEND = %i[require_dependency].freeze
27
+
28
+ def_node_matcher :require_dependency_call?, <<~PATTERN
29
+ (send {nil? (const _ :Kernel)} :require_dependency _)
30
+ PATTERN
31
+
32
+ def on_send(node)
33
+ require_dependency_call?(node) { add_offense(node) }
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -237,7 +237,7 @@ module RuboCop
237
237
 
238
238
  def check_drop_table_node(node)
239
239
  drop_table_call(node) do
240
- unless node.parent.block_type?
240
+ unless node.parent.block_type? || node.last_argument.block_pass_type?
241
241
  add_offense(
242
242
  node,
243
243
  message: format(MSG, action: 'drop_table(without block)')
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks whether the migration implements
7
+ # either a `change` method or both an `up` and a `down`
8
+ # method.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # class SomeMigration < ActiveRecord::Migration[6.0]
13
+ # def up
14
+ # # up migration
15
+ # end
16
+ #
17
+ # # <----- missing down method
18
+ # end
19
+ #
20
+ # class SomeMigration < ActiveRecord::Migration[6.0]
21
+ # # <----- missing up method
22
+ #
23
+ # def down
24
+ # # down migration
25
+ # end
26
+ # end
27
+ #
28
+ # # good
29
+ # class SomeMigration < ActiveRecord::Migration[6.0]
30
+ # def change
31
+ # # reversible migration
32
+ # end
33
+ # end
34
+ #
35
+ # # good
36
+ # class SomeMigration < ActiveRecord::Migration[6.0]
37
+ # def up
38
+ # # up migration
39
+ # end
40
+ #
41
+ # def down
42
+ # # down migration
43
+ # end
44
+ # end
45
+ class ReversibleMigrationMethodDefinition < Base
46
+ MSG = 'Migrations must contain either a `change` method, or ' \
47
+ 'both an `up` and a `down` method.'
48
+
49
+ def_node_matcher :migration_class?, <<~PATTERN
50
+ (class
51
+ (const nil? _)
52
+ (send
53
+ (const (const {nil? cbase} :ActiveRecord) :Migration)
54
+ :[]
55
+ (float _))
56
+ _)
57
+ PATTERN
58
+
59
+ def_node_matcher :change_method?, <<~PATTERN
60
+ [ #migration_class? `(def :change (args) _) ]
61
+ PATTERN
62
+
63
+ def_node_matcher :up_and_down_methods?, <<~PATTERN
64
+ [ #migration_class? `(def :up (args) _) `(def :down (args) _) ]
65
+ PATTERN
66
+
67
+ def on_class(node)
68
+ return if change_method?(node) || up_and_down_methods?(node)
69
+
70
+ add_offense(node)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end