rubocop-rails 2.12.4 → 2.13.1
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 +34 -6
- data/lib/rubocop/cop/mixin/active_record_migrations_helper.rb +34 -0
- data/lib/rubocop/cop/rails/active_record_aliases.rb +4 -0
- 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 +98 -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 +97 -0
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +4 -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/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 +21 -0
- data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +256 -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 +1 -1
- 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 +6 -0
- data/lib/rubocop/rails/version.rb +1 -1
- metadata +11 -4
@@ -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,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
|
-
#
|
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
|
@@ -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? ${:
|
182
|
+
(send nil? ${:change_column :execute} ...)
|
183
183
|
PATTERN
|
184
184
|
|
185
185
|
def_node_matcher :drop_table_call, <<~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
|
@@ -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
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop enforces the use of the `comment` option when adding a new table or column
|
7
|
+
# to the database during a migration.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad (no comment for a new column or table)
|
11
|
+
# add_column :table, :column, :integer
|
12
|
+
#
|
13
|
+
# create_table :table do |t|
|
14
|
+
# t.type :column
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# add_column :table, :column, :integer, comment: 'Number of offenses'
|
19
|
+
#
|
20
|
+
# create_table :table, comment: 'Table of offenses data' do |t|
|
21
|
+
# t.type :column, comment: 'Number of offenses'
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
class SchemaComment < Base
|
25
|
+
include ActiveRecordMigrationsHelper
|
26
|
+
|
27
|
+
COLUMN_MSG = 'New database column without `comment`.'
|
28
|
+
TABLE_MSG = 'New database table without `comment`.'
|
29
|
+
RESTRICT_ON_SEND = %i[add_column create_table].freeze
|
30
|
+
CREATE_TABLE_COLUMN_METHODS = Set[
|
31
|
+
*(
|
32
|
+
RAILS_ABSTRACT_SCHEMA_DEFINITIONS |
|
33
|
+
RAILS_ABSTRACT_SCHEMA_DEFINITIONS_HELPERS |
|
34
|
+
POSTGRES_SCHEMA_DEFINITIONS |
|
35
|
+
MYSQL_SCHEMA_DEFINITIONS
|
36
|
+
)
|
37
|
+
].freeze
|
38
|
+
|
39
|
+
# @!method comment_present?(node)
|
40
|
+
def_node_matcher :comment_present?, <<~PATTERN
|
41
|
+
(hash <(pair {(sym :comment) (str "comment")} (_ [present?])) ...>)
|
42
|
+
PATTERN
|
43
|
+
|
44
|
+
# @!method add_column?(node)
|
45
|
+
def_node_matcher :add_column?, <<~PATTERN
|
46
|
+
(send nil? :add_column _table _column _type _?)
|
47
|
+
PATTERN
|
48
|
+
|
49
|
+
# @!method add_column_with_comment?(node)
|
50
|
+
def_node_matcher :add_column_with_comment?, <<~PATTERN
|
51
|
+
(send nil? :add_column _table _column _type #comment_present?)
|
52
|
+
PATTERN
|
53
|
+
|
54
|
+
# @!method create_table?(node)
|
55
|
+
def_node_matcher :create_table?, <<~PATTERN
|
56
|
+
(send nil? :create_table _table _?)
|
57
|
+
PATTERN
|
58
|
+
|
59
|
+
# @!method create_table?(node)
|
60
|
+
def_node_matcher :create_table_with_comment?, <<~PATTERN
|
61
|
+
(send nil? :create_table _table #comment_present? ...)
|
62
|
+
PATTERN
|
63
|
+
|
64
|
+
# @!method t_column?(node)
|
65
|
+
def_node_matcher :t_column?, <<~PATTERN
|
66
|
+
(send _var CREATE_TABLE_COLUMN_METHODS ...)
|
67
|
+
PATTERN
|
68
|
+
|
69
|
+
# @!method t_column_with_comment?(node)
|
70
|
+
def_node_matcher :t_column_with_comment?, <<~PATTERN
|
71
|
+
(send _var CREATE_TABLE_COLUMN_METHODS _column _type? #comment_present?)
|
72
|
+
PATTERN
|
73
|
+
|
74
|
+
def on_send(node)
|
75
|
+
if add_column_without_comment?(node)
|
76
|
+
add_offense(node, message: COLUMN_MSG)
|
77
|
+
elsif create_table?(node)
|
78
|
+
if create_table_without_comment?(node)
|
79
|
+
add_offense(node, message: TABLE_MSG)
|
80
|
+
elsif create_table_column_call_without_comment?(node)
|
81
|
+
add_offense(node.parent.body, message: COLUMN_MSG)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def add_column_without_comment?(node)
|
89
|
+
add_column?(node) && !add_column_with_comment?(node)
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_table_without_comment?(node)
|
93
|
+
create_table?(node) && !create_table_with_comment?(node)
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_table_column_call_without_comment?(node)
|
97
|
+
create_table_with_block?(node.parent) &&
|
98
|
+
t_column?(node.parent.body) &&
|
99
|
+
!t_column_with_comment?(node.parent.body)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -5,8 +5,10 @@ module RuboCop
|
|
5
5
|
module Rails
|
6
6
|
#
|
7
7
|
# Checks SQL heredocs to use `.squish`.
|
8
|
-
#
|
9
|
-
#
|
8
|
+
#
|
9
|
+
# @safety
|
10
|
+
# Some SQL syntax (e.g. PostgreSQL comments and functions) requires newlines
|
11
|
+
# to be preserved in order to work, thus auto-correction for this cop is not safe.
|
10
12
|
#
|
11
13
|
# @example
|
12
14
|
# # bad
|