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
@@ -12,20 +12,21 @@ module RuboCop
12
12
  # The cop also reports warnings when you are using `to_time` method,
13
13
  # because it doesn't know about Rails time zone either.
14
14
  #
15
- # Two styles are supported for this cop. When EnforcedStyle is 'strict'
15
+ # Two styles are supported for this cop. When `EnforcedStyle` is 'strict'
16
16
  # then the Date methods `today`, `current`, `yesterday`, and `tomorrow`
17
17
  # are prohibited and the usage of both `to_time`
18
18
  # and 'to_time_in_current_zone' are reported as warning.
19
19
  #
20
- # When EnforcedStyle is 'flexible' then only `Date.today` is prohibited
21
- # and only `to_time` is reported as warning.
20
+ # When `EnforcedStyle` is `flexible` then only `Date.today` is prohibited.
21
+ #
22
+ # And you can set a warning for `to_time` with `AllowToTime: false`.
23
+ # `AllowToTime` is `true` by default to prevent false positive on `DateTime` object.
22
24
  #
23
25
  # @example EnforcedStyle: strict
24
26
  # # bad
25
27
  # Date.current
26
28
  # Date.yesterday
27
29
  # Date.today
28
- # date.to_time
29
30
  #
30
31
  # # good
31
32
  # Time.zone.today
@@ -34,7 +35,6 @@ module RuboCop
34
35
  # @example EnforcedStyle: flexible (default)
35
36
  # # bad
36
37
  # Date.today
37
- # date.to_time
38
38
  #
39
39
  # # good
40
40
  # Time.zone.today
@@ -43,6 +43,13 @@ module RuboCop
43
43
  # Date.yesterday
44
44
  # date.in_time_zone
45
45
  #
46
+ # @example AllowToTime: true (default)
47
+ # # good
48
+ # date.to_time
49
+ #
50
+ # @example AllowToTime: false
51
+ # # bad
52
+ # date.to_time
46
53
  class Date < Base
47
54
  include ConfigurableEnforcedStyle
48
55
 
@@ -73,7 +80,7 @@ module RuboCop
73
80
 
74
81
  def on_send(node)
75
82
  return unless node.receiver && bad_methods.include?(node.method_name)
76
-
83
+ return if allow_to_time? && node.method?(:to_time)
77
84
  return if safe_chain?(node) || safe_to_time?(node)
78
85
 
79
86
  check_deprecated_methods(node)
@@ -139,6 +146,10 @@ module RuboCop
139
146
  end
140
147
  end
141
148
 
149
+ def allow_to_time?
150
+ cop_config.fetch('AllowToTime', true)
151
+ end
152
+
142
153
  def good_days
143
154
  style == :strict ? [] : %i[current yesterday tomorrow]
144
155
  end
@@ -32,18 +32,20 @@ module RuboCop
32
32
  # # good
33
33
  # Gem::Specification.find_by_name('backend').gem_dir
34
34
  class DynamicFindBy < Base
35
+ include ActiveRecordHelper
35
36
  extend AutoCorrector
36
37
 
37
38
  MSG = 'Use `%<static_name>s` instead of dynamic `%<method>s`.'
38
39
  METHOD_PATTERN = /^find_by_(.+?)(!)?$/.freeze
40
+ IGNORED_ARGUMENT_TYPES = %i[hash splat].freeze
39
41
 
40
42
  def on_send(node)
41
- return if allowed_invocation?(node)
43
+ return if node.receiver.nil? && !inherit_active_record_base?(node) || allowed_invocation?(node)
42
44
 
43
45
  method_name = node.method_name
44
46
  static_name = static_method_name(method_name)
45
47
  return unless static_name
46
- return if node.arguments.any?(&:splat_type?)
48
+ return if node.arguments.any? { |argument| IGNORED_ARGUMENT_TYPES.include?(argument.type) }
47
49
 
48
50
  message = format(MSG, static_name: static_name, method: method_name)
49
51
  add_offense(node, message: message) do |corrector|
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks that blocks are used for interpolated strings passed to
7
+ # `Rails.logger.debug`.
8
+ #
9
+ # By default, Rails production environments use the `:info` log level.
10
+ # At the `:info` log level, `Rails.logger.debug` statements do not result
11
+ # in log output. However, Ruby must eagerly evaluate interpolated string
12
+ # arguments passed as method arguments. Passing a block to
13
+ # `Rails.logger.debug` prevents costly evaluation of interpolated strings
14
+ # when no output would be produced anyway.
15
+ #
16
+ # @example
17
+ # #bad
18
+ # Rails.logger.debug "The time is #{Time.zone.now}."
19
+ #
20
+ # #good
21
+ # Rails.logger.debug { "The time is #{Time.zone.now}." }
22
+ #
23
+ class EagerEvaluationLogMessage < Base
24
+ extend AutoCorrector
25
+
26
+ MSG = 'Pass a block to `Rails.logger.debug`.'
27
+ RESTRICT_ON_SEND = %i[debug].freeze
28
+
29
+ def_node_matcher :interpolated_string_passed_to_debug, <<~PATTERN
30
+ (send
31
+ (send
32
+ (const {cbase nil?} :Rails)
33
+ :logger
34
+ )
35
+ :debug
36
+ $(dstr ...)
37
+ )
38
+ PATTERN
39
+
40
+ def on_send(node)
41
+ return if node.parent&.block_type?
42
+
43
+ interpolated_string_passed_to_debug(node) do |arguments|
44
+ message = format(MSG)
45
+
46
+ range = replacement_range(node)
47
+ replacement = replacement_source(node, arguments)
48
+
49
+ add_offense(range, message: message) do |corrector|
50
+ corrector.replace(range, replacement)
51
+ end
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ def replacement_range(node)
58
+ stop = node.loc.expression.end
59
+ start = node.loc.selector.end
60
+
61
+ if node.parenthesized_call?
62
+ stop.with(begin_pos: start.begin_pos)
63
+ else
64
+ stop.with(begin_pos: start.begin_pos + 1)
65
+ end
66
+ end
67
+
68
+ def replacement_source(node, arguments)
69
+ if node.parenthesized_call?
70
+ " { #{arguments.source} }"
71
+ else
72
+ "{ #{arguments.source} }"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -20,8 +20,7 @@ module RuboCop
20
20
 
21
21
  MSG = 'Favor `%<bang>sRails.env.%<env>s?` over `%<source>s`.'
22
22
 
23
- SYM_MSG = 'Do not compare `Rails.env` with a symbol, it will always ' \
24
- 'evaluate to `false`.'
23
+ SYM_MSG = 'Do not compare `Rails.env` with a symbol, it will always evaluate to `false`.'
25
24
 
26
25
  RESTRICT_ON_SEND = %i[== !=].freeze
27
26
 
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop looks for direct access to environment variables through the
7
+ # `ENV` variable within the application code. This can lead to runtime
8
+ # errors due to misconfiguration that could have been discovered at boot
9
+ # time if the environment variables were loaded as part of initialization
10
+ # and copied into the application's configuration or secrets. The cop can
11
+ # be configured to allow either reads or writes if required.
12
+ #
13
+ # @example
14
+ # # good
15
+ # Rails.application.config.foo
16
+ # Rails.application.config.x.foo.bar
17
+ # Rails.application.secrets.foo
18
+ # Rails.application.config.foo = "bar"
19
+ #
20
+ # @example AllowReads: false (default)
21
+ # # bad
22
+ # ENV["FOO"]
23
+ # ENV.fetch("FOO")
24
+ #
25
+ # @example AllowReads: true
26
+ # # good
27
+ # ENV["FOO"]
28
+ # ENV.fetch("FOO")
29
+ #
30
+ # @example AllowWrites: false (default)
31
+ # # bad
32
+ # ENV["FOO"] = "bar"
33
+ #
34
+ # @example AllowWrites: true
35
+ # # good
36
+ # ENV["FOO"] = "bar"
37
+ class EnvironmentVariableAccess < Base
38
+ READ_MSG = 'Do not read from `ENV` directly post initialization.'
39
+ WRITE_MSG = 'Do not write to `ENV` directly post initialization.'
40
+
41
+ def on_const(node)
42
+ add_offense(node, message: READ_MSG) if env_read?(node) && !allow_reads?
43
+ add_offense(node, message: WRITE_MSG) if env_write?(node) && !allow_writes?
44
+ end
45
+
46
+ def_node_search :env_read?, <<~PATTERN
47
+ ^(send (const {cbase nil?} :ENV) !:[]= ...)
48
+ PATTERN
49
+
50
+ def_node_search :env_write?, <<~PATTERN
51
+ {^(indexasgn (const {cbase nil?} :ENV) ...)
52
+ ^(send (const {cbase nil?} :ENV) :[]= ...)}
53
+ PATTERN
54
+
55
+ private
56
+
57
+ def allow_reads?
58
+ cop_config['AllowReads'] == true
59
+ end
60
+
61
+ def allow_writes?
62
+ cop_config['AllowWrites'] == true
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # This cop checks for expanded date range. It only compatible `..` range is targeted.
7
+ # Incompatible `...` range is ignored.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # date.beginning_of_day..date.end_of_day
12
+ # date.beginning_of_week..date.end_of_week
13
+ # date.beginning_of_month..date.end_of_month
14
+ # date.beginning_of_quarter..date.end_of_quarter
15
+ # date.beginning_of_year..date.end_of_year
16
+ #
17
+ # # good
18
+ # date.all_day
19
+ # date.all_week
20
+ # date.all_month
21
+ # date.all_quarter
22
+ # date.all_year
23
+ #
24
+ class ExpandedDateRange < Base
25
+ extend AutoCorrector
26
+ extend TargetRailsVersion
27
+
28
+ MSG = 'Use `%<preferred_method>s` instead.'
29
+
30
+ minimum_target_rails_version 5.1
31
+
32
+ def_node_matcher :expanded_date_range, <<~PATTERN
33
+ (irange
34
+ (send
35
+ $_ {:beginning_of_day :beginning_of_week :beginning_of_month :beginning_of_quarter :beginning_of_year})
36
+ (send
37
+ $_ {:end_of_day :end_of_week :end_of_month :end_of_quarter :end_of_year}))
38
+ PATTERN
39
+
40
+ PREFERRED_METHODS = {
41
+ beginning_of_day: 'all_day',
42
+ beginning_of_week: 'all_week',
43
+ beginning_of_month: 'all_month',
44
+ beginning_of_quarter: 'all_quarter',
45
+ beginning_of_year: 'all_year'
46
+ }.freeze
47
+
48
+ MAPPED_DATE_RANGE_METHODS = {
49
+ beginning_of_day: :end_of_day,
50
+ beginning_of_week: :end_of_week,
51
+ beginning_of_month: :end_of_month,
52
+ beginning_of_quarter: :end_of_quarter,
53
+ beginning_of_year: :end_of_year
54
+ }.freeze
55
+
56
+ def on_irange(node)
57
+ return unless expanded_date_range(node)
58
+
59
+ begin_node = node.begin
60
+ end_node = node.end
61
+ return unless same_receiver?(begin_node, end_node)
62
+
63
+ beginning_method = begin_node.method_name
64
+ end_method = end_node.method_name
65
+ return unless use_mapped_methods?(beginning_method, end_method)
66
+
67
+ preferred_method = "#{begin_node.receiver.source}.#{PREFERRED_METHODS[beginning_method]}"
68
+
69
+ add_offense(node, message: format(MSG, preferred_method: preferred_method)) do |corrector|
70
+ corrector.replace(node, preferred_method)
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def same_receiver?(begin_node, end_node)
77
+ begin_node.receiver.source == end_node.receiver.source
78
+ end
79
+
80
+ def use_mapped_methods?(beginning_method, end_method)
81
+ MAPPED_DATE_RANGE_METHODS[beginning_method] == end_method
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -29,10 +29,8 @@ module RuboCop
29
29
  include ConfigurableEnforcedStyle
30
30
  include RangeHelp
31
31
 
32
- MSG_SLASHES = 'Please use `Rails.root.join(\'path/to\')` ' \
33
- 'instead.'
34
- MSG_ARGUMENTS = 'Please use `Rails.root.join(\'path\', \'to\')` ' \
35
- 'instead.'
32
+ MSG_SLASHES = 'Prefer `Rails.root.join(\'path/to\')`.'
33
+ MSG_ARGUMENTS = 'Prefer `Rails.root.join(\'path\', \'to\')`.'
36
34
  RESTRICT_ON_SEND = %i[join].freeze
37
35
 
38
36
  def_node_matcher :file_join_nodes?, <<~PATTERN
@@ -3,16 +3,27 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Rails
6
- # This cop is used to identify usages of `where.first` and
7
- # change them to use `find_by` instead.
6
+ # This cop is used to identify usages of `where.take` and change them to use `find_by` instead.
7
+ #
8
+ # And `where(...).first` can return different results from `find_by`.
9
+ # (They order records differently, so the "first" record can be different.)
10
+ #
11
+ # If you also want to detect `where.first`, you can set `IgnoreWhereFirst` to false.
8
12
  #
9
13
  # @example
10
14
  # # bad
11
- # User.where(name: 'Bruce').first
12
15
  # User.where(name: 'Bruce').take
13
16
  #
14
17
  # # good
15
18
  # User.find_by(name: 'Bruce')
19
+ #
20
+ # @example IgnoreWhereFirst: true (default)
21
+ # # good
22
+ # User.where(name: 'Bruce').first
23
+ #
24
+ # @example IgnoreWhereFirst: false
25
+ # # bad
26
+ # User.where(name: 'Bruce').first
16
27
  class FindBy < Base
17
28
  include RangeHelp
18
29
  extend AutoCorrector
@@ -20,14 +31,11 @@ module RuboCop
20
31
  MSG = 'Use `find_by` instead of `where.%<method>s`.'
21
32
  RESTRICT_ON_SEND = %i[first take].freeze
22
33
 
23
- def_node_matcher :where_first?, <<~PATTERN
24
- (send ({send csend} _ :where ...) {:first :take})
25
- PATTERN
26
-
27
34
  def on_send(node)
28
- return unless where_first?(node)
35
+ return unless node.arguments.empty? && where_method?(node.receiver)
36
+ return if ignore_where_first? && node.method?(:first)
29
37
 
30
- range = range_between(node.receiver.loc.selector.begin_pos, node.loc.selector.end_pos)
38
+ range = offense_range(node)
31
39
 
32
40
  add_offense(range, message: format(MSG, method: node.method_name)) do |corrector|
33
41
  autocorrect(corrector, node)
@@ -37,10 +45,17 @@ module RuboCop
37
45
 
38
46
  private
39
47
 
48
+ def where_method?(receiver)
49
+ return false unless receiver
50
+
51
+ receiver.respond_to?(:method?) && receiver.method?(:where)
52
+ end
53
+
54
+ def offense_range(node)
55
+ range_between(node.receiver.loc.selector.begin_pos, node.loc.selector.end_pos)
56
+ end
57
+
40
58
  def autocorrect(corrector, node)
41
- # Don't autocorrect where(...).first, because it can return different
42
- # results from find_by. (They order records differently, so the
43
- # 'first' record can be different.)
44
59
  return if node.method?(:first)
45
60
 
46
61
  where_loc = node.receiver.loc.selector
@@ -49,6 +64,10 @@ module RuboCop
49
64
  corrector.replace(where_loc, 'find_by')
50
65
  corrector.replace(first_loc, '')
51
66
  end
67
+
68
+ def ignore_where_first?
69
+ cop_config.fetch('IgnoreWhereFirst', true)
70
+ end
52
71
  end
53
72
  end
54
73
  end
@@ -17,6 +17,7 @@ module RuboCop
17
17
  # # good
18
18
  # User.order(:foo).each
19
19
  class FindEach < Base
20
+ include ActiveRecordHelper
20
21
  extend AutoCorrector
21
22
 
22
23
  MSG = 'Use `find_each` instead of `each`.'
@@ -30,6 +31,7 @@ module RuboCop
30
31
  def on_send(node)
31
32
  return unless node.receiver&.send_type?
32
33
  return unless SCOPE_METHODS.include?(node.receiver.method_name)
34
+ return if node.receiver.receiver.nil? && !inherit_active_record_base?(node)
33
35
  return if ignored?(node)
34
36
 
35
37
  range = node.loc.selector
@@ -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