rubocop-rails 2.25.1 → 2.32.0

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +60 -8
  4. data/config/default.yml +103 -51
  5. data/lib/rubocop/cop/mixin/active_record_helper.rb +2 -2
  6. data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +2 -2
  7. data/lib/rubocop/cop/mixin/database_type_resolvable.rb +2 -2
  8. data/lib/rubocop/cop/mixin/enforce_superclass.rb +6 -1
  9. data/lib/rubocop/cop/mixin/index_method.rb +69 -61
  10. data/lib/rubocop/cop/mixin/routes_helper.rb +20 -0
  11. data/lib/rubocop/cop/mixin/target_rails_version.rb +3 -5
  12. data/lib/rubocop/cop/rails/action_order.rb +1 -5
  13. data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +1 -5
  14. data/lib/rubocop/cop/rails/add_column_index.rb +1 -0
  15. data/lib/rubocop/cop/rails/application_record.rb +4 -0
  16. data/lib/rubocop/cop/rails/arel_star.rb +5 -5
  17. data/lib/rubocop/cop/rails/belongs_to.rb +1 -1
  18. data/lib/rubocop/cop/rails/blank.rb +1 -1
  19. data/lib/rubocop/cop/rails/bulk_change_table.rb +3 -2
  20. data/lib/rubocop/cop/rails/compact_blank.rb +29 -8
  21. data/lib/rubocop/cop/rails/content_tag.rb +1 -1
  22. data/lib/rubocop/cop/rails/dangerous_column_names.rb +2 -0
  23. data/lib/rubocop/cop/rails/date.rb +2 -2
  24. data/lib/rubocop/cop/rails/delegate.rb +53 -7
  25. data/lib/rubocop/cop/rails/duplicate_association.rb +8 -4
  26. data/lib/rubocop/cop/rails/eager_evaluation_log_message.rb +1 -3
  27. data/lib/rubocop/cop/rails/enum_hash.rb +31 -8
  28. data/lib/rubocop/cop/rails/enum_syntax.rb +130 -0
  29. data/lib/rubocop/cop/rails/enum_uniqueness.rb +29 -7
  30. data/lib/rubocop/cop/rails/env_local.rb +26 -3
  31. data/lib/rubocop/cop/rails/file_path.rb +62 -10
  32. data/lib/rubocop/cop/rails/http_positional_arguments.rb +7 -0
  33. data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +1 -1
  34. data/lib/rubocop/cop/rails/index_by.rb +37 -12
  35. data/lib/rubocop/cop/rails/index_with.rb +37 -12
  36. data/lib/rubocop/cop/rails/inquiry.rb +1 -1
  37. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +11 -1
  38. data/lib/rubocop/cop/rails/match_route.rb +1 -9
  39. data/lib/rubocop/cop/rails/multiple_route_paths.rb +50 -0
  40. data/lib/rubocop/cop/rails/not_null_column.rb +6 -2
  41. data/lib/rubocop/cop/rails/output.rb +1 -2
  42. data/lib/rubocop/cop/rails/pluck.rb +30 -4
  43. data/lib/rubocop/cop/rails/pluck_in_where.rb +17 -8
  44. data/lib/rubocop/cop/rails/pluralization_grammar.rb +30 -16
  45. data/lib/rubocop/cop/rails/presence.rb +1 -1
  46. data/lib/rubocop/cop/rails/present.rb +1 -3
  47. data/lib/rubocop/cop/rails/redundant_active_record_all_method.rb +1 -30
  48. data/lib/rubocop/cop/rails/redundant_foreign_key.rb +1 -1
  49. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +9 -0
  50. data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +7 -2
  51. data/lib/rubocop/cop/rails/reflection_class_name.rb +3 -3
  52. data/lib/rubocop/cop/rails/relative_date_constant.rb +1 -1
  53. data/lib/rubocop/cop/rails/render_plain_text.rb +6 -3
  54. data/lib/rubocop/cop/rails/request_referer.rb +1 -1
  55. data/lib/rubocop/cop/rails/reversible_migration.rb +4 -1
  56. data/lib/rubocop/cop/rails/root_pathname_methods.rb +21 -12
  57. data/lib/rubocop/cop/rails/save_bang.rb +8 -7
  58. data/lib/rubocop/cop/rails/schema_comment.rb +2 -1
  59. data/lib/rubocop/cop/rails/select_map.rb +3 -2
  60. data/lib/rubocop/cop/rails/skips_model_validations.rb +5 -3
  61. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +1 -1
  62. data/lib/rubocop/cop/rails/strip_heredoc.rb +1 -1
  63. data/lib/rubocop/cop/rails/strong_parameters_expect.rb +104 -0
  64. data/lib/rubocop/cop/rails/three_state_boolean_column.rb +3 -2
  65. data/lib/rubocop/cop/rails/time_zone.rb +16 -7
  66. data/lib/rubocop/cop/rails/transaction_exit_statement.rb +7 -2
  67. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +10 -33
  68. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +1 -1
  69. data/lib/rubocop/cop/rails/validation.rb +1 -1
  70. data/lib/rubocop/cop/rails/where_equals.rb +28 -12
  71. data/lib/rubocop/cop/rails/where_not.rb +11 -6
  72. data/lib/rubocop/cop/rails/where_range.rb +7 -2
  73. data/lib/rubocop/cop/rails_cops.rb +4 -0
  74. data/lib/rubocop/rails/migration_file_skippable.rb +54 -0
  75. data/lib/rubocop/rails/plugin.rb +48 -0
  76. data/lib/rubocop/rails/version.rb +1 -1
  77. data/lib/rubocop/rails.rb +1 -8
  78. data/lib/rubocop-rails.rb +4 -5
  79. metadata +29 -12
  80. data/lib/rubocop/rails/inject.rb +0 -18
@@ -68,7 +68,7 @@ module RuboCop
68
68
  end
69
69
 
70
70
  def using_squish?(node)
71
- node.parent&.send_type? && node.parent&.method?(:squish)
71
+ node.parent&.send_type? && node.parent.method?(:squish)
72
72
  end
73
73
 
74
74
  def singleline_comments_present?(node)
@@ -33,7 +33,7 @@ module RuboCop
33
33
 
34
34
  def on_send(node)
35
35
  return unless (receiver = node.receiver)
36
- return unless receiver.str_type? || receiver.dstr_type?
36
+ return unless receiver.type?(:str, :dstr)
37
37
  return unless receiver.respond_to?(:heredoc?) && receiver.heredoc?
38
38
 
39
39
  register_offense(node, receiver)
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Enforces the use of `ActionController::Parameters#expect` as a method for strong parameter handling.
7
+ #
8
+ # @safety
9
+ # This cop's autocorrection is considered unsafe because there are cases where the HTTP status may change
10
+ # from 500 to 400 when handling invalid parameters. This change, however, reflects an intentional
11
+ # incompatibility introduced for valid reasons by the `expect` method, which aligns better with
12
+ # strong parameter conventions.
13
+ #
14
+ # @example
15
+ #
16
+ # # bad
17
+ # params.require(:user).permit(:name, :age)
18
+ # params.permit(user: [:name, :age]).require(:user)
19
+ #
20
+ # # good
21
+ # params.expect(user: [:name, :age])
22
+ #
23
+ class StrongParametersExpect < Base
24
+ extend AutoCorrector
25
+ extend TargetRailsVersion
26
+
27
+ MSG = 'Use `%<prefer>s` instead.'
28
+ RESTRICT_ON_SEND = %i[require permit].freeze
29
+
30
+ minimum_target_rails_version 8.0
31
+
32
+ def_node_matcher :params_require_permit, <<~PATTERN
33
+ $(call
34
+ $(call
35
+ (send nil? :params) :require _) :permit _+)
36
+ PATTERN
37
+
38
+ def_node_matcher :params_permit_require, <<~PATTERN
39
+ $(call
40
+ $(call
41
+ (send nil? :params) :permit (hash (pair _require_param_name _ )))
42
+ :require _require_param_name)
43
+ PATTERN
44
+
45
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
46
+ def on_send(node)
47
+ return if part_of_ignored_node?(node)
48
+
49
+ if (permit_method, require_method = params_require_permit(node))
50
+ range = offense_range(require_method, node)
51
+ prefer = expect_method(require_method, permit_method)
52
+ replace_argument = true
53
+ elsif (require_method, permit_method = params_permit_require(node))
54
+ range = offense_range(permit_method, node)
55
+ prefer = "expect(#{permit_method.arguments.map(&:source).join(', ')})"
56
+ replace_argument = false
57
+ else
58
+ return
59
+ end
60
+
61
+ add_offense(range, message: format(MSG, prefer: prefer)) do |corrector|
62
+ corrector.remove(require_method.receiver.source_range.end.join(require_method.source_range.end))
63
+ corrector.replace(permit_method.loc.selector, 'expect')
64
+ if replace_argument
65
+ corrector.insert_before(permit_method.first_argument, "#{require_key(require_method)}[")
66
+ corrector.insert_after(permit_method.last_argument, ']')
67
+ end
68
+ end
69
+
70
+ ignore_node(node)
71
+ end
72
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
73
+ alias on_csend on_send
74
+
75
+ private
76
+
77
+ def offense_range(method_node, node)
78
+ method_node.loc.selector.join(node.source_range.end)
79
+ end
80
+
81
+ def expect_method(require_method, permit_method)
82
+ require_key = require_key(require_method)
83
+ permit_args = permit_method.arguments.map(&:source).join(', ')
84
+
85
+ arguments = "#{require_key}[#{permit_args}]"
86
+
87
+ "expect(#{arguments})"
88
+ end
89
+
90
+ def require_key(require_method)
91
+ if (first_argument = require_method.first_argument).respond_to?(:value)
92
+ require_arg = first_argument.value
93
+ separator = ': '
94
+ else
95
+ require_arg = first_argument.source
96
+ separator = ' => '
97
+ end
98
+
99
+ "#{require_arg}#{separator}"
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -18,8 +18,9 @@ module RuboCop
18
18
  # t.boolean :active, default: true, null: false
19
19
  #
20
20
  class ThreeStateBooleanColumn < Base
21
- MSG = 'Boolean columns should always have a default value and a `NOT NULL` constraint.'
21
+ include MigrationsHelper
22
22
 
23
+ MSG = 'Boolean columns should always have a default value and a `NOT NULL` constraint.'
23
24
  RESTRICT_ON_SEND = %i[add_column column boolean].freeze
24
25
 
25
26
  def_node_matcher :three_state_boolean?, <<~PATTERN
@@ -44,7 +45,7 @@ module RuboCop
44
45
 
45
46
  return if required_options?(options_node)
46
47
 
47
- def_node = node.each_ancestor(:def, :defs).first
48
+ def_node = node.each_ancestor(:any_def).first
48
49
  table_node = table_node(node)
49
50
  return if def_node && (table_node.nil? || change_column_null?(def_node, table_node, column_node))
50
51
 
@@ -28,6 +28,8 @@ module RuboCop
28
28
  # Time.zone.now
29
29
  # Time.zone.parse('2015-03-02T19:05:37')
30
30
  # Time.zone.parse('2015-03-02T19:05:37Z') # Respect ISO 8601 format with timezone specifier.
31
+ # Time.parse('2015-03-02T19:05:37Z') # Also respects ISO 8601
32
+ # '2015-03-02T19:05:37Z'.to_time # Also respects ISO 8601
31
33
  #
32
34
  # @example EnforcedStyle: flexible (default)
33
35
  # # `flexible` allows usage of `in_time_zone` instead of `zone`.
@@ -67,6 +69,7 @@ module RuboCop
67
69
 
68
70
  def on_send(node)
69
71
  return if !node.receiver&.str_type? || !node.method?(:to_time)
72
+ return if attach_timezone_specifier?(node.receiver)
70
73
 
71
74
  add_offense(node.loc.selector, message: MSG_STRING_TO_TIME) do |corrector|
72
75
  corrector.replace(node, "Time.zone.parse(#{node.receiver.source})") unless node.csend_type?
@@ -94,11 +97,9 @@ module RuboCop
94
97
  end
95
98
 
96
99
  def autocorrect_time_new(node, corrector)
97
- if node.arguments?
98
- corrector.replace(node.loc.selector, 'local')
99
- else
100
- corrector.replace(node.loc.selector, 'now')
101
- end
100
+ replacement = replacement(node)
101
+
102
+ corrector.replace(node.loc.selector, replacement)
102
103
  end
103
104
 
104
105
  # remove redundant `.in_time_zone` from `Time.zone.now.in_time_zone`
@@ -134,7 +135,9 @@ module RuboCop
134
135
  end
135
136
 
136
137
  def attach_timezone_specifier?(date)
137
- date.respond_to?(:value) && TIMEZONE_SPECIFIER.match?(date.value.to_s)
138
+ return false unless date.respond_to?(:value)
139
+
140
+ !date.value.to_s.valid_encoding? || TIMEZONE_SPECIFIER.match?(date.value.to_s)
138
141
  end
139
142
 
140
143
  def build_message(klass, method_name, node)
@@ -180,7 +183,7 @@ module RuboCop
180
183
 
181
184
  def safe_method(method_name, node)
182
185
  if %w[new current].include?(method_name)
183
- node.arguments? ? 'local' : 'now'
186
+ replacement(node)
184
187
  else
185
188
  method_name
186
189
  end
@@ -256,6 +259,12 @@ module RuboCop
256
259
  pair.key.sym_type? && pair.key.value == :in && !pair.value.nil_type?
257
260
  end
258
261
  end
262
+
263
+ def replacement(node)
264
+ return 'now' unless node.arguments?
265
+
266
+ node.first_argument.str_type? ? 'parse' : 'local'
267
+ end
259
268
  end
260
269
  end
261
270
  end
@@ -15,6 +15,10 @@ module RuboCop
15
15
  #
16
16
  # If you are defining custom transaction methods, you can configure it with `TransactionMethods`.
17
17
  #
18
+ # NOTE: This cop is disabled on Rails >= 7.2 because transactions were restored
19
+ # to their historical behavior. In Rails 7.1, the behavior is controlled with
20
+ # the config `active_record.commit_transaction_on_non_local_return`.
21
+ #
18
22
  # @example
19
23
  # # bad
20
24
  # ApplicationRecord.transaction do
@@ -76,6 +80,7 @@ module RuboCop
76
80
  PATTERN
77
81
 
78
82
  def on_send(node)
83
+ return if target_rails_version >= 7.2
79
84
  return unless in_transaction_block?(node)
80
85
 
81
86
  exit_statements(node.parent.body).each do |statement_node|
@@ -94,7 +99,7 @@ module RuboCop
94
99
  return false unless transaction_method_name?(node.method_name)
95
100
  return false unless (parent = node.parent)
96
101
 
97
- parent.block_type? && parent.body
102
+ parent.any_block_type? && parent.body
98
103
  end
99
104
 
100
105
  def statement(statement_node)
@@ -108,7 +113,7 @@ module RuboCop
108
113
  end
109
114
 
110
115
  def nested_block?(statement_node)
111
- name = statement_node.ancestors.find(&:block_type?).children.first.method_name
116
+ name = statement_node.ancestors.find(&:any_block_type?).children.first.method_name
112
117
  !transaction_method_name?(name)
113
118
  end
114
119
 
@@ -51,51 +51,28 @@ module RuboCop
51
51
 
52
52
  MSG = 'Use `distinct` before `pluck`.'
53
53
  RESTRICT_ON_SEND = %i[uniq].freeze
54
- NEWLINE = "\n"
55
- PATTERN = '[!^block (send (send %<type>s :pluck ...) :uniq ...)]'
56
54
 
57
- def_node_matcher :conservative_node_match, format(PATTERN, type: 'const')
58
-
59
- def_node_matcher :aggressive_node_match, format(PATTERN, type: '_')
55
+ def_node_matcher :uniq_before_pluck, '[!^any_block $(send $(send _ :pluck ...) :uniq ...)]'
60
56
 
61
57
  def on_send(node)
62
- uniq = if style == :conservative
63
- conservative_node_match(node)
64
- else
65
- aggressive_node_match(node)
66
- end
67
-
68
- return unless uniq
58
+ uniq_before_pluck(node) do |uniq_node, pluck_node|
59
+ next if style == :conservative && !pluck_node.receiver&.const_type?
69
60
 
70
- add_offense(node.loc.selector) do |corrector|
71
- autocorrect(corrector, node)
61
+ add_offense(uniq_node.loc.selector) do |corrector|
62
+ autocorrect(corrector, uniq_node, pluck_node)
63
+ end
72
64
  end
73
65
  end
74
66
 
75
67
  private
76
68
 
77
- def autocorrect(corrector, node)
78
- method = node.method_name
69
+ def autocorrect(corrector, uniq_node, pluck_node)
70
+ corrector.remove(range_between(pluck_node.loc.end.end_pos, uniq_node.loc.selector.end_pos))
79
71
 
80
- corrector.remove(dot_method_with_whitespace(method, node))
81
- if (dot = node.receiver.loc.dot)
72
+ if (dot = pluck_node.loc.dot)
82
73
  corrector.insert_before(dot.begin, '.distinct')
83
74
  else
84
- corrector.insert_before(node.receiver, 'distinct.')
85
- end
86
- end
87
-
88
- def dot_method_with_whitespace(method, node)
89
- range_between(dot_method_begin_pos(method, node), node.loc.selector.end_pos)
90
- end
91
-
92
- def dot_method_begin_pos(method, node)
93
- lines = node.source.split(NEWLINE)
94
-
95
- if lines.last.strip == ".#{method}"
96
- node.source.rindex(NEWLINE)
97
- else
98
- node.loc.dot.begin_pos
75
+ corrector.insert_before(pluck_node, 'distinct.')
99
76
  end
100
77
  end
101
78
  end
@@ -73,7 +73,7 @@ module RuboCop
73
73
 
74
74
  def column_names(node, uniqueness_part)
75
75
  arg = node.first_argument
76
- return unless arg.str_type? || arg.sym_type?
76
+ return unless arg.type?(:str, :sym)
77
77
 
78
78
  ret = [arg.value]
79
79
  names_from_scope = column_names_from_scope(uniqueness_part)
@@ -59,11 +59,11 @@ module RuboCop
59
59
 
60
60
  def on_send(node)
61
61
  return if node.receiver
62
+ return unless (last_argument = node.last_argument)
62
63
 
63
64
  range = node.loc.selector
64
65
 
65
66
  add_offense(range, message: message(node)) do |corrector|
66
- last_argument = node.last_argument
67
67
  return if !last_argument.literal? && !last_argument.splat_type? && !frozen_array_argument?(last_argument)
68
68
 
69
69
  corrector.replace(range, 'validates')
@@ -4,7 +4,8 @@ module RuboCop
4
4
  module Cop
5
5
  module Rails
6
6
  # Identifies places where manually constructed SQL
7
- # in `where` can be replaced with `where(attribute: value)`.
7
+ # in `where` and `where.not` can be replaced with
8
+ # `where(attribute: value)` and `where.not(attribute: value)`.
8
9
  #
9
10
  # @safety
10
11
  # This cop's autocorrection is unsafe because is may change SQL.
@@ -13,6 +14,7 @@ module RuboCop
13
14
  # @example
14
15
  # # bad
15
16
  # User.where('name = ?', 'Gabe')
17
+ # User.where.not('name = ?', 'Gabe')
16
18
  # User.where('name = :name', name: 'Gabe')
17
19
  # User.where('name IS NULL')
18
20
  # User.where('name IN (?)', ['john', 'jane'])
@@ -21,6 +23,7 @@ module RuboCop
21
23
  #
22
24
  # # good
23
25
  # User.where(name: 'Gabe')
26
+ # User.where.not(name: 'Gabe')
24
27
  # User.where(name: nil)
25
28
  # User.where(name: ['john', 'jane'])
26
29
  # User.where(users: { name: 'Gabe' })
@@ -29,25 +32,27 @@ module RuboCop
29
32
  extend AutoCorrector
30
33
 
31
34
  MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
32
- RESTRICT_ON_SEND = %i[where].freeze
35
+ RESTRICT_ON_SEND = %i[where not].freeze
33
36
 
34
37
  def_node_matcher :where_method_call?, <<~PATTERN
35
38
  {
36
- (call _ :where (array $str_type? $_ ?))
37
- (call _ :where $str_type? $_ ?)
39
+ (call _ {:where :not} (array $str_type? $_ ?))
40
+ (call _ {:where :not} $str_type? $_ ?)
38
41
  }
39
42
  PATTERN
40
43
 
41
44
  def on_send(node)
45
+ return if node.method?(:not) && !where_not?(node)
46
+
42
47
  where_method_call?(node) do |template_node, value_node|
43
48
  value_node = value_node.first
44
49
 
45
50
  range = offense_range(node)
46
51
 
47
- column_and_value = extract_column_and_value(template_node, value_node)
48
- return unless column_and_value
52
+ column, value = extract_column_and_value(template_node, value_node)
53
+ return unless value
49
54
 
50
- good_method = build_good_method(*column_and_value)
55
+ good_method = build_good_method(node.method_name, column, value)
51
56
  message = format(MSG, good_method: good_method)
52
57
 
53
58
  add_offense(range, message: message) do |corrector|
@@ -69,11 +74,12 @@ module RuboCop
69
74
  range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
70
75
  end
71
76
 
77
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
72
78
  def extract_column_and_value(template_node, value_node)
73
79
  value =
74
80
  case template_node.value
75
81
  when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
76
- value_node.source
82
+ value_node&.source
77
83
  when EQ_NAMED_RE, IN_NAMED_RE
78
84
  return unless value_node&.hash_type?
79
85
 
@@ -85,18 +91,28 @@ module RuboCop
85
91
  return
86
92
  end
87
93
 
88
- [Regexp.last_match(1), value]
94
+ column_qualifier = Regexp.last_match(1)
95
+ return if column_qualifier.count('.') > 1
96
+
97
+ [column_qualifier, value]
89
98
  end
99
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
90
100
 
91
- def build_good_method(column, value)
101
+ def build_good_method(method_name, column, value)
92
102
  if column.include?('.')
93
103
  table, column = column.split('.')
94
104
 
95
- "where(#{table}: { #{column}: #{value} })"
105
+ "#{method_name}(#{table}: { #{column}: #{value} })"
96
106
  else
97
- "where(#{column}: #{value})"
107
+ "#{method_name}(#{column}: #{value})"
98
108
  end
99
109
  end
110
+
111
+ def where_not?(node)
112
+ return false unless (receiver = node.receiver)
113
+
114
+ receiver.send_type? && receiver.method?(:where)
115
+ end
100
116
  end
101
117
  end
102
118
  end
@@ -43,10 +43,10 @@ module RuboCop
43
43
 
44
44
  range = offense_range(node)
45
45
 
46
- column_and_value = extract_column_and_value(template_node, value_node)
47
- return unless column_and_value
46
+ column, value = extract_column_and_value(template_node, value_node)
47
+ return unless value
48
48
 
49
- good_method = build_good_method(node.loc.dot&.source, *column_and_value)
49
+ good_method = build_good_method(node.loc.dot&.source, column, value)
50
50
  message = format(MSG, good_method: good_method)
51
51
 
52
52
  add_offense(range, message: message) do |corrector|
@@ -68,13 +68,14 @@ module RuboCop
68
68
  range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
69
69
  end
70
70
 
71
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
71
72
  def extract_column_and_value(template_node, value_node)
72
73
  value =
73
74
  case template_node.value
74
75
  when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE
75
- value_node.source
76
+ value_node&.source
76
77
  when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE
77
- return unless value_node.hash_type?
78
+ return unless value_node&.hash_type?
78
79
 
79
80
  pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
80
81
  pair.value.source
@@ -84,8 +85,12 @@ module RuboCop
84
85
  return
85
86
  end
86
87
 
87
- [Regexp.last_match(1), value]
88
+ column_qualifier = Regexp.last_match(1)
89
+ return if column_qualifier.count('.') > 1
90
+
91
+ [column_qualifier, value]
88
92
  end
93
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
89
94
 
90
95
  def build_good_method(dot, column, value)
91
96
  dot ||= '.'
@@ -89,7 +89,7 @@ module RuboCop
89
89
 
90
90
  def where_not?(node)
91
91
  receiver = node.receiver
92
- receiver&.send_type? && receiver&.method?(:where)
92
+ receiver&.send_type? && receiver.method?(:where)
93
93
  end
94
94
 
95
95
  # rubocop:disable Metrics
@@ -140,6 +140,8 @@ module RuboCop
140
140
  rhs = pair2.value
141
141
  end
142
142
  end
143
+ else
144
+ return
143
145
  end
144
146
 
145
147
  if lhs
@@ -150,7 +152,10 @@ module RuboCop
150
152
  rhs_source = parentheses_needed?(rhs) ? "(#{rhs.source})" : rhs.source
151
153
  end
152
154
 
153
- [Regexp.last_match(1), "#{lhs_source}#{operator}#{rhs_source}"] if operator
155
+ column_qualifier = Regexp.last_match(1)
156
+ return if column_qualifier.count('.') > 1
157
+
158
+ [column_qualifier, "#{lhs_source}#{operator}#{rhs_source}"] if operator
154
159
  end
155
160
  # rubocop:enable Metrics
156
161
 
@@ -7,9 +7,11 @@ require_relative 'mixin/database_type_resolvable'
7
7
  require_relative 'mixin/enforce_superclass'
8
8
  require_relative 'mixin/index_method'
9
9
  require_relative 'mixin/migrations_helper'
10
+ require_relative 'mixin/routes_helper'
10
11
  require_relative 'mixin/target_rails_version'
11
12
 
12
13
  require_relative 'rails/action_controller_flash_before_render'
14
+ require_relative 'rails/strong_parameters_expect'
13
15
  require_relative 'rails/action_controller_test_case'
14
16
  require_relative 'rails/action_filter'
15
17
  require_relative 'rails/action_order'
@@ -46,6 +48,7 @@ require_relative 'rails/duration_arithmetic'
46
48
  require_relative 'rails/dynamic_find_by'
47
49
  require_relative 'rails/eager_evaluation_log_message'
48
50
  require_relative 'rails/enum_hash'
51
+ require_relative 'rails/enum_syntax'
49
52
  require_relative 'rails/enum_uniqueness'
50
53
  require_relative 'rails/env_local'
51
54
  require_relative 'rails/environment_comparison'
@@ -76,6 +79,7 @@ require_relative 'rails/link_to_blank'
76
79
  require_relative 'rails/mailer_name'
77
80
  require_relative 'rails/match_route'
78
81
  require_relative 'rails/migration_class_name'
82
+ require_relative 'rails/multiple_route_paths'
79
83
  require_relative 'rails/negate_include'
80
84
  require_relative 'rails/not_null_column'
81
85
  require_relative 'rails/order_by_id'
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Rails
5
+ # This module allows cops to detect and ignore files that have already been migrated
6
+ # by leveraging the `AllCops: MigratedSchemaVersion` configuration.
7
+ #
8
+ # [source,yaml]
9
+ # -----
10
+ # AllCops:
11
+ # MigratedSchemaVersion: '20241225000000'
12
+ # -----
13
+ #
14
+ # When applied to cops, it overrides the `add_global_offense` and `add_offense` methods,
15
+ # ensuring that cops skip processing if the file is determined to be a migrated file
16
+ # based on the schema version.
17
+ #
18
+ # @api private
19
+ module MigrationFileSkippable
20
+ def add_global_offense(message = nil, severity: nil)
21
+ return if already_migrated_file?
22
+
23
+ super if method(__method__).super_method
24
+ end
25
+
26
+ def add_offense(node_or_range, message: nil, severity: nil, &block)
27
+ return if already_migrated_file?
28
+
29
+ super if method(__method__).super_method
30
+ end
31
+
32
+ def self.apply_to_cops!
33
+ RuboCop::Cop::Registry.all.each { |cop| cop.prepend(MigrationFileSkippable) }
34
+ end
35
+
36
+ private
37
+
38
+ def already_migrated_file?
39
+ return false unless migrated_schema_version
40
+
41
+ match_data = File.basename(processed_source.file_path).match(/(?<timestamp>\d{14})/)
42
+ schema_version = match_data['timestamp'] if match_data
43
+
44
+ return false unless schema_version
45
+
46
+ schema_version <= migrated_schema_version.to_s # Ignore applied migration files.
47
+ end
48
+
49
+ def migrated_schema_version
50
+ @migrated_schema_version ||= config.for_all_cops.fetch('MigratedSchemaVersion', nil)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lint_roller'
4
+
5
+ module RuboCop
6
+ module Rails
7
+ # A plugin that integrates RuboCop Rails with RuboCop's plugin system.
8
+ class Plugin < LintRoller::Plugin
9
+ def about
10
+ LintRoller::About.new(
11
+ name: 'rubocop-rails',
12
+ version: Version::STRING,
13
+ homepage: 'https://github.com/rubocop/rubocop-rails',
14
+ description: 'A RuboCop extension focused on enforcing Rails best practices and coding conventions.'
15
+ )
16
+ end
17
+
18
+ def supported?(context)
19
+ context.engine == :rubocop
20
+ end
21
+
22
+ def rules(_context)
23
+ project_root = Pathname.new(__dir__).join('../../..')
24
+
25
+ ConfigObsoletion.files << project_root.join('config', 'obsoletion.yml')
26
+
27
+ # FIXME: This is a dirty hack relying on a private constant to prevent
28
+ # "Warning: AllCops does not support TargetRailsVersion parameter".
29
+ # It should be updated to a better design in the future.
30
+ without_warnings do
31
+ ConfigValidator.const_set(:COMMON_PARAMS, ConfigValidator::COMMON_PARAMS.dup << 'TargetRailsVersion')
32
+ end
33
+
34
+ LintRoller::Rules.new(type: :path, config_format: :rubocop, value: project_root.join('config', 'default.yml'))
35
+ end
36
+
37
+ private
38
+
39
+ def without_warnings
40
+ original_verbose = $VERBOSE
41
+ $VERBOSE = nil
42
+ yield
43
+ ensure
44
+ $VERBOSE = original_verbose
45
+ end
46
+ end
47
+ end
48
+ end
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Rails
5
5
  # This module holds the RuboCop Rails version information.
6
6
  module Version
7
- STRING = '2.25.1'
7
+ STRING = '2.32.0'
8
8
 
9
9
  def self.document_version
10
10
  STRING.match('\d+\.\d+').to_s
data/lib/rubocop/rails.rb CHANGED
@@ -1,14 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RuboCop
4
- # RuboCop Rails project namespace
4
+ # RuboCop Rails project namespace.
5
5
  module Rails
6
- PROJECT_ROOT = Pathname.new(__dir__).parent.parent.expand_path.freeze
7
- CONFIG_DEFAULT = PROJECT_ROOT.join('config', 'default.yml').freeze
8
- CONFIG = YAML.safe_load(CONFIG_DEFAULT.read, permitted_classes: [Regexp, Symbol]).freeze
9
-
10
- private_constant(:CONFIG_DEFAULT, :PROJECT_ROOT)
11
-
12
- ::RuboCop::ConfigObsoletion.files << PROJECT_ROOT.join('config', 'obsoletion.yml')
13
6
  end
14
7
  end