rubocop-rails 2.12.4 → 2.13.0
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/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/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/redundant_presence_validation_on_belongs_to.rb +192 -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/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
@@ -0,0 +1,192 @@
|
|
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 - with presence options
|
47
|
+
# validates :user, presence: { message: 'duplicate' }
|
48
|
+
#
|
49
|
+
# @example source that matches - by a foreign key
|
50
|
+
# validates :user_id, presence: true
|
51
|
+
def_node_matcher :presence_validation?, <<~PATTERN
|
52
|
+
$(
|
53
|
+
send nil? :validates
|
54
|
+
(sym $_)
|
55
|
+
...
|
56
|
+
$(hash <$(pair (sym :presence) {true hash}) ...>)
|
57
|
+
)
|
58
|
+
PATTERN
|
59
|
+
|
60
|
+
# @!method optional_option?(node)
|
61
|
+
# Match a `belongs_to` association with an optional option in a hash
|
62
|
+
def_node_matcher :optional?, <<~PATTERN
|
63
|
+
(send nil? :belongs_to _ ... #optional_option?)
|
64
|
+
PATTERN
|
65
|
+
|
66
|
+
# @!method optional_option?(node)
|
67
|
+
# Match an optional option in a hash
|
68
|
+
def_node_matcher :optional_option?, <<~PATTERN
|
69
|
+
{
|
70
|
+
(hash <(pair (sym :optional) true) ...>) # optional: true
|
71
|
+
(hash <(pair (sym :required) false) ...>) # required: false
|
72
|
+
}
|
73
|
+
PATTERN
|
74
|
+
|
75
|
+
# @!method any_belongs_to?(node, association:)
|
76
|
+
# Match a class with `belongs_to` with no regard to `foreign_key` option
|
77
|
+
#
|
78
|
+
# @example source that matches
|
79
|
+
# belongs_to :user
|
80
|
+
#
|
81
|
+
# @example source that matches - regardless of `foreign_key`
|
82
|
+
# belongs_to :author, foreign_key: :user_id
|
83
|
+
#
|
84
|
+
# @param node [RuboCop::AST::Node]
|
85
|
+
# @param association [Symbol]
|
86
|
+
# @return [Array<RuboCop::AST::Node>, nil] matching node
|
87
|
+
def_node_matcher :any_belongs_to?, <<~PATTERN
|
88
|
+
(begin
|
89
|
+
<
|
90
|
+
$(send nil? :belongs_to (sym %association) ...)
|
91
|
+
...
|
92
|
+
>
|
93
|
+
)
|
94
|
+
PATTERN
|
95
|
+
|
96
|
+
# @!method belongs_to?(node, key:, fk:)
|
97
|
+
# Match a class with a matching association, either by name or an explicit
|
98
|
+
# `foreign_key` option
|
99
|
+
#
|
100
|
+
# @example source that matches - fk matches `foreign_key` option
|
101
|
+
# belongs_to :author, foreign_key: :user_id
|
102
|
+
#
|
103
|
+
# @example source that matches - key matches association name
|
104
|
+
# belongs_to :user
|
105
|
+
#
|
106
|
+
# @example source that does not match - explicit `foreign_key` does not match
|
107
|
+
# belongs_to :user, foreign_key: :account_id
|
108
|
+
#
|
109
|
+
# @param node [RuboCop::AST::Node]
|
110
|
+
# @param key [Symbol] e.g. `:user`
|
111
|
+
# @param fk [Symbol] e.g. `:user_id`
|
112
|
+
# @return [Array<RuboCop::AST::Node>] matching nodes
|
113
|
+
def_node_matcher :belongs_to?, <<~PATTERN
|
114
|
+
(begin
|
115
|
+
<
|
116
|
+
${
|
117
|
+
#belongs_to_without_fk?(%key) # belongs_to :user
|
118
|
+
#belongs_to_with_a_matching_fk?(%fk) # belongs_to :author, foreign_key: :user_id
|
119
|
+
}
|
120
|
+
...
|
121
|
+
>
|
122
|
+
)
|
123
|
+
PATTERN
|
124
|
+
|
125
|
+
# @!method belongs_to_without_fk?(node, fk)
|
126
|
+
# Match a matching `belongs_to` association, without an explicit `foreign_key` option
|
127
|
+
#
|
128
|
+
# @param node [RuboCop::AST::Node]
|
129
|
+
# @param key [Symbol] e.g. `:user`
|
130
|
+
# @return [Array<RuboCop::AST::Node>] matching nodes
|
131
|
+
def_node_matcher :belongs_to_without_fk?, <<~PATTERN
|
132
|
+
{
|
133
|
+
(send nil? :belongs_to (sym %1)) # belongs_to :user
|
134
|
+
(send nil? :belongs_to (sym %1) !hash) # belongs_to :user, -> { not_deleted }
|
135
|
+
(send nil? :belongs_to (sym %1) !(hash <(pair (sym :foreign_key) _) ...>))
|
136
|
+
}
|
137
|
+
PATTERN
|
138
|
+
|
139
|
+
# @!method belongs_to_with_a_matching_fk?(node, fk)
|
140
|
+
# Match a matching `belongs_to` association with a matching explicit `foreign_key` option
|
141
|
+
#
|
142
|
+
# @example source that matches
|
143
|
+
# belongs_to :author, foreign_key: :user_id
|
144
|
+
#
|
145
|
+
# @param node [RuboCop::AST::Node]
|
146
|
+
# @param fk [Symbol] e.g. `:user_id`
|
147
|
+
# @return [Array<RuboCop::AST::Node>] matching nodes
|
148
|
+
def_node_matcher :belongs_to_with_a_matching_fk?, <<~PATTERN
|
149
|
+
(send nil? :belongs_to ... (hash <(pair (sym :foreign_key) (sym %1)) ...>))
|
150
|
+
PATTERN
|
151
|
+
|
152
|
+
def on_send(node)
|
153
|
+
validation, key, options, presence = presence_validation?(node)
|
154
|
+
return unless validation
|
155
|
+
|
156
|
+
belongs_to = belongs_to_for(node.parent, key)
|
157
|
+
return unless belongs_to
|
158
|
+
return if optional?(belongs_to)
|
159
|
+
|
160
|
+
message = format(MSG, association: key.to_s)
|
161
|
+
|
162
|
+
add_offense(presence, message: message) do |corrector|
|
163
|
+
remove_presence_validation(corrector, node, options, presence)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def belongs_to_for(model_class_node, key)
|
170
|
+
if key.to_s.end_with?('_id')
|
171
|
+
normalized_key = key.to_s.delete_suffix('_id').to_sym
|
172
|
+
belongs_to?(model_class_node, key: normalized_key, fk: key)
|
173
|
+
else
|
174
|
+
any_belongs_to?(model_class_node, association: key)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def remove_presence_validation(corrector, node, options, presence)
|
179
|
+
if options.children.one?
|
180
|
+
corrector.remove(range_by_whole_lines(node.source_range, include_final_newline: true))
|
181
|
+
else
|
182
|
+
range = range_with_surrounding_comma(
|
183
|
+
range_with_surrounding_space(range: presence.source_range, side: :left),
|
184
|
+
:left
|
185
|
+
)
|
186
|
+
corrector.remove(range)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
end
|
192
|
+
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? ${:execute :remove_belongs_to} ...)
|
182
|
+
(send nil? ${:change_column :execute :remove_belongs_to :remove_reference} ...)
|
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
|
@@ -3,47 +3,46 @@
|
|
3
3
|
module RuboCop
|
4
4
|
module Cop
|
5
5
|
module Rails
|
6
|
-
# Prefer
|
6
|
+
# Prefer using `distinct` before `pluck` instead of `uniq` after `pluck`.
|
7
7
|
#
|
8
|
-
# The use of distinct before pluck is preferred because it executes
|
8
|
+
# The use of distinct before pluck is preferred because it executes by
|
9
9
|
# the database.
|
10
10
|
#
|
11
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
|
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
14
|
#
|
15
|
-
# When the EnforcedStyle is aggressive then all calls to pluck before
|
15
|
+
# When the EnforcedStyle is `aggressive` then all calls to `pluck` before
|
16
16
|
# distinct are added as offenses. This may lead to false positives
|
17
|
-
# as the cop cannot distinguish between calls to pluck on an
|
17
|
+
# as the cop cannot distinguish between calls to `pluck` on an
|
18
18
|
# ActiveRecord::Relation vs a call to pluck on an
|
19
19
|
# ActiveRecord::Associations::CollectionProxy.
|
20
20
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
# false positives.
|
21
|
+
# @safety
|
22
|
+
# This cop is unsafe for autocorrection because the behavior may change
|
23
|
+
# depending on the database collation.
|
25
24
|
#
|
26
25
|
# @example EnforcedStyle: conservative (default)
|
27
|
-
# # bad
|
28
|
-
#
|
26
|
+
# # bad - redundantly fetches duplicate values
|
27
|
+
# Album.pluck(:band_name).uniq
|
29
28
|
#
|
30
29
|
# # good
|
31
|
-
#
|
30
|
+
# Album.distinct.pluck(:band_name)
|
32
31
|
#
|
33
32
|
# @example EnforcedStyle: aggressive
|
34
|
-
# # bad
|
35
|
-
#
|
36
|
-
# Model.where(cond: true).pluck(:id).uniq
|
33
|
+
# # bad - redundantly fetches duplicate values
|
34
|
+
# Album.pluck(:band_name).uniq
|
37
35
|
#
|
38
|
-
# # bad
|
39
|
-
#
|
40
|
-
# instance.assoc.pluck(:id).uniq
|
36
|
+
# # bad - redundantly fetches duplicate values
|
37
|
+
# Album.where(year: 1985).pluck(:band_name).uniq
|
41
38
|
#
|
42
|
-
# # bad
|
43
|
-
#
|
39
|
+
# # bad - redundantly fetches duplicate values
|
40
|
+
# customer.favourites.pluck(:color).uniq
|
44
41
|
#
|
45
42
|
# # good
|
46
|
-
#
|
43
|
+
# Album.distinct.pluck(:band_name)
|
44
|
+
# Album.distinct.where(year: 1985).pluck(:band_name)
|
45
|
+
# customer.favourites.distinct.pluck(:color)
|
47
46
|
#
|
48
47
|
class UniqBeforePluck < Base
|
49
48
|
include ConfigurableEnforcedStyle
|
@@ -51,10 +50,9 @@ module RuboCop
|
|
51
50
|
extend AutoCorrector
|
52
51
|
|
53
52
|
MSG = 'Use `distinct` before `pluck`.'
|
54
|
-
RESTRICT_ON_SEND = %i[uniq
|
53
|
+
RESTRICT_ON_SEND = %i[uniq].freeze
|
55
54
|
NEWLINE = "\n"
|
56
|
-
PATTERN = '[!^block (send (send %<type>s :pluck ...) '
|
57
|
-
'${:uniq :distinct} ...)]'
|
55
|
+
PATTERN = '[!^block (send (send %<type>s :pluck ...) :uniq ...)]'
|
58
56
|
|
59
57
|
def_node_matcher :conservative_node_match,
|
60
58
|
format(PATTERN, type: 'const')
|
@@ -63,13 +61,13 @@ module RuboCop
|
|
63
61
|
format(PATTERN, type: '_')
|
64
62
|
|
65
63
|
def on_send(node)
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
64
|
+
uniq = if style == :conservative
|
65
|
+
conservative_node_match(node)
|
66
|
+
else
|
67
|
+
aggressive_node_match(node)
|
68
|
+
end
|
71
69
|
|
72
|
-
return unless
|
70
|
+
return unless uniq
|
73
71
|
|
74
72
|
add_offense(node.loc.selector) do |corrector|
|
75
73
|
method = node.method_name
|
@@ -81,10 +79,6 @@ module RuboCop
|
|
81
79
|
|
82
80
|
private
|
83
81
|
|
84
|
-
def style_parameter_name
|
85
|
-
'EnforcedStyle'
|
86
|
-
end
|
87
|
-
|
88
82
|
def dot_method_with_whitespace(method, node)
|
89
83
|
range_between(dot_method_begin_pos(method, node),
|
90
84
|
node.loc.selector.end_pos)
|
@@ -6,6 +6,10 @@ module RuboCop
|
|
6
6
|
# This cop identifies places where manually constructed SQL
|
7
7
|
# in `where` can be replaced with `where(attribute: value)`.
|
8
8
|
#
|
9
|
+
# @safety
|
10
|
+
# This cop's autocorrection is unsafe because is may change SQL.
|
11
|
+
# See: https://github.com/rubocop/rubocop-rails/issues/403
|
12
|
+
#
|
9
13
|
# @example
|
10
14
|
# # bad
|
11
15
|
# User.where('name = ?', 'Gabe')
|
@@ -11,16 +11,17 @@ module RuboCop
|
|
11
11
|
# When EnforcedStyle is 'where' then the cop enforces
|
12
12
|
# `where(...).exists?` over `exists?(...)`.
|
13
13
|
#
|
14
|
-
#
|
14
|
+
# @safety
|
15
|
+
# This cop is unsafe for auto-correction because the behavior may change on the following case:
|
15
16
|
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
17
|
+
# [source,ruby]
|
18
|
+
# ----
|
19
|
+
# Author.includes(:articles).where(articles: {id: id}).exists?
|
20
|
+
# #=> Perform `eager_load` behavior (`LEFT JOIN` query) and get result.
|
20
21
|
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
22
|
+
# Author.includes(:articles).exists?(articles: {id: id})
|
23
|
+
# #=> Perform `preload` behavior and `ActiveRecord::StatementInvalid` error occurs.
|
24
|
+
# ----
|
24
25
|
#
|
25
26
|
# @example EnforcedStyle: exists (default)
|
26
27
|
# # bad
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative 'mixin/active_record_helper'
|
4
|
+
require_relative 'mixin/active_record_migrations_helper'
|
4
5
|
require_relative 'mixin/enforce_superclass'
|
5
6
|
require_relative 'mixin/index_method'
|
6
7
|
require_relative 'mixin/target_rails_version'
|
@@ -22,12 +23,14 @@ require_relative 'rails/attribute_default_block_value'
|
|
22
23
|
require_relative 'rails/belongs_to'
|
23
24
|
require_relative 'rails/blank'
|
24
25
|
require_relative 'rails/bulk_change_table'
|
26
|
+
require_relative 'rails/compact_blank'
|
25
27
|
require_relative 'rails/content_tag'
|
26
28
|
require_relative 'rails/create_table_with_timestamps'
|
27
29
|
require_relative 'rails/date'
|
28
30
|
require_relative 'rails/default_scope'
|
29
31
|
require_relative 'rails/delegate'
|
30
32
|
require_relative 'rails/delegate_allow_blank'
|
33
|
+
require_relative 'rails/duration_arithmetic'
|
31
34
|
require_relative 'rails/dynamic_find_by'
|
32
35
|
require_relative 'rails/eager_evaluation_log_message'
|
33
36
|
require_relative 'rails/enum_hash'
|
@@ -71,6 +74,7 @@ require_relative 'rails/rake_environment'
|
|
71
74
|
require_relative 'rails/read_write_attribute'
|
72
75
|
require_relative 'rails/redundant_allow_nil'
|
73
76
|
require_relative 'rails/redundant_foreign_key'
|
77
|
+
require_relative 'rails/redundant_presence_validation_on_belongs_to'
|
74
78
|
require_relative 'rails/redundant_receiver_in_with_options'
|
75
79
|
require_relative 'rails/redundant_travel_back'
|
76
80
|
require_relative 'rails/reflection_class_name'
|
@@ -82,9 +86,11 @@ require_relative 'rails/request_referer'
|
|
82
86
|
require_relative 'rails/require_dependency'
|
83
87
|
require_relative 'rails/reversible_migration'
|
84
88
|
require_relative 'rails/reversible_migration_method_definition'
|
89
|
+
require_relative 'rails/root_join_chain'
|
85
90
|
require_relative 'rails/safe_navigation'
|
86
91
|
require_relative 'rails/safe_navigation_with_blank'
|
87
92
|
require_relative 'rails/save_bang'
|
93
|
+
require_relative 'rails/schema_comment'
|
88
94
|
require_relative 'rails/scope_args'
|
89
95
|
require_relative 'rails/short_i18n'
|
90
96
|
require_relative 'rails/skips_model_validations'
|