rubocop-rails 2.9.1 → 2.11.2

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