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
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Looks for enums written with keyword arguments syntax.
7
+ #
8
+ # Defining enums with keyword arguments syntax is deprecated and will be removed in Rails 8.0.
9
+ # Positional arguments should be used instead:
10
+ #
11
+ # @example
12
+ # # bad
13
+ # enum status: { active: 0, archived: 1 }, _prefix: true
14
+ #
15
+ # # good
16
+ # enum :status, { active: 0, archived: 1 }, prefix: true
17
+ #
18
+ class EnumSyntax < Base
19
+ extend AutoCorrector
20
+ extend TargetRubyVersion
21
+ extend TargetRailsVersion
22
+
23
+ minimum_target_ruby_version 3.0
24
+ minimum_target_rails_version 7.0
25
+
26
+ MSG = 'Enum defined with keyword arguments in `%<enum>s` enum declaration. Use positional arguments instead.'
27
+ MSG_OPTIONS = 'Enum defined with deprecated options in `%<enum>s` enum declaration. Remove the `_` prefix.'
28
+ RESTRICT_ON_SEND = %i[enum].freeze
29
+
30
+ # From https://github.com/rails/rails/blob/v7.2.1/activerecord/lib/active_record/enum.rb#L231
31
+ OPTION_NAMES = %w[prefix suffix scopes default instance_methods].freeze
32
+ UNDERSCORED_OPTION_NAMES = OPTION_NAMES.map { |option| "_#{option}" }.freeze
33
+
34
+ def_node_matcher :enum?, <<~PATTERN
35
+ (send nil? :enum (hash $...))
36
+ PATTERN
37
+
38
+ def_node_matcher :enum_with_options?, <<~PATTERN
39
+ (send nil? :enum $_ ${array hash} $_)
40
+ PATTERN
41
+
42
+ def on_send(node)
43
+ check_and_correct_keyword_args(node)
44
+ check_enum_options(node)
45
+ end
46
+
47
+ private
48
+
49
+ def check_and_correct_keyword_args(node)
50
+ enum?(node) do |pairs|
51
+ pairs.each do |pair|
52
+ next if option_key?(pair)
53
+
54
+ correct_keyword_args(node, pair.key, pair.value, pairs[1..])
55
+ end
56
+ end
57
+ end
58
+
59
+ def check_enum_options(node)
60
+ enum_with_options?(node) do |key, _, options|
61
+ options.children.each do |option|
62
+ next unless option_key?(option)
63
+
64
+ add_offense(option.key, message: format(MSG_OPTIONS, enum: enum_name_value(key))) do |corrector|
65
+ corrector.replace(option.key, option.key.source.delete_prefix('_'))
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def correct_keyword_args(node, key, values, options)
72
+ add_offense(values, message: format(MSG, enum: enum_name_value(key))) do |corrector|
73
+ # TODO: Multi-line autocorrect could be implemented in the future.
74
+ next if multiple_enum_definitions?(node)
75
+
76
+ preferred_syntax = "enum #{enum_name(key)}, #{values.source}#{correct_options(options)}"
77
+
78
+ corrector.replace(node, preferred_syntax)
79
+ end
80
+ end
81
+
82
+ def multiple_enum_definitions?(node)
83
+ keys = node.first_argument.keys.map { |key| key.source.delete_prefix('_') }
84
+ filterred_keys = keys.filter { |key| !OPTION_NAMES.include?(key) }
85
+ filterred_keys.size >= 2
86
+ end
87
+
88
+ def enum_name_value(key)
89
+ case key.type
90
+ when :sym, :str
91
+ key.value
92
+ else
93
+ key.source
94
+ end
95
+ end
96
+
97
+ def enum_name(elem)
98
+ case elem.type
99
+ when :str
100
+ elem.value.dump
101
+ when :sym
102
+ elem.value.inspect
103
+ else
104
+ elem.source
105
+ end
106
+ end
107
+
108
+ def option_key?(pair)
109
+ return false unless pair.respond_to?(:key)
110
+
111
+ UNDERSCORED_OPTION_NAMES.include?(pair.key.source)
112
+ end
113
+
114
+ def correct_options(options)
115
+ corrected_options = options.map do |pair|
116
+ name = if pair.key.source[0] == '_'
117
+ pair.key.source[1..]
118
+ else
119
+ pair.key.source
120
+ end
121
+
122
+ "#{name}: #{pair.value.source}"
123
+ end.join(', ')
124
+
125
+ ", #{corrected_options}" unless corrected_options.empty?
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -7,6 +7,18 @@ module RuboCop
7
7
  #
8
8
  # @example
9
9
  # # bad
10
+ # enum :status, { active: 0, archived: 0 }
11
+ #
12
+ # # good
13
+ # enum :status, { active: 0, archived: 1 }
14
+ #
15
+ # # bad
16
+ # enum :status, [:active, :archived, :active]
17
+ #
18
+ # # good
19
+ # enum :status, [:active, :archived]
20
+ #
21
+ # # bad
10
22
  # enum status: { active: 0, archived: 0 }
11
23
  #
12
24
  # # good
@@ -24,6 +36,10 @@ module RuboCop
24
36
  RESTRICT_ON_SEND = %i[enum].freeze
25
37
 
26
38
  def_node_matcher :enum?, <<~PATTERN
39
+ (send nil? :enum $_ ${array hash} ...)
40
+ PATTERN
41
+
42
+ def_node_matcher :enum_with_old_syntax?, <<~PATTERN
27
43
  (send nil? :enum (hash $...))
28
44
  PATTERN
29
45
 
@@ -32,15 +48,17 @@ module RuboCop
32
48
  PATTERN
33
49
 
34
50
  def on_send(node)
35
- enum?(node) do |pairs|
51
+ enum?(node) do |key, args|
52
+ consecutive_duplicates(args.values).each do |item|
53
+ add_offense(item, message: message(key, item))
54
+ end
55
+ end
56
+
57
+ enum_with_old_syntax?(node) do |pairs|
36
58
  pairs.each do |pair|
37
59
  enum_values(pair) do |key, args|
38
- items = args.values
39
-
40
- next unless duplicates?(items)
41
-
42
- consecutive_duplicates(items).each do |item|
43
- add_offense(item, message: format(MSG, value: item.source, enum: enum_name(key)))
60
+ consecutive_duplicates(args.values).each do |item|
61
+ add_offense(item, message: message(key, item))
44
62
  end
45
63
  end
46
64
  end
@@ -57,6 +75,10 @@ module RuboCop
57
75
  key.source
58
76
  end
59
77
  end
78
+
79
+ def message(key, item)
80
+ format(MSG, value: item.source, enum: enum_name(key))
81
+ end
60
82
  end
61
83
  end
62
84
  end
@@ -19,20 +19,33 @@ module RuboCop
19
19
  extend TargetRailsVersion
20
20
 
21
21
  MSG = 'Use `Rails.env.local?` instead.'
22
+ MSG_NEGATED = 'Use `!Rails.env.local?` instead.'
22
23
  LOCAL_ENVIRONMENTS = %i[development? test?].to_set.freeze
23
24
 
24
25
  minimum_target_rails_version 7.1
25
26
 
26
- # @!method rails_env_local_candidate?(node)
27
- def_node_matcher :rails_env_local_candidate?, <<~PATTERN
27
+ # @!method rails_env_local_or?(node)
28
+ def_node_matcher :rails_env_local_or?, <<~PATTERN
28
29
  (or
29
30
  (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
30
31
  (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
31
32
  )
32
33
  PATTERN
33
34
 
35
+ # @!method rails_env_local_and?(node)
36
+ def_node_matcher :rails_env_local_and?, <<~PATTERN
37
+ (and
38
+ (send
39
+ (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
40
+ :!)
41
+ (send
42
+ (send (send (const {cbase nil? } :Rails) :env) $%LOCAL_ENVIRONMENTS)
43
+ :!)
44
+ )
45
+ PATTERN
46
+
34
47
  def on_or(node)
35
- rails_env_local_candidate?(node) do |*environments|
48
+ rails_env_local_or?(node) do |*environments|
36
49
  next unless environments.to_set == LOCAL_ENVIRONMENTS
37
50
 
38
51
  add_offense(node) do |corrector|
@@ -40,6 +53,16 @@ module RuboCop
40
53
  end
41
54
  end
42
55
  end
56
+
57
+ def on_and(node)
58
+ rails_env_local_and?(node) do |*environments|
59
+ next unless environments.to_set == LOCAL_ENVIRONMENTS
60
+
61
+ add_offense(node, message: MSG_NEGATED) do |corrector|
62
+ corrector.replace(node, '!Rails.env.local?')
63
+ end
64
+ end
65
+ end
43
66
  end
44
67
  end
45
68
  end
@@ -6,6 +6,9 @@ module RuboCop
6
6
  # Identifies usages of file path joining process to use `Rails.root.join` clause.
7
7
  # It is used to add uniformity when joining paths.
8
8
  #
9
+ # NOTE: This cop ignores leading slashes in string literal arguments for `Rails.root.join`
10
+ # and multiple slashes in string literal arguments for `Rails.root.join` and `File.join`.
11
+ #
9
12
  # @example EnforcedStyle: slashes (default)
10
13
  # # bad
11
14
  # Rails.root.join('app', 'models', 'goober')
@@ -66,6 +69,8 @@ module RuboCop
66
69
 
67
70
  def on_send(node)
68
71
  check_for_file_join_with_rails_root(node)
72
+ return unless node.receiver
73
+
69
74
  check_for_rails_root_join_with_slash_separated_path(node)
70
75
  check_for_rails_root_join_with_string_arguments(node)
71
76
  end
@@ -76,6 +81,7 @@ module RuboCop
76
81
  rails_root_index = find_rails_root_index(node)
77
82
  slash_node = node.children[rails_root_index + 1]
78
83
  return unless slash_node&.str_type? && slash_node.source.start_with?(File::SEPARATOR)
84
+ return unless node.children[rails_root_index].children.first.send_type?
79
85
 
80
86
  register_offense(node, require_to_s: false) do |corrector|
81
87
  autocorrect_slash_after_rails_root_in_dstr(corrector, node, rails_root_index)
@@ -94,10 +100,10 @@ module RuboCop
94
100
 
95
101
  def check_for_file_join_with_rails_root(node)
96
102
  return unless file_join_nodes?(node)
97
- return unless node.arguments.any? { |e| rails_root_nodes?(e) }
103
+ return unless valid_arguments_for_file_join_with_rails_root?(node.arguments)
98
104
 
99
105
  register_offense(node, require_to_s: true) do |corrector|
100
- autocorrect_file_join(corrector, node)
106
+ autocorrect_file_join(corrector, node) unless node.first_argument.array_type?
101
107
  end
102
108
  end
103
109
 
@@ -105,8 +111,7 @@ module RuboCop
105
111
  return unless style == :slashes
106
112
  return unless rails_root_nodes?(node)
107
113
  return unless rails_root_join_nodes?(node)
108
- return unless node.arguments.size > 1
109
- return unless node.arguments.all?(&:str_type?)
114
+ return unless valid_string_arguments_for_rails_root_join?(node.arguments)
110
115
 
111
116
  register_offense(node, require_to_s: false) do |corrector|
112
117
  autocorrect_rails_root_join_with_string_arguments(corrector, node)
@@ -117,15 +122,42 @@ module RuboCop
117
122
  return unless style == :arguments
118
123
  return unless rails_root_nodes?(node)
119
124
  return unless rails_root_join_nodes?(node)
120
- return unless node.arguments.any? { |arg| string_with_slash?(arg) }
125
+ return unless valid_slash_separated_path_for_rails_root_join?(node.arguments)
121
126
 
122
127
  register_offense(node, require_to_s: false) do |corrector|
123
128
  autocorrect_rails_root_join_with_slash_separated_path(corrector, node)
124
129
  end
125
130
  end
126
131
 
127
- def string_with_slash?(node)
128
- node.str_type? && node.source.include?(File::SEPARATOR)
132
+ def valid_arguments_for_file_join_with_rails_root?(arguments)
133
+ return false unless arguments.any? { |arg| rails_root_nodes?(arg) }
134
+
135
+ arguments.none? { |arg| arg.variable? || arg.const_type? || string_contains_multiple_slashes?(arg) }
136
+ end
137
+
138
+ def valid_string_arguments_for_rails_root_join?(arguments)
139
+ return false unless arguments.size > 1
140
+ return false unless arguments.all?(&:str_type?)
141
+
142
+ arguments.none? { |arg| string_with_leading_slash?(arg) || string_contains_multiple_slashes?(arg) }
143
+ end
144
+
145
+ def valid_slash_separated_path_for_rails_root_join?(arguments)
146
+ return false unless arguments.any? { |arg| string_contains_slash?(arg) }
147
+
148
+ arguments.none? { |arg| string_with_leading_slash?(arg) || string_contains_multiple_slashes?(arg) }
149
+ end
150
+
151
+ def string_contains_slash?(node)
152
+ node.str_type? && node.value.include?(File::SEPARATOR)
153
+ end
154
+
155
+ def string_contains_multiple_slashes?(node)
156
+ node.str_type? && node.value.include?('//')
157
+ end
158
+
159
+ def string_with_leading_slash?(node)
160
+ node.str_type? && node.value.start_with?(File::SEPARATOR)
129
161
  end
130
162
 
131
163
  def register_offense(node, require_to_s:, &block)
@@ -170,7 +202,17 @@ module RuboCop
170
202
  end
171
203
 
172
204
  def autocorrect_file_join(corrector, node)
205
+ replace_receiver_with_rails_root(corrector, node)
206
+ remove_first_argument_with_comma(corrector, node)
207
+ process_arguments(corrector, node.arguments)
208
+ append_to_string_conversion(corrector, node)
209
+ end
210
+
211
+ def replace_receiver_with_rails_root(corrector, node)
173
212
  corrector.replace(node.receiver, 'Rails.root')
213
+ end
214
+
215
+ def remove_first_argument_with_comma(corrector, node)
174
216
  corrector.remove(
175
217
  range_with_surrounding_space(
176
218
  range_with_surrounding_comma(
@@ -180,9 +222,19 @@ module RuboCop
180
222
  side: :right
181
223
  )
182
224
  )
183
- node.arguments.filter(&:str_type?).each do |argument|
184
- corrector.replace(argument, argument.value.delete_prefix('/').inspect)
225
+ end
226
+
227
+ def process_arguments(corrector, arguments)
228
+ arguments.each do |argument|
229
+ if argument.str_type?
230
+ corrector.replace(argument, argument.value.delete_prefix('/').inspect)
231
+ elsif argument.array_type?
232
+ corrector.replace(argument, "*#{argument.source}")
233
+ end
185
234
  end
235
+ end
236
+
237
+ def append_to_string_conversion(corrector, node)
186
238
  corrector.insert_after(node, '.to_s')
187
239
  end
188
240
 
@@ -203,7 +255,7 @@ module RuboCop
203
255
 
204
256
  def autocorrect_rails_root_join_with_slash_separated_path(corrector, node)
205
257
  node.arguments.each do |argument|
206
- next unless string_with_slash?(argument)
258
+ next unless string_contains_slash?(argument)
207
259
 
208
260
  index = argument.source.index(File::SEPARATOR)
209
261
  rest = inner_range_of(argument).adjust(begin_pos: index - 1)
@@ -40,6 +40,10 @@ module RuboCop
40
40
  (hash (kwsplat _))
41
41
  PATTERN
42
42
 
43
+ def_node_matcher :forwarded_kwrestarg?, <<~PATTERN
44
+ (hash (forwarded-kwrestarg))
45
+ PATTERN
46
+
43
47
  def_node_matcher :include_rack_test_methods?, <<~PATTERN
44
48
  (send nil? :include
45
49
  (const
@@ -83,7 +87,9 @@ module RuboCop
83
87
  end
84
88
  end
85
89
 
90
+ # rubocop:disable Metrics/CyclomaticComplexity
86
91
  def needs_conversion?(data)
92
+ return false if data.forwarded_args_type? || forwarded_kwrestarg?(data)
87
93
  return true unless data.hash_type?
88
94
  return false if kwsplat_hash?(data)
89
95
 
@@ -91,6 +97,7 @@ module RuboCop
91
97
  special_keyword_arg?(pair.key) || (format_arg?(pair.key) && data.pairs.one?)
92
98
  end
93
99
  end
100
+ # rubocop:enable Metrics/CyclomaticComplexity
94
101
 
95
102
  def special_keyword_arg?(node)
96
103
  node.sym_type? && KEYWORD_ARGS.include?(node.value)
@@ -52,7 +52,7 @@ module RuboCop
52
52
  (send
53
53
  nil?
54
54
  {#{FILTERS.join(' ')}}
55
- _
55
+ ...
56
56
  $_)
57
57
  PATTERN
58
58
 
@@ -29,18 +29,34 @@ module RuboCop
29
29
  PATTERN
30
30
 
31
31
  def_node_matcher :on_bad_to_h, <<~PATTERN
32
- (block
33
- (call _ :to_h)
34
- (args (arg $_el))
35
- (array $_ (lvar _el)))
32
+ {
33
+ (block
34
+ (call _ :to_h)
35
+ (args (arg $_el))
36
+ (array $_ (lvar _el)))
37
+ (numblock
38
+ (call _ :to_h) $1
39
+ (array $_ (lvar :_1)))
40
+ (itblock
41
+ (call _ :to_h) $:it
42
+ (array $_ (lvar :it)))
43
+ }
36
44
  PATTERN
37
45
 
38
46
  def_node_matcher :on_bad_map_to_h, <<~PATTERN
39
47
  (call
40
- (block
41
- (call _ {:map :collect})
42
- (args (arg $_el))
43
- (array $_ (lvar _el)))
48
+ {
49
+ (block
50
+ (call _ {:map :collect})
51
+ (args (arg $_el))
52
+ (array $_ (lvar _el)))
53
+ (numblock
54
+ (call _ {:map :collect}) $1
55
+ (array $_ (lvar :_1)))
56
+ (itblock
57
+ (call _ {:map :collect}) $:it
58
+ (array $_ (lvar :it)))
59
+ }
44
60
  :to_h)
45
61
  PATTERN
46
62
 
@@ -48,10 +64,19 @@ module RuboCop
48
64
  (send
49
65
  (const {nil? cbase} :Hash)
50
66
  :[]
51
- (block
52
- (call _ {:map :collect})
53
- (args (arg $_el))
54
- (array $_ (lvar _el))))
67
+ {
68
+ (block
69
+ (call _ {:map :collect})
70
+ (args (arg $_el))
71
+ (array $_ (lvar _el)))
72
+ (numblock
73
+ (call _ {:map :collect}) $1
74
+ (array $_ (lvar :_1)))
75
+ (itblock
76
+ (call _ {:map :collect}) $:it
77
+ (array $_ (lvar :it)))
78
+ }
79
+ )
55
80
  PATTERN
56
81
 
57
82
  private
@@ -32,18 +32,34 @@ module RuboCop
32
32
  PATTERN
33
33
 
34
34
  def_node_matcher :on_bad_to_h, <<~PATTERN
35
- (block
36
- (call _ :to_h)
37
- (args (arg $_el))
38
- (array (lvar _el) $_))
35
+ {
36
+ (block
37
+ (call _ :to_h)
38
+ (args (arg $_el))
39
+ (array (lvar _el) $_))
40
+ (numblock
41
+ (call _ :to_h) $1
42
+ (array (lvar :_1) $_))
43
+ (itblock
44
+ (call _ :to_h) $:it
45
+ (array (lvar :it) $_))
46
+ }
39
47
  PATTERN
40
48
 
41
49
  def_node_matcher :on_bad_map_to_h, <<~PATTERN
42
50
  (call
43
- (block
44
- (call _ {:map :collect})
45
- (args (arg $_el))
46
- (array (lvar _el) $_))
51
+ {
52
+ (block
53
+ (call _ {:map :collect})
54
+ (args (arg $_el))
55
+ (array (lvar _el) $_))
56
+ (numblock
57
+ (call _ {:map :collect}) $1
58
+ (array (lvar :_1) $_))
59
+ (itblock
60
+ (call _ {:map :collect}) $:it
61
+ (array (lvar :it) $_))
62
+ }
47
63
  :to_h)
48
64
  PATTERN
49
65
 
@@ -51,10 +67,19 @@ module RuboCop
51
67
  (send
52
68
  (const {nil? cbase} :Hash)
53
69
  :[]
54
- (block
55
- (call _ {:map :collect})
56
- (args (arg $_el))
57
- (array (lvar _el) $_)))
70
+ {
71
+ (block
72
+ (call _ {:map :collect})
73
+ (args (arg $_el))
74
+ (array (lvar _el) $_))
75
+ (numblock
76
+ (call _ {:map :collect}) $1
77
+ (array (lvar :_1) $_))
78
+ (itblock
79
+ (call _ {:map :collect}) $:it
80
+ (array (lvar :it) $_))
81
+ }
82
+ )
58
83
  PATTERN
59
84
 
60
85
  private
@@ -29,7 +29,7 @@ module RuboCop
29
29
  def on_send(node)
30
30
  return unless node.arguments.empty?
31
31
  return unless (receiver = node.receiver)
32
- return if !receiver.str_type? && !receiver.array_type?
32
+ return unless receiver.type?(:str, :array)
33
33
 
34
34
  add_offense(node.loc.selector)
35
35
  end
@@ -115,6 +115,10 @@ module RuboCop
115
115
  $_)))
116
116
  PATTERN
117
117
 
118
+ def_node_matcher :delegated_methods, <<~PATTERN
119
+ (send nil? :delegate (sym $_)+ (hash <(pair (sym :to) _) ...>))
120
+ PATTERN
121
+
118
122
  def on_send(node)
119
123
  methods_node = only_or_except_filter_methods(node)
120
124
  return unless methods_node
@@ -139,7 +143,13 @@ module RuboCop
139
143
  return [] unless block
140
144
 
141
145
  defined_methods = block.each_child_node(:def).map(&:method_name)
142
- defined_methods + aliased_action_methods(block, defined_methods)
146
+ defined_methods + delegated_action_methods(block) + aliased_action_methods(block, defined_methods)
147
+ end
148
+
149
+ def delegated_action_methods(node)
150
+ node.each_child_node(:send).flat_map do |child_node|
151
+ delegated_methods(child_node) || []
152
+ end
143
153
  end
144
154
 
145
155
  def aliased_action_methods(node, defined_methods)
@@ -21,11 +21,11 @@ module RuboCop
21
21
  # match 'photos/:id', to: 'photos#show', via: :all
22
22
  #
23
23
  class MatchRoute < Base
24
+ include RoutesHelper
24
25
  extend AutoCorrector
25
26
 
26
27
  MSG = 'Use `%<http_method>s` instead of `match` to define a route.'
27
28
  RESTRICT_ON_SEND = %i[match].freeze
28
- HTTP_METHODS = %i[get post put patch delete].freeze
29
29
 
30
30
  def_node_matcher :match_method_call?, <<~PATTERN
31
31
  (send nil? :match $_ $(hash ...) ?)
@@ -60,14 +60,6 @@ module RuboCop
60
60
  end
61
61
  end
62
62
 
63
- def_node_matcher :routes_draw?, <<~PATTERN
64
- (send (send _ :routes) :draw)
65
- PATTERN
66
-
67
- def within_routes?(node)
68
- node.each_ancestor(:block).any? { |a| routes_draw?(a.send_node) }
69
- end
70
-
71
63
  def extract_via(node)
72
64
  via_pair = via_pair(node)
73
65
  return %i[get] unless via_pair
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Checks for mapping a route with multiple paths, which is deprecated and will be removed in Rails 8.1.
7
+ #
8
+ # @example
9
+ #
10
+ # # bad
11
+ # get '/users', '/other_path', to: 'users#index'
12
+ #
13
+ # # good
14
+ # get '/users', to: 'users#index'
15
+ # get '/other_path', to: 'users#index'
16
+ #
17
+ class MultipleRoutePaths < Base
18
+ include RoutesHelper
19
+ extend AutoCorrector
20
+
21
+ MSG = 'Use separate routes instead of combining multiple route paths in a single route.'
22
+ RESTRICT_ON_SEND = HTTP_METHODS
23
+
24
+ IGNORED_ARGUMENT_TYPES = %i[array hash].freeze
25
+
26
+ def on_send(node)
27
+ return unless within_routes?(node)
28
+
29
+ route_paths = node.arguments.reject { |argument| IGNORED_ARGUMENT_TYPES.include?(argument.type) }
30
+ return if route_paths.count < 2
31
+
32
+ add_offense(node) do |corrector|
33
+ corrector.replace(node, migrate_to_multiple_routes(node, route_paths))
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def migrate_to_multiple_routes(node, route_paths)
40
+ rest = route_paths.last.source_range.end.join(node.source_range.end).source
41
+ indentation = ' ' * node.source_range.column
42
+
43
+ route_paths.map do |route_path|
44
+ "#{node.method_name} #{route_path.source}#{rest}"
45
+ end.join("\n#{indentation}")
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end