rubocop-rails 2.12.3 → 2.13.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/config/default.yml +40 -8
  4. data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +34 -0
  5. data/lib/rubocop/cop/mixin/migrations_helper.rb +26 -0
  6. data/lib/rubocop/cop/rails/active_record_aliases.rb +6 -2
  7. data/lib/rubocop/cop/rails/application_controller.rb +5 -1
  8. data/lib/rubocop/cop/rails/application_job.rb +5 -1
  9. data/lib/rubocop/cop/rails/application_mailer.rb +5 -1
  10. data/lib/rubocop/cop/rails/application_record.rb +6 -1
  11. data/lib/rubocop/cop/rails/arel_star.rb +6 -0
  12. data/lib/rubocop/cop/rails/blank.rb +5 -4
  13. data/lib/rubocop/cop/rails/compact_blank.rb +99 -0
  14. data/lib/rubocop/cop/rails/content_tag.rb +2 -2
  15. data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +2 -7
  16. data/lib/rubocop/cop/rails/duration_arithmetic.rb +98 -0
  17. data/lib/rubocop/cop/rails/dynamic_find_by.rb +4 -0
  18. data/lib/rubocop/cop/rails/find_each.rb +2 -0
  19. data/lib/rubocop/cop/rails/http_positional_arguments.rb +1 -1
  20. data/lib/rubocop/cop/rails/index_by.rb +6 -6
  21. data/lib/rubocop/cop/rails/index_with.rb +6 -6
  22. data/lib/rubocop/cop/rails/inverse_of.rb +17 -1
  23. data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +8 -7
  24. data/lib/rubocop/cop/rails/mailer_name.rb +4 -0
  25. data/lib/rubocop/cop/rails/negate_include.rb +3 -2
  26. data/lib/rubocop/cop/rails/output.rb +4 -0
  27. data/lib/rubocop/cop/rails/pick.rb +7 -0
  28. data/lib/rubocop/cop/rails/pluck_id.rb +3 -0
  29. data/lib/rubocop/cop/rails/pluck_in_where.rb +7 -6
  30. data/lib/rubocop/cop/rails/rake_environment.rb +5 -0
  31. data/lib/rubocop/cop/rails/read_write_attribute.rb +51 -14
  32. data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +257 -0
  33. data/lib/rubocop/cop/rails/reflection_class_name.rb +4 -2
  34. data/lib/rubocop/cop/rails/relative_date_constant.rb +3 -0
  35. data/lib/rubocop/cop/rails/reversible_migration.rb +5 -3
  36. data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +2 -10
  37. data/lib/rubocop/cop/rails/root_join_chain.rb +72 -0
  38. data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +12 -3
  39. data/lib/rubocop/cop/rails/save_bang.rb +19 -0
  40. data/lib/rubocop/cop/rails/schema_comment.rb +104 -0
  41. data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +4 -2
  42. data/lib/rubocop/cop/rails/time_zone.rb +3 -0
  43. data/lib/rubocop/cop/rails/uniq_before_pluck.rb +29 -35
  44. data/lib/rubocop/cop/rails/unused_ignored_columns.rb +2 -0
  45. data/lib/rubocop/cop/rails/where_equals.rb +4 -0
  46. data/lib/rubocop/cop/rails/where_exists.rb +9 -8
  47. data/lib/rubocop/cop/rails_cops.rb +7 -0
  48. data/lib/rubocop/rails/version.rb +1 -1
  49. metadata +12 -4
@@ -126,6 +126,18 @@ module RuboCop
126
126
  # has_many :physicians, through: :appointments
127
127
  # end
128
128
  #
129
+ # @example IgnoreScopes: false (default)
130
+ # # bad
131
+ # class Blog < ApplicationRecord
132
+ # has_many :posts, -> { order(published_at: :desc) }
133
+ # end
134
+ #
135
+ # @example IgnoreScopes: true
136
+ # # good
137
+ # class Blog < ApplicationRecord
138
+ # has_many :posts, -> { order(published_at: :desc) }
139
+ # end
140
+ #
129
141
  # @see https://guides.rubyonrails.org/association_basics.html#bi-directional-associations
130
142
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Setting+Inverses
131
143
  class InverseOf < Base
@@ -189,7 +201,7 @@ module RuboCop
189
201
  end
190
202
 
191
203
  def scope?(arguments)
192
- arguments.any?(&:block_type?)
204
+ !ignore_scopes? && arguments.any?(&:block_type?)
193
205
  end
194
206
 
195
207
  def options_requiring_inverse_of?(options)
@@ -236,6 +248,10 @@ module RuboCop
236
248
  SPECIFY_MSG
237
249
  end
238
250
  end
251
+
252
+ def ignore_scopes?
253
+ cop_config['IgnoreScopes'] == true
254
+ end
239
255
  end
240
256
  end
241
257
  end
@@ -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,10 +23,21 @@ 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
 
29
- MSG = 'Prefer `%<prefer>s` over `%<current>s`.'
40
+ MSG = 'Prefer `%<prefer>s`.'
30
41
  RESTRICT_ON_SEND = %i[read_attribute write_attribute].freeze
31
42
 
32
43
  def_node_matcher :read_write_attribute?, <<~PATTERN
@@ -38,27 +49,53 @@ 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
- add_offense(node.loc.selector, message: message(node)) do |corrector|
43
- case node.method_name
44
- when :read_attribute
45
- replacement = read_attribute_replacement(node)
46
- when :write_attribute
47
- replacement = write_attribute_replacement(node)
48
- end
49
-
50
- corrector.replace(node.source_range, replacement)
54
+ add_offense(node, message: build_message(node)) do |corrector|
55
+ corrector.replace(node.source_range, node_replacement(node))
51
56
  end
52
57
  end
53
58
 
54
59
  private
55
60
 
56
- def message(node)
61
+ def within_shadowing_method?(node)
62
+ first_arg = node.first_argument
63
+ return false unless first_arg.respond_to?(:value)
64
+
65
+ enclosing_method = node.each_ancestor(:def).first
66
+ return false unless enclosing_method
67
+
68
+ shadowing_method_name = first_arg.value.to_s
69
+ shadowing_method_name << '=' if node.method?(:write_attribute)
70
+ enclosing_method.method?(shadowing_method_name)
71
+ end
72
+
73
+ def build_message(node)
74
+ if node.single_line?
75
+ single_line_message(node)
76
+ else
77
+ multi_line_message(node)
78
+ end
79
+ end
80
+
81
+ def single_line_message(node)
82
+ format(MSG, prefer: node_replacement(node))
83
+ end
84
+
85
+ def multi_line_message(node)
57
86
  if node.method?(:read_attribute)
58
- format(MSG, prefer: 'self[:attr]', current: 'read_attribute(:attr)')
87
+ format(MSG, prefer: 'self[:attr]')
59
88
  else
60
- format(MSG, prefer: 'self[:attr] = val',
61
- current: 'write_attribute(:attr, val)')
89
+ format(MSG, prefer: 'self[:attr] = val')
90
+ end
91
+ end
92
+
93
+ def node_replacement(node)
94
+ case node.method_name
95
+ when :read_attribute
96
+ read_attribute_replacement(node)
97
+ when :write_attribute
98
+ write_attribute_replacement(node)
62
99
  end
63
100
  end
64
101
 
@@ -0,0 +1,257 @@
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
+ # @safety
12
+ # This cop's autocorrection is unsafe because it changes the default error message
13
+ # from "can't be blank" to "must exist".
14
+ #
15
+ # @example
16
+ # # bad
17
+ # belongs_to :user
18
+ # validates :user, presence: true
19
+ #
20
+ # # bad
21
+ # belongs_to :user
22
+ # validates :user_id, presence: true
23
+ #
24
+ # # bad
25
+ # belongs_to :author, foreign_key: :user_id
26
+ # validates :user_id, presence: true
27
+ #
28
+ # # good
29
+ # belongs_to :user
30
+ #
31
+ # # good
32
+ # belongs_to :author, foreign_key: :user_id
33
+ #
34
+ class RedundantPresenceValidationOnBelongsTo < Base
35
+ include RangeHelp
36
+ extend AutoCorrector
37
+ extend TargetRailsVersion
38
+
39
+ MSG = 'Remove explicit presence validation for %<association>s.'
40
+ RESTRICT_ON_SEND = %i[validates].freeze
41
+
42
+ minimum_target_rails_version 5.0
43
+
44
+ # @!method presence_validation?(node)
45
+ # Match a `validates` statement with a presence check
46
+ #
47
+ # @example source that matches - by association
48
+ # validates :user, presence: true
49
+ #
50
+ # @example source that matches - by association
51
+ # validates :name, :user, presence: true
52
+ #
53
+ # @example source that matches - by a foreign key
54
+ # validates :user_id, presence: true
55
+ #
56
+ # @example source that DOES NOT match - strict validation
57
+ # validates :user_id, presence: true, strict: true
58
+ #
59
+ # @example source that DOES NOT match - custom strict validation
60
+ # validates :user_id, presence: true, strict: MissingUserError
61
+ def_node_matcher :presence_validation?, <<~PATTERN
62
+ (
63
+ send nil? :validates
64
+ (sym $_)+
65
+ $[
66
+ (hash <$(pair (sym :presence) true) ...>) # presence: true
67
+ !(hash <$(pair (sym :strict) {true const}) ...>) # strict: true
68
+ ]
69
+ )
70
+ PATTERN
71
+
72
+ # @!method optional?(node)
73
+ # Match a `belongs_to` association with an optional option in a hash
74
+ def_node_matcher :optional?, <<~PATTERN
75
+ (send nil? :belongs_to _ ... #optional_option?)
76
+ PATTERN
77
+
78
+ # @!method optional_option?(node)
79
+ # Match an optional option in a hash
80
+ def_node_matcher :optional_option?, <<~PATTERN
81
+ {
82
+ (hash <(pair (sym :optional) true) ...>) # optional: true
83
+ (hash <(pair (sym :required) false) ...>) # required: false
84
+ }
85
+ PATTERN
86
+
87
+ # @!method any_belongs_to?(node, association:)
88
+ # Match a class with `belongs_to` with no regard to `foreign_key` option
89
+ #
90
+ # @example source that matches
91
+ # belongs_to :user
92
+ #
93
+ # @example source that matches - regardless of `foreign_key`
94
+ # belongs_to :author, foreign_key: :user_id
95
+ #
96
+ # @param node [RuboCop::AST::Node]
97
+ # @param association [Symbol]
98
+ # @return [Array<RuboCop::AST::Node>, nil] matching node
99
+ def_node_matcher :any_belongs_to?, <<~PATTERN
100
+ (begin
101
+ <
102
+ $(send nil? :belongs_to (sym %association) ...)
103
+ ...
104
+ >
105
+ )
106
+ PATTERN
107
+
108
+ # @!method belongs_to?(node, key:, fk:)
109
+ # Match a class with a matching association, either by name or an explicit
110
+ # `foreign_key` option
111
+ #
112
+ # @example source that matches - fk matches `foreign_key` option
113
+ # belongs_to :author, foreign_key: :user_id
114
+ #
115
+ # @example source that matches - key matches association name
116
+ # belongs_to :user
117
+ #
118
+ # @example source that does not match - explicit `foreign_key` does not match
119
+ # belongs_to :user, foreign_key: :account_id
120
+ #
121
+ # @param node [RuboCop::AST::Node]
122
+ # @param key [Symbol] e.g. `:user`
123
+ # @param fk [Symbol] e.g. `:user_id`
124
+ # @return [Array<RuboCop::AST::Node>] matching nodes
125
+ def_node_matcher :belongs_to?, <<~PATTERN
126
+ (begin
127
+ <
128
+ ${
129
+ #belongs_to_without_fk?(%key) # belongs_to :user
130
+ #belongs_to_with_a_matching_fk?(%fk) # belongs_to :author, foreign_key: :user_id
131
+ }
132
+ ...
133
+ >
134
+ )
135
+ PATTERN
136
+
137
+ # @!method belongs_to_without_fk?(node, key)
138
+ # Match a matching `belongs_to` association, without an explicit `foreign_key` option
139
+ #
140
+ # @param node [RuboCop::AST::Node]
141
+ # @param key [Symbol] e.g. `:user`
142
+ # @return [Array<RuboCop::AST::Node>] matching nodes
143
+ def_node_matcher :belongs_to_without_fk?, <<~PATTERN
144
+ {
145
+ (send nil? :belongs_to (sym %1)) # belongs_to :user
146
+ (send nil? :belongs_to (sym %1) !hash) # belongs_to :user, -> { not_deleted }
147
+ (send nil? :belongs_to (sym %1) !(hash <(pair (sym :foreign_key) _) ...>))
148
+ }
149
+ PATTERN
150
+
151
+ # @!method belongs_to_with_a_matching_fk?(node, fk)
152
+ # Match a matching `belongs_to` association with a matching explicit `foreign_key` option
153
+ #
154
+ # @example source that matches
155
+ # belongs_to :author, foreign_key: :user_id
156
+ #
157
+ # @param node [RuboCop::AST::Node]
158
+ # @param fk [Symbol] e.g. `:user_id`
159
+ # @return [Array<RuboCop::AST::Node>] matching nodes
160
+ def_node_matcher :belongs_to_with_a_matching_fk?, <<~PATTERN
161
+ (send nil? :belongs_to ... (hash <(pair (sym :foreign_key) (sym %1)) ...>))
162
+ PATTERN
163
+
164
+ def on_send(node)
165
+ presence_validation?(node) do |all_keys, options, presence|
166
+ keys = non_optional_belongs_to(node.parent, all_keys)
167
+ return if keys.none?
168
+
169
+ add_offense_and_correct(node, all_keys, keys, options, presence)
170
+ end
171
+ end
172
+
173
+ private
174
+
175
+ def add_offense_and_correct(node, all_keys, keys, options, presence)
176
+ add_offense(presence, message: message_for(keys)) do |corrector|
177
+ if options.children.one? # `presence: true` is the only option
178
+ if keys == all_keys
179
+ remove_validation(corrector, node)
180
+ else
181
+ remove_keys_from_validation(corrector, node, keys)
182
+ end
183
+ elsif keys == all_keys
184
+ remove_presence_option(corrector, presence)
185
+ else
186
+ extract_validation_for_keys(corrector, node, keys, options)
187
+ end
188
+ end
189
+ end
190
+
191
+ def message_for(keys)
192
+ display_keys = keys.map { |key| "`#{key}`" }.join('/')
193
+ format(MSG, association: display_keys)
194
+ end
195
+
196
+ def non_optional_belongs_to(node, keys)
197
+ keys.select do |key|
198
+ belongs_to = belongs_to_for(node, key)
199
+ belongs_to && !optional?(belongs_to)
200
+ end
201
+ end
202
+
203
+ def belongs_to_for(model_class_node, key)
204
+ if key.to_s.end_with?('_id')
205
+ normalized_key = key.to_s.delete_suffix('_id').to_sym
206
+ belongs_to?(model_class_node, key: normalized_key, fk: key)
207
+ else
208
+ any_belongs_to?(model_class_node, association: key)
209
+ end
210
+ end
211
+
212
+ def remove_validation(corrector, node)
213
+ corrector.remove(validation_range(node))
214
+ end
215
+
216
+ def remove_keys_from_validation(corrector, node, keys)
217
+ keys.each do |key|
218
+ key_node = node.arguments.find { |arg| arg.value == key }
219
+ key_range = range_with_surrounding_space(
220
+ range: range_with_surrounding_comma(key_node.source_range, :right),
221
+ side: :right
222
+ )
223
+ corrector.remove(key_range)
224
+ end
225
+ end
226
+
227
+ def remove_presence_option(corrector, presence)
228
+ range = range_with_surrounding_comma(
229
+ range_with_surrounding_space(range: presence.source_range, side: :left),
230
+ :left
231
+ )
232
+ corrector.remove(range)
233
+ end
234
+
235
+ def extract_validation_for_keys(corrector, node, keys, options)
236
+ indentation = ' ' * node.source_range.column
237
+ options_without_presence = options.children.reject { |pair| pair.key.value == :presence }
238
+ source = [
239
+ indentation,
240
+ 'validates ',
241
+ keys.map(&:inspect).join(', '),
242
+ ', ',
243
+ options_without_presence.map(&:source).join(', '),
244
+ "\n"
245
+ ].join
246
+
247
+ remove_keys_from_validation(corrector, node, keys)
248
+ corrector.insert_after(validation_range(node), source)
249
+ end
250
+
251
+ def validation_range(node)
252
+ range_by_whole_lines(node.source_range, include_final_newline: true)
253
+ end
254
+ end
255
+ end
256
+ end
257
+ 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
@@ -176,10 +176,12 @@ module RuboCop
176
176
  #
177
177
  # @see https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
178
178
  class ReversibleMigration < Base
179
+ include MigrationsHelper
180
+
179
181
  MSG = '%<action>s is not reversible.'
180
182
 
181
183
  def_node_matcher :irreversible_schema_statement_call, <<~PATTERN
182
- (send nil? ${:execute :remove_belongs_to} ...)
184
+ (send nil? ${:change_column :execute} ...)
183
185
  PATTERN
184
186
 
185
187
  def_node_matcher :drop_table_call, <<~PATTERN
@@ -207,7 +209,7 @@ module RuboCop
207
209
  PATTERN
208
210
 
209
211
  def on_send(node)
210
- return unless within_change_method?(node)
212
+ return unless in_migration?(node) && within_change_method?(node)
211
213
  return if within_reversible_or_up_only_block?(node)
212
214
 
213
215
  check_irreversible_schema_statement_node(node)
@@ -220,7 +222,7 @@ module RuboCop
220
222
  end
221
223
 
222
224
  def on_block(node)
223
- return unless within_change_method?(node)
225
+ return unless in_migration?(node) && within_change_method?(node)
224
226
  return if within_reversible_or_up_only_block?(node)
225
227
  return if node.body.nil?
226
228
 
@@ -43,19 +43,11 @@ module RuboCop
43
43
  # end
44
44
  # end
45
45
  class ReversibleMigrationMethodDefinition < Base
46
+ include MigrationsHelper
47
+
46
48
  MSG = 'Migrations must contain either a `change` method, or ' \
47
49
  'both an `up` and a `down` method.'
48
50
 
49
- def_node_matcher :migration_class?, <<~PATTERN
50
- (class
51
- (const nil? _)
52
- (send
53
- (const (const {nil? cbase} :ActiveRecord) :Migration)
54
- :[]
55
- (float _))
56
- _)
57
- PATTERN
58
-
59
51
  def_node_matcher :change_method?, <<~PATTERN
60
52
  [ #migration_class? `(def :change (args) _) ]
61
53
  PATTERN
@@ -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