rubocop-rails 2.12.3 → 2.13.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/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