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.
- checksums.yaml +4 -4
- data/LICENSE.txt +1 -1
- data/config/default.yml +40 -8
- data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +34 -0
- data/lib/rubocop/cop/mixin/migrations_helper.rb +26 -0
- data/lib/rubocop/cop/rails/active_record_aliases.rb +6 -2
- data/lib/rubocop/cop/rails/application_controller.rb +5 -1
- data/lib/rubocop/cop/rails/application_job.rb +5 -1
- data/lib/rubocop/cop/rails/application_mailer.rb +5 -1
- data/lib/rubocop/cop/rails/application_record.rb +6 -1
- data/lib/rubocop/cop/rails/arel_star.rb +6 -0
- data/lib/rubocop/cop/rails/blank.rb +5 -4
- data/lib/rubocop/cop/rails/compact_blank.rb +99 -0
- data/lib/rubocop/cop/rails/content_tag.rb +2 -2
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +2 -7
- data/lib/rubocop/cop/rails/duration_arithmetic.rb +98 -0
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +4 -0
- data/lib/rubocop/cop/rails/find_each.rb +2 -0
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +1 -1
- data/lib/rubocop/cop/rails/index_by.rb +6 -6
- data/lib/rubocop/cop/rails/index_with.rb +6 -6
- data/lib/rubocop/cop/rails/inverse_of.rb +17 -1
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +8 -7
- data/lib/rubocop/cop/rails/mailer_name.rb +4 -0
- data/lib/rubocop/cop/rails/negate_include.rb +3 -2
- data/lib/rubocop/cop/rails/output.rb +4 -0
- data/lib/rubocop/cop/rails/pick.rb +7 -0
- data/lib/rubocop/cop/rails/pluck_id.rb +3 -0
- data/lib/rubocop/cop/rails/pluck_in_where.rb +7 -6
- data/lib/rubocop/cop/rails/rake_environment.rb +5 -0
- data/lib/rubocop/cop/rails/read_write_attribute.rb +51 -14
- data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +257 -0
- data/lib/rubocop/cop/rails/reflection_class_name.rb +4 -2
- data/lib/rubocop/cop/rails/relative_date_constant.rb +3 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +5 -3
- data/lib/rubocop/cop/rails/reversible_migration_method_definition.rb +2 -10
- data/lib/rubocop/cop/rails/root_join_chain.rb +72 -0
- data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +12 -3
- data/lib/rubocop/cop/rails/save_bang.rb +19 -0
- data/lib/rubocop/cop/rails/schema_comment.rb +104 -0
- data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +4 -2
- data/lib/rubocop/cop/rails/time_zone.rb +3 -0
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +29 -35
- data/lib/rubocop/cop/rails/unused_ignored_columns.rb +2 -0
- data/lib/rubocop/cop/rails/where_equals.rb +4 -0
- data/lib/rubocop/cop/rails/where_exists.rb +9 -8
- data/lib/rubocop/cop/rails_cops.rb +7 -0
- data/lib/rubocop/rails/version.rb +1 -1
- 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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
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
|
-
#
|
10
|
-
#
|
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
|
@@ -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
|
-
#
|
17
|
-
# `
|
18
|
-
#
|
19
|
-
# `
|
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
|
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
|
43
|
-
|
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
|
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]'
|
87
|
+
format(MSG, prefer: 'self[:attr]')
|
59
88
|
else
|
60
|
-
format(MSG, prefer: 'self[:attr] = val'
|
61
|
-
|
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
|
-
#
|
9
|
-
#
|
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? ${:
|
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
|