rubocop-rails 2.12.2 → 2.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/config/default.yml +43 -8
  4. data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +34 -0
  5. data/lib/rubocop/cop/rails/active_record_aliases.rb +6 -2
  6. data/lib/rubocop/cop/rails/application_controller.rb +5 -1
  7. data/lib/rubocop/cop/rails/application_job.rb +5 -1
  8. data/lib/rubocop/cop/rails/application_mailer.rb +5 -1
  9. data/lib/rubocop/cop/rails/application_record.rb +6 -1
  10. data/lib/rubocop/cop/rails/arel_star.rb +6 -0
  11. data/lib/rubocop/cop/rails/blank.rb +5 -4
  12. data/lib/rubocop/cop/rails/compact_blank.rb +98 -0
  13. data/lib/rubocop/cop/rails/content_tag.rb +15 -8
  14. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +2 -7
  15. data/lib/rubocop/cop/rails/duration_arithmetic.rb +97 -0
  16. data/lib/rubocop/cop/rails/dynamic_find_by.rb +4 -0
  17. data/lib/rubocop/cop/rails/find_each.rb +13 -0
  18. data/lib/rubocop/cop/rails/http_positional_arguments.rb +1 -1
  19. data/lib/rubocop/cop/rails/index_by.rb +6 -6
  20. data/lib/rubocop/cop/rails/index_with.rb +6 -6
  21. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +8 -7
  22. data/lib/rubocop/cop/rails/mailer_name.rb +4 -0
  23. data/lib/rubocop/cop/rails/negate_include.rb +3 -2
  24. data/lib/rubocop/cop/rails/output.rb +4 -0
  25. data/lib/rubocop/cop/rails/pick.rb +7 -0
  26. data/lib/rubocop/cop/rails/pluck_id.rb +3 -0
  27. data/lib/rubocop/cop/rails/pluck_in_where.rb +7 -6
  28. data/lib/rubocop/cop/rails/rake_environment.rb +5 -0
  29. data/lib/rubocop/cop/rails/read_write_attribute.rb +21 -0
  30. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +256 -0
  31. data/lib/rubocop/cop/rails/reflection_class_name.rb +4 -2
  32. data/lib/rubocop/cop/rails/relative_date_constant.rb +4 -1
  33. data/lib/rubocop/cop/rails/reversible_migration.rb +11 -3
  34. data/lib/rubocop/cop/rails/root_join_chain.rb +72 -0
  35. data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +12 -3
  36. data/lib/rubocop/cop/rails/save_bang.rb +19 -0
  37. data/lib/rubocop/cop/rails/schema_comment.rb +104 -0
  38. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +4 -2
  39. data/lib/rubocop/cop/rails/time_zone.rb +3 -0
  40. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +29 -35
  41. data/lib/rubocop/cop/rails/unique_validation_without_index.rb +1 -1
  42. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +2 -0
  43. data/lib/rubocop/cop/rails/where_equals.rb +4 -0
  44. data/lib/rubocop/cop/rails/where_exists.rb +9 -8
  45. data/lib/rubocop/cop/rails_cops.rb +6 -0
  46. data/lib/rubocop/rails/version.rb +1 -1
  47. metadata +11 -4
@@ -26,22 +26,22 @@ module RuboCop
26
26
 
27
27
  def_node_matcher :on_bad_each_with_object, <<~PATTERN
28
28
  (block
29
- ({send csend} _ :each_with_object (hash))
29
+ (call _ :each_with_object (hash))
30
30
  (args (arg $_el) (arg _memo))
31
- ({send csend} (lvar _memo) :[]= (lvar _el) $!`_memo))
31
+ (call (lvar _memo) :[]= (lvar _el) $!`_memo))
32
32
  PATTERN
33
33
 
34
34
  def_node_matcher :on_bad_to_h, <<~PATTERN
35
35
  (block
36
- ({send csend} _ :to_h)
36
+ (call _ :to_h)
37
37
  (args (arg $_el))
38
38
  (array (lvar _el) $_))
39
39
  PATTERN
40
40
 
41
41
  def_node_matcher :on_bad_map_to_h, <<~PATTERN
42
- ({send csend}
42
+ (call
43
43
  (block
44
- ({send csend} _ {:map :collect})
44
+ (call _ {:map :collect})
45
45
  (args (arg $_el))
46
46
  (array (lvar _el) $_))
47
47
  :to_h)
@@ -52,7 +52,7 @@ module RuboCop
52
52
  (const _ :Hash)
53
53
  :[]
54
54
  (block
55
- ({send csend} _ {:map :collect})
55
+ (call _ {:map :collect})
56
56
  (args (arg $_el))
57
57
  (array (lvar _el) $_)))
58
58
  PATTERN
@@ -6,13 +6,14 @@ module RuboCop
6
6
  # This cop checks that methods specified in the filter's `only` or
7
7
  # `except` options are defined within the same class or module.
8
8
  #
9
- # You can technically specify methods of superclass or methods added by
10
- # mixins on the filter, but these can confuse developers. If you specify
11
- # methods that are defined in other classes or modules, you should
12
- # define the filter in that class or module.
13
- #
14
- # If you rely on behaviour defined in the superclass actions, you must
15
- # remember to invoke `super` in the subclass actions.
9
+ # @safety
10
+ # You can technically specify methods of superclass or methods added by
11
+ # mixins on the filter, but these can confuse developers. If you specify
12
+ # methods that are defined in other classes or modules, you should
13
+ # define the filter in that class or module.
14
+ #
15
+ # If you rely on behaviour defined in the superclass actions, you must
16
+ # remember to invoke `super` in the subclass actions.
16
17
  #
17
18
  # @example
18
19
  # # bad
@@ -8,6 +8,10 @@ module RuboCop
8
8
  # Without the `Mailer` suffix it isn't immediately apparent what's a mailer
9
9
  # and which views are related to the mailer.
10
10
  #
11
+ # @safety
12
+ # This cop's autocorrection is unsafe because renaming a constant is
13
+ # always an unsafe operation.
14
+ #
11
15
  # @example
12
16
  # # bad
13
17
  # class User < ActionMailer::Base
@@ -6,8 +6,9 @@ module RuboCop
6
6
  # This cop enforces the use of `collection.exclude?(obj)`
7
7
  # over `!collection.include?(obj)`.
8
8
  #
9
- # It is marked as unsafe by default because false positive will occur for
10
- # a receiver object that do not have `exclude?` method. (e.g. `IPAddr`)
9
+ # @safety
10
+ # This cop is unsafe because false positive will occur for
11
+ # receiver objects that do not have an `exclude?` method. (e.g. `IPAddr`)
11
12
  #
12
13
  # @example
13
14
  # # bad
@@ -5,6 +5,10 @@ module RuboCop
5
5
  module Rails
6
6
  # This cop checks for the use of output calls like puts and print
7
7
  #
8
+ # @safety
9
+ # This cop's autocorrection is unsafe because depending on the Rails log level configuration,
10
+ # changing from `puts` to `Rails.logger.debug` could result in no output being shown.
11
+ #
8
12
  # @example
9
13
  # # bad
10
14
  # puts 'A debug message'
@@ -9,6 +9,13 @@ module RuboCop
9
9
  # `pick` avoids. When called on an Active Record relation, `pick` adds a
10
10
  # limit to the query so that only one value is fetched from the database.
11
11
  #
12
+ # @safety
13
+ # This cop is unsafe because `pluck` is defined on both `ActiveRecord::Relation` and `Enumerable`,
14
+ # whereas `pick` is only defined on `ActiveRecord::Relation` in Rails 6.0. This was addressed
15
+ # in Rails 6.1 via rails/rails#38760, at which point the cop is safe.
16
+ #
17
+ # See: https://github.com/rubocop/rubocop-rails/pull/249
18
+ #
12
19
  # @example
13
20
  # # bad
14
21
  # Model.pluck(:a).first
@@ -5,6 +5,9 @@ module RuboCop
5
5
  module Rails
6
6
  # This cop enforces the use of `ids` over `pluck(:id)` and `pluck(primary_key)`.
7
7
  #
8
+ # @safety
9
+ # This cop is unsafe if the receiver object is not an Active Record object.
10
+ #
8
11
  # @example
9
12
  # # bad
10
13
  # User.pluck(:id)
@@ -9,14 +9,15 @@ module RuboCop
9
9
  # Since `pluck` is an eager method and hits the database immediately,
10
10
  # using `select` helps to avoid additional database queries.
11
11
  #
12
- # This cop has two different enforcement modes. When the EnforcedStyle
13
- # is conservative (the default) then only calls to `pluck` on a constant
12
+ # This cop has two different enforcement modes. When the `EnforcedStyle`
13
+ # is `conservative` (the default) then only calls to `pluck` on a constant
14
14
  # (i.e. a model class) in the `where` is used as offenses.
15
15
  #
16
- # When the EnforcedStyle is aggressive then all calls to `pluck` in the
17
- # `where` is used as offenses. This may lead to false positives
18
- # as the cop cannot replace to `select` between calls to `pluck` on an
19
- # `ActiveRecord::Relation` instance vs a call to `pluck` on an `Array` instance.
16
+ # @safety
17
+ # When the `EnforcedStyle` is `aggressive` then all calls to `pluck` in the
18
+ # `where` is used as offenses. This may lead to false positives
19
+ # as the cop cannot replace to `select` between calls to `pluck` on an
20
+ # `ActiveRecord::Relation` instance vs a call to `pluck` on an `Array` instance.
20
21
  #
21
22
  # @example
22
23
  # # bad
@@ -14,6 +14,11 @@ module RuboCop
14
14
  # * The task does not need application code.
15
15
  # * The task invokes the `:environment` task.
16
16
  #
17
+ # @safety
18
+ # Probably not a problem in most cases, but it is possible that calling `:environment` task
19
+ # will break a behavior. It's also slower. E.g. some task that only needs one gem to be
20
+ # loaded to run will run significantly faster without loading the whole application.
21
+ #
17
22
  # @example
18
23
  # # bad
19
24
  # task :foo do
@@ -23,6 +23,17 @@ module RuboCop
23
23
  # # good
24
24
  # x = self[:attr]
25
25
  # self[:attr] = val
26
+ #
27
+ # When called from within a method with the same name as the attribute,
28
+ # `read_attribute` and `write_attribute` must be used to prevent an
29
+ # infinite loop:
30
+ #
31
+ # @example
32
+ #
33
+ # # good
34
+ # def foo
35
+ # bar || read_attribute(:foo)
36
+ # end
26
37
  class ReadWriteAttribute < Base
27
38
  extend AutoCorrector
28
39
 
@@ -38,6 +49,7 @@ module RuboCop
38
49
 
39
50
  def on_send(node)
40
51
  return unless read_write_attribute?(node)
52
+ return if within_shadowing_method?(node)
41
53
 
42
54
  add_offense(node.loc.selector, message: message(node)) do |corrector|
43
55
  case node.method_name
@@ -53,6 +65,15 @@ module RuboCop
53
65
 
54
66
  private
55
67
 
68
+ def within_shadowing_method?(node)
69
+ node.each_ancestor(:def).any? do |enclosing_method|
70
+ shadowing_method_name = node.first_argument.value.to_s
71
+ shadowing_method_name << '=' if node.method?(:write_attribute)
72
+
73
+ enclosing_method.method_name.to_s == shadowing_method_name
74
+ end
75
+ end
76
+
56
77
  def message(node)
57
78
  if node.method?(:read_attribute)
58
79
  format(MSG, prefer: 'self[:attr]', current: 'read_attribute(:attr)')
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Since Rails 5.0 the default for `belongs_to` is `optional: false`
7
+ # unless `config.active_record.belongs_to_required_by_default` is
8
+ # explicitly set to `false`. The presence validator is added
9
+ # automatically, and explicit presence validation is redundant.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # belongs_to :user
14
+ # validates :user, presence: true
15
+ #
16
+ # # bad
17
+ # belongs_to :user
18
+ # validates :user_id, presence: true
19
+ #
20
+ # # bad
21
+ # belongs_to :author, foreign_key: :user_id
22
+ # validates :user_id, presence: true
23
+ #
24
+ # # good
25
+ # belongs_to :user
26
+ #
27
+ # # good
28
+ # belongs_to :author, foreign_key: :user_id
29
+ #
30
+ class RedundantPresenceValidationOnBelongsTo < Base
31
+ include RangeHelp
32
+ extend AutoCorrector
33
+ extend TargetRailsVersion
34
+
35
+ MSG = 'Remove explicit presence validation for %<association>s.'
36
+ RESTRICT_ON_SEND = %i[validates].freeze
37
+
38
+ minimum_target_rails_version 5.0
39
+
40
+ # @!method presence_validation?(node)
41
+ # Match a `validates` statement with a presence check
42
+ #
43
+ # @example source that matches - by association
44
+ # validates :user, presence: true
45
+ #
46
+ # @example source that matches - by association
47
+ # validates :name, :user, presence: true
48
+ #
49
+ # @example source that matches - with presence options
50
+ # validates :user, presence: { message: 'duplicate' }
51
+ #
52
+ # @example source that matches - by a foreign key
53
+ # validates :user_id, presence: true
54
+ #
55
+ # @example source that DOES NOT match - strict validation
56
+ # validates :user_id, presence: true, strict: true
57
+ #
58
+ # @example source that DOES NOT match - custom strict validation
59
+ # validates :user_id, presence: true, strict: MissingUserError
60
+ def_node_matcher :presence_validation?, <<~PATTERN
61
+ (
62
+ send nil? :validates
63
+ (sym $_)+
64
+ $[
65
+ (hash <$(pair (sym :presence) {true hash}) ...>) # presence: true
66
+ !(hash <$(pair (sym :strict) {true const}) ...>) # strict: true
67
+ ]
68
+ )
69
+ PATTERN
70
+
71
+ # @!method optional?(node)
72
+ # Match a `belongs_to` association with an optional option in a hash
73
+ def_node_matcher :optional?, <<~PATTERN
74
+ (send nil? :belongs_to _ ... #optional_option?)
75
+ PATTERN
76
+
77
+ # @!method optional_option?(node)
78
+ # Match an optional option in a hash
79
+ def_node_matcher :optional_option?, <<~PATTERN
80
+ {
81
+ (hash <(pair (sym :optional) true) ...>) # optional: true
82
+ (hash <(pair (sym :required) false) ...>) # required: false
83
+ }
84
+ PATTERN
85
+
86
+ # @!method any_belongs_to?(node, association:)
87
+ # Match a class with `belongs_to` with no regard to `foreign_key` option
88
+ #
89
+ # @example source that matches
90
+ # belongs_to :user
91
+ #
92
+ # @example source that matches - regardless of `foreign_key`
93
+ # belongs_to :author, foreign_key: :user_id
94
+ #
95
+ # @param node [RuboCop::AST::Node]
96
+ # @param association [Symbol]
97
+ # @return [Array<RuboCop::AST::Node>, nil] matching node
98
+ def_node_matcher :any_belongs_to?, <<~PATTERN
99
+ (begin
100
+ <
101
+ $(send nil? :belongs_to (sym %association) ...)
102
+ ...
103
+ >
104
+ )
105
+ PATTERN
106
+
107
+ # @!method belongs_to?(node, key:, fk:)
108
+ # Match a class with a matching association, either by name or an explicit
109
+ # `foreign_key` option
110
+ #
111
+ # @example source that matches - fk matches `foreign_key` option
112
+ # belongs_to :author, foreign_key: :user_id
113
+ #
114
+ # @example source that matches - key matches association name
115
+ # belongs_to :user
116
+ #
117
+ # @example source that does not match - explicit `foreign_key` does not match
118
+ # belongs_to :user, foreign_key: :account_id
119
+ #
120
+ # @param node [RuboCop::AST::Node]
121
+ # @param key [Symbol] e.g. `:user`
122
+ # @param fk [Symbol] e.g. `:user_id`
123
+ # @return [Array<RuboCop::AST::Node>] matching nodes
124
+ def_node_matcher :belongs_to?, <<~PATTERN
125
+ (begin
126
+ <
127
+ ${
128
+ #belongs_to_without_fk?(%key) # belongs_to :user
129
+ #belongs_to_with_a_matching_fk?(%fk) # belongs_to :author, foreign_key: :user_id
130
+ }
131
+ ...
132
+ >
133
+ )
134
+ PATTERN
135
+
136
+ # @!method belongs_to_without_fk?(node, key)
137
+ # Match a matching `belongs_to` association, without an explicit `foreign_key` option
138
+ #
139
+ # @param node [RuboCop::AST::Node]
140
+ # @param key [Symbol] e.g. `:user`
141
+ # @return [Array<RuboCop::AST::Node>] matching nodes
142
+ def_node_matcher :belongs_to_without_fk?, <<~PATTERN
143
+ {
144
+ (send nil? :belongs_to (sym %1)) # belongs_to :user
145
+ (send nil? :belongs_to (sym %1) !hash) # belongs_to :user, -> { not_deleted }
146
+ (send nil? :belongs_to (sym %1) !(hash <(pair (sym :foreign_key) _) ...>))
147
+ }
148
+ PATTERN
149
+
150
+ # @!method belongs_to_with_a_matching_fk?(node, fk)
151
+ # Match a matching `belongs_to` association with a matching explicit `foreign_key` option
152
+ #
153
+ # @example source that matches
154
+ # belongs_to :author, foreign_key: :user_id
155
+ #
156
+ # @param node [RuboCop::AST::Node]
157
+ # @param fk [Symbol] e.g. `:user_id`
158
+ # @return [Array<RuboCop::AST::Node>] matching nodes
159
+ def_node_matcher :belongs_to_with_a_matching_fk?, <<~PATTERN
160
+ (send nil? :belongs_to ... (hash <(pair (sym :foreign_key) (sym %1)) ...>))
161
+ PATTERN
162
+
163
+ def on_send(node)
164
+ presence_validation?(node) do |all_keys, options, presence|
165
+ keys = non_optional_belongs_to(node.parent, all_keys)
166
+ return if keys.none?
167
+
168
+ add_offense_and_correct(node, all_keys, keys, options, presence)
169
+ end
170
+ end
171
+
172
+ private
173
+
174
+ def add_offense_and_correct(node, all_keys, keys, options, presence)
175
+ add_offense(presence, message: message_for(keys)) do |corrector|
176
+ if options.children.one? # `presence: true` is the only option
177
+ if keys == all_keys
178
+ remove_validation(corrector, node)
179
+ else
180
+ remove_keys_from_validation(corrector, node, keys)
181
+ end
182
+ elsif keys == all_keys
183
+ remove_presence_option(corrector, presence)
184
+ else
185
+ extract_validation_for_keys(corrector, node, keys, options)
186
+ end
187
+ end
188
+ end
189
+
190
+ def message_for(keys)
191
+ display_keys = keys.map { |key| "`#{key}`" }.join('/')
192
+ format(MSG, association: display_keys)
193
+ end
194
+
195
+ def non_optional_belongs_to(node, keys)
196
+ keys.select do |key|
197
+ belongs_to = belongs_to_for(node, key)
198
+ belongs_to && !optional?(belongs_to)
199
+ end
200
+ end
201
+
202
+ def belongs_to_for(model_class_node, key)
203
+ if key.to_s.end_with?('_id')
204
+ normalized_key = key.to_s.delete_suffix('_id').to_sym
205
+ belongs_to?(model_class_node, key: normalized_key, fk: key)
206
+ else
207
+ any_belongs_to?(model_class_node, association: key)
208
+ end
209
+ end
210
+
211
+ def remove_validation(corrector, node)
212
+ corrector.remove(validation_range(node))
213
+ end
214
+
215
+ def remove_keys_from_validation(corrector, node, keys)
216
+ keys.each do |key|
217
+ key_node = node.arguments.find { |arg| arg.value == key }
218
+ key_range = range_with_surrounding_space(
219
+ range: range_with_surrounding_comma(key_node.source_range, :right),
220
+ side: :right
221
+ )
222
+ corrector.remove(key_range)
223
+ end
224
+ end
225
+
226
+ def remove_presence_option(corrector, presence)
227
+ range = range_with_surrounding_comma(
228
+ range_with_surrounding_space(range: presence.source_range, side: :left),
229
+ :left
230
+ )
231
+ corrector.remove(range)
232
+ end
233
+
234
+ def extract_validation_for_keys(corrector, node, keys, options)
235
+ indentation = ' ' * node.source_range.column
236
+ options_without_presence = options.children.reject { |pair| pair.key.value == :presence }
237
+ source = [
238
+ indentation,
239
+ 'validates ',
240
+ keys.map(&:inspect).join(', '),
241
+ ', ',
242
+ options_without_presence.map(&:source).join(', '),
243
+ "\n"
244
+ ].join
245
+
246
+ remove_keys_from_validation(corrector, node, keys)
247
+ corrector.insert_after(validation_range(node), source)
248
+ end
249
+
250
+ def validation_range(node)
251
+ range_by_whole_lines(node.source_range, include_final_newline: true)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -5,8 +5,10 @@ module RuboCop
5
5
  module Rails
6
6
  # This cop checks if the value of the option `class_name`, in
7
7
  # the definition of a reflection is a string.
8
- # It is marked as unsafe because it cannot be determined whether
9
- # constant or method return value specified to `class_name` is a string.
8
+ #
9
+ # @safety
10
+ # This cop is unsafe because it cannot be determined whether
11
+ # constant or method return value specified to `class_name` is a string.
10
12
  #
11
13
  # @example
12
14
  # # bad
@@ -6,6 +6,9 @@ module RuboCop
6
6
  # This cop checks whether constant value isn't relative date.
7
7
  # Because the relative date will be evaluated only once.
8
8
  #
9
+ # @safety
10
+ # This cop's autocorrection is unsafe because its dependence on the constant is not corrected.
11
+ #
9
12
  # @example
10
13
  # # bad
11
14
  # class SomeClass
@@ -90,7 +93,7 @@ module RuboCop
90
93
  end
91
94
 
92
95
  def nested_relative_date(node, &callback)
93
- return if node.block_type?
96
+ return if node.nil? || node.block_type?
94
97
 
95
98
  node.each_child_node do |child|
96
99
  nested_relative_date(child, &callback)
@@ -179,7 +179,7 @@ module RuboCop
179
179
  MSG = '%<action>s is not reversible.'
180
180
 
181
181
  def_node_matcher :irreversible_schema_statement_call, <<~PATTERN
182
- (send nil? ${:execute :remove_belongs_to} ...)
182
+ (send nil? ${:change_column :execute} ...)
183
183
  PATTERN
184
184
 
185
185
  def_node_matcher :drop_table_call, <<~PATTERN
@@ -317,16 +317,24 @@ module RuboCop
317
317
  return if receiver != node.receiver &&
318
318
  reversible_change_table_call?(node)
319
319
 
320
+ action = if method_name == :remove
321
+ target_rails_version >= 6.1 ? 't.remove (without type)' : 't.remove'
322
+ else
323
+ "change_table(with #{method_name})"
324
+ end
325
+
320
326
  add_offense(
321
327
  node,
322
- message: format(MSG, action: "change_table(with #{method_name})")
328
+ message: format(MSG, action: action)
323
329
  )
324
330
  end
325
331
 
326
332
  def reversible_change_table_call?(node)
327
333
  case node.method_name
328
- when :change, :remove
334
+ when :change
329
335
  false
336
+ when :remove
337
+ target_rails_version >= 6.1 && all_hash_key?(node.arguments.last, :type)
330
338
  when :change_default, :change_column_default, :change_table_comment,
331
339
  :change_column_comment
332
340
  all_hash_key?(node.arguments.last, :from, :to)
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Rails
6
+ # Use a single `#join` instead of chaining on `Rails.root` or `Rails.public_path`.
7
+ #
8
+ # @example
9
+ # # bad
10
+ # Rails.root.join('db').join('schema.rb')
11
+ # Rails.root.join('db').join(migrate).join('migration.rb')
12
+ # Rails.public_path.join('path').join('file.pdf')
13
+ # Rails.public_path.join('path').join(to).join('file.pdf')
14
+ #
15
+ # # good
16
+ # Rails.root.join('db', 'schema.rb')
17
+ # Rails.root.join('db', migrate, 'migration.rb')
18
+ # Rails.public_path.join('path', 'file.pdf')
19
+ # Rails.public_path.join('path', to, 'file.pdf')
20
+ #
21
+ class RootJoinChain < Base
22
+ extend AutoCorrector
23
+ include RangeHelp
24
+
25
+ MSG = 'Use `%<root>s.join(...)` instead of chaining `#join` calls.'
26
+
27
+ RESTRICT_ON_SEND = %i[join].to_set.freeze
28
+
29
+ # @!method rails_root?(node)
30
+ def_node_matcher :rails_root?, <<~PATTERN
31
+ (send (const {nil? cbase} :Rails) {:root :public_path})
32
+ PATTERN
33
+
34
+ # @!method join?(node)
35
+ def_node_matcher :join?, <<~PATTERN
36
+ (send _ :join $...)
37
+ PATTERN
38
+
39
+ def on_send(node)
40
+ evidence(node) do |rails_node, args|
41
+ add_offense(node, message: format(MSG, root: rails_node.source)) do |corrector|
42
+ range = range_between(rails_node.loc.selector.end_pos, node.loc.expression.end_pos)
43
+ replacement = ".join(#{args.map(&:source).join(', ')})"
44
+
45
+ corrector.replace(range, replacement)
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def evidence(node)
53
+ # Are we at the *end* of the join chain?
54
+ return if join?(node.parent)
55
+ # Is there only one join?
56
+ return if rails_root?(node.receiver)
57
+
58
+ all_args = []
59
+
60
+ while (args = join?(node))
61
+ all_args = args + all_args
62
+ node = node.receiver
63
+ end
64
+
65
+ rails_root?(node) do
66
+ yield(node, all_args)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -6,9 +6,18 @@ module RuboCop
6
6
  # This cop checks to make sure safe navigation isn't used with `blank?` in
7
7
  # a conditional.
8
8
  #
9
- # While the safe navigation operator is generally a good idea, when
10
- # checking `foo&.blank?` in a conditional, `foo` being `nil` will actually
11
- # do the opposite of what the author intends.
9
+ # @safety
10
+ # While the safe navigation operator is generally a good idea, when
11
+ # checking `foo&.blank?` in a conditional, `foo` being `nil` will actually
12
+ # do the opposite of what the author intends.
13
+ #
14
+ # For example:
15
+ #
16
+ # [source,ruby]
17
+ # ----
18
+ # foo&.blank? #=> nil
19
+ # foo.blank? #=> true
20
+ # ----
12
21
  #
13
22
  # @example
14
23
  # # bad
@@ -25,6 +25,25 @@ module RuboCop
25
25
  # You can permit receivers that are giving false positives with
26
26
  # `AllowedReceivers: []`
27
27
  #
28
+ # @safety
29
+ # This cop's autocorrection is unsafe because a custom `update` method call would be changed to `update!`,
30
+ # but the method name in the definition would be unchanged.
31
+ #
32
+ # [source,ruby]
33
+ # ----
34
+ # # Original code
35
+ # def update_attributes
36
+ # end
37
+ #
38
+ # update_attributes
39
+ #
40
+ # # After running rubocop --safe-auto-correct
41
+ # def update_attributes
42
+ # end
43
+ #
44
+ # update
45
+ # ----
46
+ #
28
47
  # @example
29
48
  #
30
49
  # # bad