rubocop-rails 2.27.0 → 2.30.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +23 -6
  4. data/config/default.yml +22 -0
  5. data/lib/rubocop/cop/mixin/active_record_helper.rb +1 -1
  6. data/lib/rubocop/cop/mixin/index_method.rb +68 -61
  7. data/lib/rubocop/cop/mixin/routes_helper.rb +20 -0
  8. data/lib/rubocop/cop/rails/add_column_index.rb +1 -0
  9. data/lib/rubocop/cop/rails/belongs_to.rb +1 -1
  10. data/lib/rubocop/cop/rails/blank.rb +1 -1
  11. data/lib/rubocop/cop/rails/bulk_change_table.rb +1 -0
  12. data/lib/rubocop/cop/rails/content_tag.rb +1 -1
  13. data/lib/rubocop/cop/rails/dangerous_column_names.rb +2 -0
  14. data/lib/rubocop/cop/rails/delegate.rb +50 -7
  15. data/lib/rubocop/cop/rails/duplicate_association.rb +8 -4
  16. data/lib/rubocop/cop/rails/file_path.rb +61 -9
  17. data/lib/rubocop/cop/rails/http_positional_arguments.rb +7 -0
  18. data/lib/rubocop/cop/rails/index_by.rb +28 -12
  19. data/lib/rubocop/cop/rails/index_with.rb +28 -12
  20. data/lib/rubocop/cop/rails/inquiry.rb +1 -1
  21. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +11 -1
  22. data/lib/rubocop/cop/rails/match_route.rb +1 -9
  23. data/lib/rubocop/cop/rails/multiple_route_paths.rb +50 -0
  24. data/lib/rubocop/cop/rails/not_null_column.rb +6 -2
  25. data/lib/rubocop/cop/rails/pluck.rb +21 -1
  26. data/lib/rubocop/cop/rails/pluralization_grammar.rb +1 -1
  27. data/lib/rubocop/cop/rails/presence.rb +1 -1
  28. data/lib/rubocop/cop/rails/present.rb +1 -1
  29. data/lib/rubocop/cop/rails/redundant_active_record_all_method.rb +1 -1
  30. data/lib/rubocop/cop/rails/reversible_migration.rb +3 -1
  31. data/lib/rubocop/cop/rails/root_pathname_methods.rb +6 -1
  32. data/lib/rubocop/cop/rails/save_bang.rb +7 -6
  33. data/lib/rubocop/cop/rails/schema_comment.rb +1 -0
  34. data/lib/rubocop/cop/rails/select_map.rb +1 -1
  35. data/lib/rubocop/cop/rails/skips_model_validations.rb +1 -1
  36. data/lib/rubocop/cop/rails/strip_heredoc.rb +1 -1
  37. data/lib/rubocop/cop/rails/strong_parameters_expect.rb +104 -0
  38. data/lib/rubocop/cop/rails/three_state_boolean_column.rb +2 -1
  39. data/lib/rubocop/cop/rails/time_zone.rb +10 -6
  40. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +10 -33
  41. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +1 -1
  42. data/lib/rubocop/cop/rails_cops.rb +3 -0
  43. data/lib/rubocop/rails/migration_file_skippable.rb +54 -0
  44. data/lib/rubocop/rails/plugin.rb +48 -0
  45. data/lib/rubocop/rails/version.rb +1 -1
  46. data/lib/rubocop/rails.rb +1 -8
  47. data/lib/rubocop-rails.rb +4 -4
  48. metadata +28 -9
  49. data/lib/rubocop/rails/inject.rb +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8a20ef13a67612f99cb522625350e57836511b07a2666b75a8cbc556620adf01
4
- data.tar.gz: 436aaa2f094779e0101e17d2cb56f4a7bdd64fbf14360e9e22102be13da413af
3
+ metadata.gz: 69854cebe11dde8160c57b94c6ebbe60e7df48c1357aae64b932f73bb2f230ad
4
+ data.tar.gz: 04b388703ccde52554cb01f0e04a50291beada4dec9199fbb46b3dd750d8a4ff
5
5
  SHA512:
6
- metadata.gz: 4d73f8ec4a0567acff57d0e39b267a24ee615e58d20114fa8fd37d5614ea2d4b3e6435c9ed34db322d7368086f9df150de0646c5e63cfd2987d0847dc3abd057
7
- data.tar.gz: 93f42a0c60daec890a8cd363f588e37e5f65210d63dbb7e85e1c19cd32dc6459d6e593fbc3947b1cef832b981ca7b06bb5871c60cf6439a07e32c3df54257bcc
6
+ metadata.gz: 79fda1fbb14ab98b4472576b7cb776294f3c46eb56c3b0751e47c5fba308615b4f1579a41ea3b14a8c2fb22e19e693ea1e615fe48c5d9fff6f8c857ef887a53f
7
+ data.tar.gz: f219f779b57f3ecd44937c02e85872058b453bdd7ce5c83cc1d6ac05175b2c93c01d2548a02c9b46ca26bc2dca6af116611a9d9595d81b53e5a165e47a8b6cf4
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2012-23 Bozhidar Batsov
1
+ Copyright (c) 2012-25 Bozhidar Batsov
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -31,13 +31,13 @@ ways to do this:
31
31
  Put this into your `.rubocop.yml`.
32
32
 
33
33
  ```yaml
34
- require: rubocop-rails
34
+ plugins: rubocop-rails
35
35
  ```
36
36
 
37
37
  Alternatively, use the following array notation when specifying multiple extensions.
38
38
 
39
39
  ```yaml
40
- require:
40
+ plugins:
41
41
  - rubocop-other-extension
42
42
  - rubocop-rails
43
43
  ```
@@ -45,21 +45,22 @@ require:
45
45
  Now you can run `rubocop` and it will automatically load the RuboCop Rails
46
46
  cops together with the standard cops.
47
47
 
48
+ > [!NOTE]
49
+ > The plugin system is supported in RuboCop 1.72+. In earlier versions, use `require` instead of `plugins`.
50
+
48
51
  ### Command line
49
52
 
50
53
  ```sh
51
- $ rubocop --require rubocop-rails
54
+ $ rubocop --plugin rubocop-rails
52
55
  ```
53
56
 
54
- Note: `--rails` option is required while `rubocop` command supports `--rails` option.
55
-
56
57
  ### Rake task
57
58
 
58
59
  ```ruby
59
60
  require 'rubocop/rake_task'
60
61
 
61
62
  RuboCop::RakeTask.new do |task|
62
- task.requires << 'rubocop-rails'
63
+ task.plugins << 'rubocop-rails'
63
64
  end
64
65
  ```
65
66
 
@@ -83,6 +84,22 @@ gems.locked file to find the version of Rails that has been bound to the
83
84
  application. If neither of those files exist, RuboCop will use Rails 5.0
84
85
  as the default.
85
86
 
87
+ ### `AllCops: MigratedSchemaVersion`
88
+
89
+ By specifying the `MigratedSchemaVersion` option, migration files that have already been run can be ignored.
90
+ When `MigratedSchemaVersion: '20241225000000'` is set, migration files lower than or equal to '20241225000000' will be ignored.
91
+ For example, to ignore db/migrate/20241225000000_create_articles.rb and earlier migrations you would configure it the following way:
92
+
93
+ ```yaml
94
+ AllCops:
95
+ MigratedSchemaVersion: '20241225000000'
96
+ ```
97
+
98
+ This prevents inspecting schema settings for already applied migration files.
99
+ Changing already applied migrations should be avoided because it can lead to the schema getting out of sync
100
+ between your local copy and what it actually is in production, depending on when `bin/rails db:migrate` was executed.
101
+ If you want to modify your schema to comply with the cops, you should instead create new migrations.
102
+
86
103
  ## Rails configuration tip
87
104
 
88
105
  In Rails 6.1+, add the following `config.generators.after_generate` setting to
data/config/default.yml CHANGED
@@ -25,6 +25,10 @@ AllCops:
25
25
  # application. If neither of those files exist, RuboCop will use Rails 5.0
26
26
  # as the default.
27
27
  TargetRailsVersion: ~
28
+ # By specifying `MigratedSchemaVersion` option, migration files that have been migrated can be ignored.
29
+ # When `MigratedSchemaVersion: '20241231000000'` is set. Migration files lower than or equal to '20250101000000' will be ignored.
30
+ # For example, this is the timestamp in db/migrate/20250101000000_create_articles.rb.
31
+ MigratedSchemaVersion: '19700101000000' # NOTE: Used as a sentinel value for the UNIX epoch time.
28
32
 
29
33
  Lint/NumberConversion:
30
34
  # Add Rails' duration methods to the ignore list for `Lint/NumberConversion`
@@ -694,6 +698,15 @@ Rails/MigrationClassName:
694
698
  Include:
695
699
  - db/**/*.rb
696
700
 
701
+ Rails/MultipleRoutePaths:
702
+ Description: 'Checks for mapping a route with multiple paths, which is deprecated and will be removed in Rails 8.1.'
703
+ Enabled: pending
704
+ Severity: warning
705
+ VersionAdded: '2.29'
706
+ Include:
707
+ - config/routes.rb
708
+ - config/routes/**/*.rb
709
+
697
710
  Rails/NegateInclude:
698
711
  Description: 'Prefer `collection.exclude?(obj)` over `!collection.include?(obj)`.'
699
712
  StyleGuide: 'https://rails.rubystyle.guide#exclude'
@@ -1068,6 +1081,15 @@ Rails/StripHeredoc:
1068
1081
  Enabled: pending
1069
1082
  VersionAdded: '2.15'
1070
1083
 
1084
+ Rails/StrongParametersExpect:
1085
+ Description: 'Enforces the use of `ActionController::Parameters#expect` as a method for strong parameter handling.'
1086
+ Reference: 'https://api.rubyonrails.org/classes/ActionController/Parameters.html#method-i-expect'
1087
+ Enabled: pending
1088
+ Include:
1089
+ - app/controllers/**/*.rb
1090
+ SafeAutoCorrect: false
1091
+ VersionAdded: '2.29'
1092
+
1071
1093
  Rails/TableNameAssignment:
1072
1094
  Description: >-
1073
1095
  Do not use `self.table_name =`. Use Inflections or `table_name_prefix` instead.
@@ -87,7 +87,7 @@ module RuboCop
87
87
 
88
88
  options.each_pair.find do |pair|
89
89
  next unless pair.key.sym_type? && pair.key.value == :foreign_key
90
- next unless pair.value.sym_type? || pair.value.str_type?
90
+ next unless pair.value.type?(:sym, :str)
91
91
 
92
92
  break pair.value.value.to_s
93
93
  end
@@ -6,7 +6,72 @@ module RuboCop
6
6
  module IndexMethod # rubocop:disable Metrics/ModuleLength
7
7
  RESTRICT_ON_SEND = %i[each_with_object to_h map collect []].freeze
8
8
 
9
- def on_block(node) # rubocop:todo InternalAffairs/NumblockHandler
9
+ # Internal helper class to hold match data
10
+ Captures = ::Struct.new(
11
+ :transformed_argname,
12
+ :transforming_body_expr
13
+ ) do
14
+ def noop_transformation?
15
+ transforming_body_expr.lvar_type? && transforming_body_expr.children == [transformed_argname]
16
+ end
17
+ end
18
+
19
+ # Internal helper class to hold autocorrect data
20
+ Autocorrection = ::Struct.new(:match, :block_node, :leading, :trailing) do
21
+ def self.from_each_with_object(node, match)
22
+ new(match, node, 0, 0)
23
+ end
24
+
25
+ def self.from_to_h(node, match)
26
+ new(match, node, 0, 0)
27
+ end
28
+
29
+ def self.from_map_to_h(node, match)
30
+ if node.block_literal?
31
+ strip_trailing_chars = 0
32
+ else
33
+ map_range = node.children.first.source_range
34
+ node_range = node.source_range
35
+ strip_trailing_chars = node_range.end_pos - map_range.end_pos
36
+ end
37
+
38
+ new(match, node.children.first, 0, strip_trailing_chars)
39
+ end
40
+
41
+ def self.from_hash_brackets_map(node, match)
42
+ new(match, node.children.last, "#{node.receiver.source}[".length, ']'.length)
43
+ end
44
+
45
+ def strip_prefix_and_suffix(node, corrector)
46
+ expression = node.source_range
47
+ corrector.remove_leading(expression, leading)
48
+ corrector.remove_trailing(expression, trailing)
49
+ end
50
+
51
+ def set_new_method_name(new_method_name, corrector)
52
+ range = block_node.send_node.loc.selector
53
+ if (send_end = block_node.send_node.loc.end)
54
+ # If there are arguments (only true in the `each_with_object` case)
55
+ range = range.begin.join(send_end)
56
+ end
57
+ corrector.replace(range, new_method_name)
58
+ end
59
+
60
+ def set_new_arg_name(transformed_argname, corrector)
61
+ return if block_node.numblock_type?
62
+
63
+ corrector.replace(block_node.arguments, "|#{transformed_argname}|")
64
+ end
65
+
66
+ def set_new_body_expression(transforming_body_expr, corrector)
67
+ body_source = transforming_body_expr.source
68
+ body_source = "{ #{body_source} }" if transforming_body_expr.hash_type? && !transforming_body_expr.braces?
69
+
70
+ corrector.replace(block_node.body, body_source)
71
+ end
72
+ end
73
+
74
+ def on_block(node)
10
75
  on_bad_each_with_object(node) do |*match|
11
76
  handle_possible_offense(node, match, 'each_with_object')
12
77
  end
@@ -18,6 +83,8 @@ module RuboCop
18
83
  end
19
84
  end
20
85
 
86
+ alias on_numblock on_block
87
+
21
88
  def on_send(node)
22
89
  on_bad_map_to_h(node) do |*match|
23
90
  handle_possible_offense(node, match, 'map { ... }.to_h')
@@ -100,66 +167,6 @@ module RuboCop
100
167
  correction.set_new_arg_name(captures.transformed_argname, corrector)
101
168
  correction.set_new_body_expression(captures.transforming_body_expr, corrector)
102
169
  end
103
-
104
- # Internal helper class to hold match data
105
- Captures = ::Struct.new(
106
- :transformed_argname,
107
- :transforming_body_expr
108
- ) do
109
- def noop_transformation?
110
- transforming_body_expr.lvar_type? && transforming_body_expr.children == [transformed_argname]
111
- end
112
- end
113
-
114
- # Internal helper class to hold autocorrect data
115
- Autocorrection = ::Struct.new(:match, :block_node, :leading, :trailing) do
116
- def self.from_each_with_object(node, match)
117
- new(match, node, 0, 0)
118
- end
119
-
120
- def self.from_to_h(node, match)
121
- new(match, node, 0, 0)
122
- end
123
-
124
- def self.from_map_to_h(node, match)
125
- strip_trailing_chars = 0
126
-
127
- unless node.parent&.block_type?
128
- map_range = node.children.first.source_range
129
- node_range = node.source_range
130
- strip_trailing_chars = node_range.end_pos - map_range.end_pos
131
- end
132
-
133
- new(match, node.children.first, 0, strip_trailing_chars)
134
- end
135
-
136
- def self.from_hash_brackets_map(node, match)
137
- new(match, node.children.last, "#{node.receiver.source}[".length, ']'.length)
138
- end
139
-
140
- def strip_prefix_and_suffix(node, corrector)
141
- expression = node.source_range
142
- corrector.remove_leading(expression, leading)
143
- corrector.remove_trailing(expression, trailing)
144
- end
145
-
146
- def set_new_method_name(new_method_name, corrector)
147
- range = block_node.send_node.loc.selector
148
- if (send_end = block_node.send_node.loc.end)
149
- # If there are arguments (only true in the `each_with_object` case)
150
- range = range.begin.join(send_end)
151
- end
152
- corrector.replace(range, new_method_name)
153
- end
154
-
155
- def set_new_arg_name(transformed_argname, corrector)
156
- corrector.replace(block_node.arguments, "|#{transformed_argname}|")
157
- end
158
-
159
- def set_new_body_expression(transforming_body_expr, corrector)
160
- corrector.replace(block_node.body, transforming_body_expr.source)
161
- end
162
- end
163
170
  end
164
171
  end
165
172
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ # Common functionality for cops working with routes.
6
+ module RoutesHelper
7
+ extend NodePattern::Macros
8
+
9
+ HTTP_METHODS = %i[get post put patch delete].freeze
10
+
11
+ def_node_matcher :routes_draw?, <<~PATTERN
12
+ (send (send _ :routes) :draw)
13
+ PATTERN
14
+
15
+ def within_routes?(node)
16
+ node.each_ancestor(:block).any? { |block| routes_draw?(block.send_node) }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -19,6 +19,7 @@ module RuboCop
19
19
  class AddColumnIndex < Base
20
20
  extend AutoCorrector
21
21
  include RangeHelp
22
+ include MigrationsHelper
22
23
 
23
24
  MSG = '`add_column` does not accept an `index` key, use `add_index` instead.'
24
25
  RESTRICT_ON_SEND = %i[add_column].freeze
@@ -66,7 +66,7 @@ module RuboCop
66
66
 
67
67
  def_node_matcher :match_belongs_to_with_options, <<~PATTERN
68
68
  (send _ :belongs_to ...
69
- (hash <$(pair (sym :required) ${true false}) ...>)
69
+ (hash <$(pair (sym :required) $boolean) ...>)
70
70
  )
71
71
  PATTERN
72
72
 
@@ -123,7 +123,7 @@ module RuboCop
123
123
  def on_if(node)
124
124
  return unless cop_config['UnlessPresent']
125
125
  return unless node.unless?
126
- return if node.else? && config.for_cop('Style/UnlessElse')['Enabled']
126
+ return if node.else? && config.cop_enabled?('Style/UnlessElse')
127
127
 
128
128
  unless_present?(node) do |method_call, receiver|
129
129
  range = unless_condition(node, method_call)
@@ -65,6 +65,7 @@ module RuboCop
65
65
  # end
66
66
  class BulkChangeTable < Base
67
67
  include DatabaseTypeResolvable
68
+ include MigrationsHelper
68
69
 
69
70
  MSG_FOR_CHANGE_TABLE = <<~MSG.chomp
70
71
  You can combine alter queries using `bulk: true` options.
@@ -79,7 +79,7 @@ module RuboCop
79
79
  end
80
80
 
81
81
  def allowed_name?(argument)
82
- return false unless argument.str_type? || argument.sym_type?
82
+ return false unless argument.type?(:str, :sym)
83
83
 
84
84
  !/^[a-zA-Z-][a-zA-Z\-0-9]*$/.match?(argument.value)
85
85
  end
@@ -14,6 +14,8 @@ module RuboCop
14
14
  # # good
15
15
  # add_column :users, :saved
16
16
  class DangerousColumnNames < Base # rubocop:disable Metrics/ClassLength
17
+ include MigrationsHelper
18
+
17
19
  COLUMN_TYPE_METHOD_NAMES = %i[
18
20
  bigint
19
21
  binary
@@ -68,12 +68,13 @@ module RuboCop
68
68
 
69
69
  def_node_matcher :delegate?, <<~PATTERN
70
70
  (def _method_name _args
71
- (send {(send nil? _) (self)} _ ...))
71
+ (send {(send nil? _) (self) (send (self) :class) ({cvar gvar ivar} _) (const _ _)} _ ...))
72
72
  PATTERN
73
73
 
74
74
  def on_def(node)
75
75
  return unless trivial_delegate?(node)
76
76
  return if private_or_protected_delegation(node)
77
+ return if module_function_declared?(node)
77
78
 
78
79
  register_offense(node)
79
80
  end
@@ -82,15 +83,40 @@ module RuboCop
82
83
 
83
84
  def register_offense(node)
84
85
  add_offense(node.loc.keyword) do |corrector|
85
- body = node.body
86
+ receiver = determine_register_offense_receiver(node.body.receiver)
87
+ delegation = build_delegation(node, receiver)
86
88
 
87
- receiver = body.receiver.self_type? ? 'self' : ":#{body.receiver.method_name}"
89
+ corrector.replace(node, delegation)
90
+ end
91
+ end
88
92
 
89
- delegation = ["delegate :#{body.method_name}", "to: #{receiver}"]
90
- delegation << ['prefix: true'] if node.method?(prefixed_method_name(node.body))
93
+ def determine_register_offense_receiver(receiver)
94
+ case receiver.type
95
+ when :self
96
+ 'self'
97
+ when :const
98
+ full_name = full_const_name(receiver)
99
+ full_name.include?('::') ? ":'#{full_name}'" : ":#{full_name}"
100
+ when :cvar, :gvar, :ivar
101
+ ":#{receiver.source}"
102
+ else
103
+ ":#{receiver.method_name}"
104
+ end
105
+ end
91
106
 
92
- corrector.replace(node, delegation.join(', '))
107
+ def build_delegation(node, receiver)
108
+ delegation = ["delegate :#{node.body.method_name}", "to: #{receiver}"]
109
+ delegation << ['prefix: true'] if node.method?(prefixed_method_name(node.body))
110
+ delegation.join(', ')
111
+ end
112
+
113
+ def full_const_name(node)
114
+ return unless node.const_type?
115
+ unless node.namespace
116
+ return node.absolute? ? "::#{node.source}" : node.source
93
117
  end
118
+
119
+ "#{full_const_name(node.namespace)}::#{node.short_name}"
94
120
  end
95
121
 
96
122
  def trivial_delegate?(def_node)
@@ -120,13 +146,30 @@ module RuboCop
120
146
  def prefixed_method_name(body)
121
147
  return '' if body.receiver.self_type?
122
148
 
123
- [body.receiver.method_name, body.method_name].join('_').to_sym
149
+ [determine_prefixed_method_receiver_name(body.receiver), body.method_name].join('_').to_sym
150
+ end
151
+
152
+ def determine_prefixed_method_receiver_name(receiver)
153
+ case receiver.type
154
+ when :cvar, :gvar, :ivar
155
+ receiver.source
156
+ when :const
157
+ full_const_name(receiver)
158
+ else
159
+ receiver.method_name.to_s
160
+ end
124
161
  end
125
162
 
126
163
  def private_or_protected_delegation(node)
127
164
  private_or_protected_inline(node) || node_visibility(node) != :public
128
165
  end
129
166
 
167
+ def module_function_declared?(node)
168
+ node.each_ancestor(:module, :begin).any? do |ancestor|
169
+ ancestor.children.any? { |child| child.send_type? && child.method?(:module_function) }
170
+ end
171
+ end
172
+
130
173
  def private_or_protected_inline(node)
131
174
  processed_source[node.first_line - 1].strip.match?(/\A(private )|(protected )/)
132
175
  end
@@ -63,11 +63,15 @@ module RuboCop
63
63
  private
64
64
 
65
65
  def register_offense(name, nodes, message_template)
66
- nodes.each do |node|
67
- add_offense(node, message: format(message_template, name: name)) do |corrector|
68
- next if same_line?(nodes.last, node)
66
+ last_node = nodes.last
69
67
 
70
- corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
68
+ nodes.each_with_index do |node, index|
69
+ add_offense(node, message: format(message_template, name: name)) do |corrector|
70
+ if index.zero?
71
+ corrector.replace(node, last_node.source)
72
+ else
73
+ corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
74
+ end
71
75
  end
72
76
  end
73
77
  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,7 +100,7 @@ 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
106
  autocorrect_file_join(corrector, node) unless node.first_argument.array_type?
@@ -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)