rubocop-rails 2.9.0 → 2.11.1

Sign up to get free protection for your applications and to get access to all the features.
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