rubocop-rails 2.9.1 → 2.11.2

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 (44) 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/bulk_change_table.rb +5 -1
  12. data/lib/rubocop/cop/rails/content_tag.rb +18 -3
  13. data/lib/rubocop/cop/rails/date.rb +17 -6
  14. data/lib/rubocop/cop/rails/dynamic_find_by.rb +4 -2
  15. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +78 -0
  16. data/lib/rubocop/cop/rails/environment_comparison.rb +1 -2
  17. data/lib/rubocop/cop/rails/environment_variable_access.rb +67 -0
  18. data/lib/rubocop/cop/rails/expanded_date_range.rb +86 -0
  19. data/lib/rubocop/cop/rails/file_path.rb +2 -4
  20. data/lib/rubocop/cop/rails/find_by.rb +31 -12
  21. data/lib/rubocop/cop/rails/find_each.rb +2 -0
  22. data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +34 -4
  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_exists.rb +11 -0
  39. data/lib/rubocop/cop/rails/where_not.rb +5 -1
  40. data/lib/rubocop/cop/rails_cops.rb +9 -0
  41. data/lib/rubocop/rails.rb +2 -0
  42. data/lib/rubocop/rails/schema_loader/schema.rb +2 -4
  43. data/lib/rubocop/rails/version.rb +1 -1
  44. metadata +21 -11
@@ -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
@@ -44,9 +44,22 @@ module RuboCop
44
44
  RESTRICT_ON_SEND = %i[try try!].freeze
45
45
 
46
46
  def_node_matcher :try_call, <<~PATTERN
47
- (send !nil? ${:try :try!} $_ ...)
47
+ (send _ ${:try :try!} $_ ...)
48
48
  PATTERN
49
49
 
50
+ # Monkey patching for `Style/RedundantSelf` of RuboCop core.
51
+ # rubocop:disable Style/ClassAndModuleChildren
52
+ class Style::RedundantSelf
53
+ def self.autocorrect_incompatible_with
54
+ [Rails::SafeNavigation]
55
+ end
56
+ end
57
+ # rubocop:enable Style/ClassAndModuleChildren
58
+
59
+ def self.autocorrect_incompatible_with
60
+ [Style::RedundantSelf]
61
+ end
62
+
50
63
  def on_send(node)
51
64
  try_call(node) do |try_method, dispatch|
52
65
  return if try_method == :try && !cop_config['ConvertTry']
@@ -64,7 +77,12 @@ module RuboCop
64
77
  method_node, *params = *node.arguments
65
78
  method = method_node.source[1..-1]
66
79
 
67
- range = range_between(node.loc.dot.begin_pos, node.loc.expression.end_pos)
80
+ range = if node.receiver
81
+ range_between(node.loc.dot.begin_pos, node.loc.expression.end_pos)
82
+ else
83
+ corrector.insert_before(node, 'self')
84
+ node
85
+ end
68
86
 
69
87
  corrector.replace(range, replacement(method, params))
70
88
  end
@@ -8,40 +8,33 @@ module RuboCop
8
8
  # Built on top of Ruby on Rails style guide (https://rails.rubystyle.guide#time)
9
9
  # and the article http://danilenko.org/2012/7/6/rails_timezones/
10
10
  #
11
- # Two styles are supported for this cop. When EnforcedStyle is 'strict'
12
- # then only use of Time.zone is allowed.
11
+ # Two styles are supported for this cop. When `EnforcedStyle` is 'strict'
12
+ # then only use of `Time.zone` is allowed.
13
13
  #
14
14
  # When EnforcedStyle is 'flexible' then it's also allowed
15
- # to use Time.in_time_zone.
16
- #
17
- # @example EnforcedStyle: strict
18
- # # `strict` means that `Time` should be used with `zone`.
15
+ # to use `Time#in_time_zone`.
19
16
  #
17
+ # @example
20
18
  # # bad
21
19
  # Time.now
22
- # Time.parse('2015-03-02 19:05:37')
23
- #
24
- # # bad
25
- # Time.current
26
- # Time.at(timestamp).in_time_zone
20
+ # Time.parse('2015-03-02T19:05:37')
27
21
  #
28
22
  # # good
23
+ # Time.current
29
24
  # Time.zone.now
30
- # Time.zone.parse('2015-03-02 19:05:37')
25
+ # Time.zone.parse('2015-03-02T19:05:37')
26
+ # Time.zone.parse('2015-03-02T19:05:37Z') # Respect ISO 8601 format with timezone specifier.
31
27
  #
32
- # @example EnforcedStyle: flexible (default)
33
- # # `flexible` allows usage of `in_time_zone` instead of `zone`.
28
+ # @example EnforcedStyle: strict
29
+ # # `strict` means that `Time` should be used with `zone`.
34
30
  #
35
31
  # # bad
36
- # Time.now
37
- # Time.parse('2015-03-02 19:05:37')
32
+ # Time.at(timestamp).in_time_zone
38
33
  #
39
- # # good
40
- # Time.zone.now
41
- # Time.zone.parse('2015-03-02 19:05:37')
34
+ # @example EnforcedStyle: flexible (default)
35
+ # # `flexible` allows usage of `in_time_zone` instead of `zone`.
42
36
  #
43
37
  # # good
44
- # Time.current
45
38
  # Time.at(timestamp).in_time_zone
46
39
  class TimeZone < Base
47
40
  include ConfigurableEnforcedStyle
@@ -58,11 +51,13 @@ module RuboCop
58
51
 
59
52
  GOOD_METHODS = %i[zone zone_default find_zone find_zone!].freeze
60
53
 
61
- DANGEROUS_METHODS = %i[now local new parse at current].freeze
54
+ DANGEROUS_METHODS = %i[now local new parse at].freeze
62
55
 
63
56
  ACCEPTED_METHODS = %i[in_time_zone utc getlocal xmlschema iso8601
64
57
  jisx0301 rfc3339 httpdate to_i to_f].freeze
65
58
 
59
+ TIMEZONE_SPECIFIER = /[A-z]/.freeze
60
+
66
61
  def on_const(node)
67
62
  mod, klass = *node
68
63
  # we should only check core classes
@@ -116,9 +111,10 @@ module RuboCop
116
111
  end
117
112
 
118
113
  def check_time_node(klass, node)
114
+ return if attach_timezone_specifier?(node.first_argument)
115
+
119
116
  chain = extract_method_chain(node)
120
117
  return if not_danger_chain?(chain)
121
-
122
118
  return check_localtime(node) if need_check_localtime?(chain)
123
119
 
124
120
  method_name = (chain & DANGEROUS_METHODS).join('.')
@@ -132,6 +128,10 @@ module RuboCop
132
128
  end
133
129
  end
134
130
 
131
+ def attach_timezone_specifier?(date)
132
+ date.respond_to?(:value) && TIMEZONE_SPECIFIER.match?(date.value.to_s[-1])
133
+ end
134
+
135
135
  def build_message(klass, method_name, node)
136
136
  if flexible?
137
137
  format(