rubocop-rails 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.md +73 -0
- data/bin/setup +7 -0
- data/config/default.yml +466 -0
- data/lib/rubocop-rails.rb +12 -0
- data/lib/rubocop/cop/mixin/target_rails_version.rb +16 -0
- data/lib/rubocop/cop/rails/action_filter.rb +117 -0
- data/lib/rubocop/cop/rails/active_record_aliases.rb +48 -0
- data/lib/rubocop/cop/rails/active_record_override.rb +82 -0
- data/lib/rubocop/cop/rails/active_support_aliases.rb +69 -0
- data/lib/rubocop/cop/rails/application_job.rb +40 -0
- data/lib/rubocop/cop/rails/application_record.rb +40 -0
- data/lib/rubocop/cop/rails/assert_not.rb +44 -0
- data/lib/rubocop/cop/rails/belongs_to.rb +102 -0
- data/lib/rubocop/cop/rails/blank.rb +164 -0
- data/lib/rubocop/cop/rails/bulk_change_table.rb +289 -0
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +91 -0
- data/lib/rubocop/cop/rails/date.rb +161 -0
- data/lib/rubocop/cop/rails/delegate.rb +132 -0
- data/lib/rubocop/cop/rails/delegate_allow_blank.rb +37 -0
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +91 -0
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +45 -0
- data/lib/rubocop/cop/rails/environment_comparison.rb +68 -0
- data/lib/rubocop/cop/rails/exit.rb +67 -0
- data/lib/rubocop/cop/rails/file_path.rb +108 -0
- data/lib/rubocop/cop/rails/find_by.rb +55 -0
- data/lib/rubocop/cop/rails/find_each.rb +51 -0
- data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +25 -0
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +106 -0
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +39 -0
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +117 -0
- data/lib/rubocop/cop/rails/http_status.rb +160 -0
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +94 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +246 -0
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +175 -0
- data/lib/rubocop/cop/rails/link_to_blank.rb +98 -0
- data/lib/rubocop/cop/rails/not_null_column.rb +67 -0
- data/lib/rubocop/cop/rails/output.rb +49 -0
- data/lib/rubocop/cop/rails/output_safety.rb +99 -0
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +107 -0
- data/lib/rubocop/cop/rails/presence.rb +124 -0
- data/lib/rubocop/cop/rails/present.rb +153 -0
- data/lib/rubocop/cop/rails/read_write_attribute.rb +74 -0
- data/lib/rubocop/cop/rails/redundant_allow_nil.rb +111 -0
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +136 -0
- data/lib/rubocop/cop/rails/reflection_class_name.rb +37 -0
- data/lib/rubocop/cop/rails/refute_methods.rb +76 -0
- data/lib/rubocop/cop/rails/relative_date_constant.rb +93 -0
- data/lib/rubocop/cop/rails/request_referer.rb +56 -0
- data/lib/rubocop/cop/rails/reversible_migration.rb +286 -0
- data/lib/rubocop/cop/rails/safe_navigation.rb +87 -0
- data/lib/rubocop/cop/rails/save_bang.rb +316 -0
- data/lib/rubocop/cop/rails/scope_args.rb +29 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +87 -0
- data/lib/rubocop/cop/rails/time_zone.rb +238 -0
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +105 -0
- data/lib/rubocop/cop/rails/unknown_env.rb +63 -0
- data/lib/rubocop/cop/rails/validation.rb +109 -0
- data/lib/rubocop/cop/rails_cops.rb +64 -0
- data/lib/rubocop/rails.rb +12 -0
- data/lib/rubocop/rails/inject.rb +18 -0
- data/lib/rubocop/rails/version.rb +10 -0
- metadata +143 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for scope calls where it was passed
|
7
|
+
# a method (usually a scope) instead of a lambda/proc.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
#
|
11
|
+
# # bad
|
12
|
+
# scope :something, where(something: true)
|
13
|
+
#
|
14
|
+
# # good
|
15
|
+
# scope :something, -> { where(something: true) }
|
16
|
+
class ScopeArgs < Cop
|
17
|
+
MSG = 'Use `lambda`/`proc` instead of a plain method call.'
|
18
|
+
|
19
|
+
def_node_matcher :scope?, '(send nil? :scope _ $send)'
|
20
|
+
|
21
|
+
def on_send(node)
|
22
|
+
scope?(node) do |second_arg|
|
23
|
+
add_offense(second_arg)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the use of methods which skip
|
7
|
+
# validations which are listed in
|
8
|
+
# https://guides.rubyonrails.org/active_record_validations.html#skipping-validations
|
9
|
+
#
|
10
|
+
# Methods may be ignored from this rule by configuring a `Whitelist`.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# Article.first.decrement!(:view_count)
|
15
|
+
# DiscussionBoard.decrement_counter(:post_count, 5)
|
16
|
+
# Article.first.increment!(:view_count)
|
17
|
+
# DiscussionBoard.increment_counter(:post_count, 5)
|
18
|
+
# person.toggle :active
|
19
|
+
# product.touch
|
20
|
+
# Billing.update_all("category = 'authorized', author = 'David'")
|
21
|
+
# user.update_attribute(:website, 'example.com')
|
22
|
+
# user.update_columns(last_request_at: Time.current)
|
23
|
+
# Post.update_counters 5, comment_count: -1, action_count: 1
|
24
|
+
#
|
25
|
+
# # good
|
26
|
+
# user.update(website: 'example.com')
|
27
|
+
# FileUtils.touch('file')
|
28
|
+
#
|
29
|
+
# @example Whitelist: ["touch"]
|
30
|
+
# # bad
|
31
|
+
# DiscussionBoard.decrement_counter(:post_count, 5)
|
32
|
+
# DiscussionBoard.increment_counter(:post_count, 5)
|
33
|
+
# person.toggle :active
|
34
|
+
#
|
35
|
+
# # good
|
36
|
+
# user.touch
|
37
|
+
#
|
38
|
+
class SkipsModelValidations < Cop
|
39
|
+
MSG = 'Avoid using `%<method>s` because it skips validations.'
|
40
|
+
|
41
|
+
METHODS_WITH_ARGUMENTS = %w[decrement!
|
42
|
+
decrement_counter
|
43
|
+
increment!
|
44
|
+
increment_counter
|
45
|
+
toggle!
|
46
|
+
update_all
|
47
|
+
update_attribute
|
48
|
+
update_column
|
49
|
+
update_columns
|
50
|
+
update_counters].freeze
|
51
|
+
|
52
|
+
def_node_matcher :good_touch?, <<-PATTERN
|
53
|
+
(send (const nil? :FileUtils) :touch ...)
|
54
|
+
PATTERN
|
55
|
+
|
56
|
+
def on_send(node)
|
57
|
+
return if whitelist.include?(node.method_name.to_s)
|
58
|
+
return unless blacklist.include?(node.method_name.to_s)
|
59
|
+
return if allowed_method?(node)
|
60
|
+
return if good_touch?(node)
|
61
|
+
|
62
|
+
add_offense(node, location: :selector)
|
63
|
+
end
|
64
|
+
alias on_csend on_send
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
def message(node)
|
69
|
+
format(MSG, method: node.method_name)
|
70
|
+
end
|
71
|
+
|
72
|
+
def allowed_method?(node)
|
73
|
+
METHODS_WITH_ARGUMENTS.include?(node.method_name.to_s) &&
|
74
|
+
!node.arguments?
|
75
|
+
end
|
76
|
+
|
77
|
+
def blacklist
|
78
|
+
cop_config['Blacklist'] || []
|
79
|
+
end
|
80
|
+
|
81
|
+
def whitelist
|
82
|
+
cop_config['Whitelist'] || []
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,238 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop checks for the use of Time methods without zone.
|
7
|
+
#
|
8
|
+
# Built on top of Ruby on Rails style guide (https://github.com/rubocop-hq/rails-style-guide#time)
|
9
|
+
# and the article http://danilenko.org/2012/7/6/rails_timezones/
|
10
|
+
#
|
11
|
+
# Two styles are supported for this cop. When EnforcedStyle is 'strict'
|
12
|
+
# then only use of Time.zone is allowed.
|
13
|
+
#
|
14
|
+
# When EnforcedStyle is 'flexible' then it's also allowed
|
15
|
+
# to use Time.in_time_zone.
|
16
|
+
#
|
17
|
+
# @example EnforcedStyle: strict
|
18
|
+
# # `strict` means that `Time` should be used with `zone`.
|
19
|
+
#
|
20
|
+
# # bad
|
21
|
+
# Time.now
|
22
|
+
# Time.parse('2015-03-02 19:05:37')
|
23
|
+
#
|
24
|
+
# # bad
|
25
|
+
# Time.current
|
26
|
+
# Time.at(timestamp).in_time_zone
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# Time.zone.now
|
30
|
+
# Time.zone.parse('2015-03-02 19:05:37')
|
31
|
+
#
|
32
|
+
# @example EnforcedStyle: flexible (default)
|
33
|
+
# # `flexible` allows usage of `in_time_zone` instead of `zone`.
|
34
|
+
#
|
35
|
+
# # bad
|
36
|
+
# Time.now
|
37
|
+
# Time.parse('2015-03-02 19:05:37')
|
38
|
+
#
|
39
|
+
# # good
|
40
|
+
# Time.zone.now
|
41
|
+
# Time.zone.parse('2015-03-02 19:05:37')
|
42
|
+
#
|
43
|
+
# # good
|
44
|
+
# Time.current
|
45
|
+
# Time.at(timestamp).in_time_zone
|
46
|
+
class TimeZone < Cop
|
47
|
+
include ConfigurableEnforcedStyle
|
48
|
+
|
49
|
+
MSG = 'Do not use `%<current>s` without zone. Use `%<prefer>s` ' \
|
50
|
+
'instead.'
|
51
|
+
|
52
|
+
MSG_ACCEPTABLE = 'Do not use `%<current>s` without zone. ' \
|
53
|
+
'Use one of %<prefer>s instead.'
|
54
|
+
|
55
|
+
MSG_LOCALTIME = 'Do not use `Time.localtime` without ' \
|
56
|
+
'offset or zone.'
|
57
|
+
|
58
|
+
TIMECLASSES = %i[Time DateTime].freeze
|
59
|
+
|
60
|
+
GOOD_METHODS = %i[zone zone_default find_zone find_zone!].freeze
|
61
|
+
|
62
|
+
DANGEROUS_METHODS = %i[now local new parse at current].freeze
|
63
|
+
|
64
|
+
ACCEPTED_METHODS = %i[in_time_zone utc getlocal xmlschema iso8601
|
65
|
+
jisx0301 rfc3339 httpdate to_i to_f].freeze
|
66
|
+
|
67
|
+
def on_const(node)
|
68
|
+
mod, klass = *node
|
69
|
+
# we should only check core classes
|
70
|
+
# (`DateTime`, `Time`, `::DateTime` or `::Time`)
|
71
|
+
return unless (mod.nil? || mod.cbase_type?) && method_send?(node)
|
72
|
+
|
73
|
+
check_time_node(klass, node.parent) if TIMECLASSES.include?(klass)
|
74
|
+
end
|
75
|
+
|
76
|
+
def autocorrect(node)
|
77
|
+
lambda do |corrector|
|
78
|
+
# add `.zone`: `Time.at` => `Time.zone.at`
|
79
|
+
corrector.insert_after(node.children[0].source_range, '.zone')
|
80
|
+
# replace `Time.zone.current` => `Time.zone.now`
|
81
|
+
if node.method_name == :current
|
82
|
+
corrector.replace(node.loc.selector, 'now')
|
83
|
+
end
|
84
|
+
# prefer `Time` over `DateTime` class
|
85
|
+
if strict?
|
86
|
+
corrector.replace(node.children.first.source_range, 'Time')
|
87
|
+
end
|
88
|
+
remove_redundant_in_time_zone(corrector, node)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# remove redundant `.in_time_zone` from `Time.zone.now.in_time_zone`
|
95
|
+
def remove_redundant_in_time_zone(corrector, node)
|
96
|
+
time_methods_called = extract_method_chain(node)
|
97
|
+
return unless time_methods_called.include?(:in_time_zone) ||
|
98
|
+
time_methods_called.include?(:zone)
|
99
|
+
|
100
|
+
while node&.send_type?
|
101
|
+
if node.children.last == :in_time_zone
|
102
|
+
in_time_zone_with_dot =
|
103
|
+
node.loc.selector.adjust(begin_pos: -1)
|
104
|
+
corrector.remove(in_time_zone_with_dot)
|
105
|
+
end
|
106
|
+
node = node.parent
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def check_time_node(klass, node)
|
111
|
+
chain = extract_method_chain(node)
|
112
|
+
return if not_danger_chain?(chain)
|
113
|
+
|
114
|
+
return check_localtime(node) if need_check_localtime?(chain)
|
115
|
+
|
116
|
+
method_name = (chain & DANGEROUS_METHODS).join('.')
|
117
|
+
|
118
|
+
return if offset_provided?(node)
|
119
|
+
|
120
|
+
message = build_message(klass, method_name, node)
|
121
|
+
|
122
|
+
add_offense(node, location: :selector, message: message)
|
123
|
+
end
|
124
|
+
|
125
|
+
def build_message(klass, method_name, node)
|
126
|
+
if flexible?
|
127
|
+
format(
|
128
|
+
MSG_ACCEPTABLE,
|
129
|
+
current: "#{klass}.#{method_name}",
|
130
|
+
prefer: acceptable_methods(klass, method_name, node).join(', ')
|
131
|
+
)
|
132
|
+
else
|
133
|
+
safe_method_name = safe_method(method_name, node)
|
134
|
+
format(MSG,
|
135
|
+
current: "#{klass}.#{method_name}",
|
136
|
+
prefer: "Time.zone.#{safe_method_name}")
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def extract_method_chain(node)
|
141
|
+
chain = []
|
142
|
+
while !node.nil? && node.send_type?
|
143
|
+
chain << node.method_name if method_from_time_class?(node)
|
144
|
+
node = node.parent
|
145
|
+
end
|
146
|
+
chain
|
147
|
+
end
|
148
|
+
|
149
|
+
# Only add the method to the chain if the method being
|
150
|
+
# called is part of the time class.
|
151
|
+
def method_from_time_class?(node)
|
152
|
+
receiver, method_name, *_args = *node
|
153
|
+
if (receiver.is_a? RuboCop::AST::Node) && !receiver.cbase_type?
|
154
|
+
method_from_time_class?(receiver)
|
155
|
+
else
|
156
|
+
TIMECLASSES.include?(method_name)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# checks that parent node of send_type
|
161
|
+
# and receiver is the given node
|
162
|
+
def method_send?(node)
|
163
|
+
return false unless node.parent&.send_type?
|
164
|
+
|
165
|
+
node.parent.receiver == node
|
166
|
+
end
|
167
|
+
|
168
|
+
def safe_method(method_name, node)
|
169
|
+
if %w[new current].include?(method_name)
|
170
|
+
node.arguments? ? 'local' : 'now'
|
171
|
+
else
|
172
|
+
method_name
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def check_localtime(node)
|
177
|
+
selector_node = node
|
178
|
+
|
179
|
+
while node&.send_type?
|
180
|
+
break if node.method_name == :localtime
|
181
|
+
|
182
|
+
node = node.parent
|
183
|
+
end
|
184
|
+
|
185
|
+
return if node.arguments?
|
186
|
+
|
187
|
+
add_offense(selector_node,
|
188
|
+
location: :selector, message: MSG_LOCALTIME)
|
189
|
+
end
|
190
|
+
|
191
|
+
def not_danger_chain?(chain)
|
192
|
+
(chain & DANGEROUS_METHODS).empty? || !(chain & good_methods).empty?
|
193
|
+
end
|
194
|
+
|
195
|
+
def need_check_localtime?(chain)
|
196
|
+
flexible? && chain.include?(:localtime)
|
197
|
+
end
|
198
|
+
|
199
|
+
def flexible?
|
200
|
+
style == :flexible
|
201
|
+
end
|
202
|
+
|
203
|
+
def strict?
|
204
|
+
style == :strict
|
205
|
+
end
|
206
|
+
|
207
|
+
def good_methods
|
208
|
+
if strict?
|
209
|
+
GOOD_METHODS
|
210
|
+
else
|
211
|
+
GOOD_METHODS + [:current] + ACCEPTED_METHODS
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def acceptable_methods(klass, method_name, node)
|
216
|
+
acceptable = [
|
217
|
+
"`Time.zone.#{safe_method(method_name, node)}`",
|
218
|
+
"`#{klass}.current`"
|
219
|
+
]
|
220
|
+
|
221
|
+
ACCEPTED_METHODS.each do |am|
|
222
|
+
acceptable << "`#{klass}.#{method_name}.#{am}`"
|
223
|
+
end
|
224
|
+
|
225
|
+
acceptable
|
226
|
+
end
|
227
|
+
|
228
|
+
# Time.new can be called with a time zone offset
|
229
|
+
# When it is, that should be considered safe
|
230
|
+
# Example:
|
231
|
+
# Time.new(1988, 3, 15, 3, 0, 0, "-05:00")
|
232
|
+
def offset_provided?(node)
|
233
|
+
node.arguments.size >= 7
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# Prefer the use of uniq (or distinct), before pluck instead of after.
|
7
|
+
#
|
8
|
+
# The use of uniq before pluck is preferred because it executes within
|
9
|
+
# the database.
|
10
|
+
#
|
11
|
+
# This cop has two different enforcement modes. When the EnforcedStyle
|
12
|
+
# is conservative (the default) then only calls to pluck on a constant
|
13
|
+
# (i.e. a model class) before uniq are added as offenses.
|
14
|
+
#
|
15
|
+
# When the EnforcedStyle is aggressive then all calls to pluck before
|
16
|
+
# uniq are added as offenses. This may lead to false positives as the cop
|
17
|
+
# cannot distinguish between calls to pluck on an ActiveRecord::Relation
|
18
|
+
# vs a call to pluck on an ActiveRecord::Associations::CollectionProxy.
|
19
|
+
#
|
20
|
+
# Autocorrect is disabled by default for this cop since it may generate
|
21
|
+
# false positives.
|
22
|
+
#
|
23
|
+
# @example EnforcedStyle: conservative (default)
|
24
|
+
# # bad
|
25
|
+
# Model.pluck(:id).uniq
|
26
|
+
#
|
27
|
+
# # good
|
28
|
+
# Model.uniq.pluck(:id)
|
29
|
+
#
|
30
|
+
# @example EnforcedStyle: aggressive
|
31
|
+
# # bad
|
32
|
+
# # this will return a Relation that pluck is called on
|
33
|
+
# Model.where(cond: true).pluck(:id).uniq
|
34
|
+
#
|
35
|
+
# # bad
|
36
|
+
# # an association on an instance will return a CollectionProxy
|
37
|
+
# instance.assoc.pluck(:id).uniq
|
38
|
+
#
|
39
|
+
# # bad
|
40
|
+
# Model.pluck(:id).uniq
|
41
|
+
#
|
42
|
+
# # good
|
43
|
+
# Model.uniq.pluck(:id)
|
44
|
+
#
|
45
|
+
class UniqBeforePluck < RuboCop::Cop::Cop
|
46
|
+
include ConfigurableEnforcedStyle
|
47
|
+
include RangeHelp
|
48
|
+
|
49
|
+
MSG = 'Use `%<method>s` before `pluck`.'
|
50
|
+
NEWLINE = "\n"
|
51
|
+
PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
|
52
|
+
'${:uniq :distinct} ...)]'
|
53
|
+
|
54
|
+
def_node_matcher :conservative_node_match,
|
55
|
+
format(PATTERN, type: 'const')
|
56
|
+
|
57
|
+
def_node_matcher :aggressive_node_match,
|
58
|
+
format(PATTERN, type: '_')
|
59
|
+
|
60
|
+
def on_send(node)
|
61
|
+
method = if style == :conservative
|
62
|
+
conservative_node_match(node)
|
63
|
+
else
|
64
|
+
aggressive_node_match(node)
|
65
|
+
end
|
66
|
+
|
67
|
+
return unless method
|
68
|
+
|
69
|
+
add_offense(node, location: :selector,
|
70
|
+
message: format(MSG, method: method))
|
71
|
+
end
|
72
|
+
|
73
|
+
def autocorrect(node)
|
74
|
+
lambda do |corrector|
|
75
|
+
method = node.method_name
|
76
|
+
|
77
|
+
corrector.remove(dot_method_with_whitespace(method, node))
|
78
|
+
corrector.insert_before(node.receiver.loc.dot.begin, ".#{method}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def style_parameter_name
|
85
|
+
'EnforcedStyle'
|
86
|
+
end
|
87
|
+
|
88
|
+
def dot_method_with_whitespace(method, node)
|
89
|
+
range_between(dot_method_begin_pos(method, node),
|
90
|
+
node.loc.selector.end_pos)
|
91
|
+
end
|
92
|
+
|
93
|
+
def dot_method_begin_pos(method, node)
|
94
|
+
lines = node.source.split(NEWLINE)
|
95
|
+
|
96
|
+
if lines.last.strip == ".#{method}"
|
97
|
+
node.source.rindex(NEWLINE)
|
98
|
+
else
|
99
|
+
node.loc.dot.begin_pos
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|