rubocop-rails 2.6.0 → 2.9.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/README.md +16 -0
- data/config/default.yml +189 -6
- data/lib/rubocop/cop/mixin/active_record_helper.rb +12 -3
- data/lib/rubocop/cop/mixin/enforce_superclass.rb +40 -0
- data/lib/rubocop/cop/mixin/index_method.rb +25 -11
- data/lib/rubocop/cop/rails/action_filter.rb +10 -14
- data/lib/rubocop/cop/rails/active_record_aliases.rb +13 -17
- data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +148 -0
- data/lib/rubocop/cop/rails/active_record_override.rb +1 -1
- data/lib/rubocop/cop/rails/active_support_aliases.rb +12 -21
- data/lib/rubocop/cop/rails/after_commit_override.rb +91 -0
- data/lib/rubocop/cop/rails/application_controller.rb +3 -7
- data/lib/rubocop/cop/rails/application_job.rb +2 -1
- data/lib/rubocop/cop/rails/application_mailer.rb +2 -7
- data/lib/rubocop/cop/rails/application_record.rb +2 -7
- data/lib/rubocop/cop/rails/arel_star.rb +41 -0
- data/lib/rubocop/cop/rails/assert_not.rb +8 -10
- data/lib/rubocop/cop/rails/attribute_default_block_value.rb +90 -0
- data/lib/rubocop/cop/rails/belongs_to.rb +9 -18
- data/lib/rubocop/cop/rails/blank.rb +27 -27
- data/lib/rubocop/cop/rails/bulk_change_table.rb +1 -1
- data/lib/rubocop/cop/rails/content_tag.rb +20 -33
- data/lib/rubocop/cop/rails/create_table_with_timestamps.rb +2 -1
- data/lib/rubocop/cop/rails/date.rb +10 -11
- data/lib/rubocop/cop/rails/default_scope.rb +61 -0
- data/lib/rubocop/cop/rails/delegate.rb +10 -10
- data/lib/rubocop/cop/rails/delegate_allow_blank.rb +7 -8
- data/lib/rubocop/cop/rails/dynamic_find_by.rb +13 -11
- data/lib/rubocop/cop/rails/enum_hash.rb +11 -10
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +2 -1
- data/lib/rubocop/cop/rails/environment_comparison.rb +18 -14
- data/lib/rubocop/cop/rails/exit.rb +4 -10
- data/lib/rubocop/cop/rails/file_path.rb +5 -4
- data/lib/rubocop/cop/rails/find_by.rb +13 -13
- data/lib/rubocop/cop/rails/find_by_id.rb +94 -0
- data/lib/rubocop/cop/rails/find_each.rb +16 -14
- data/lib/rubocop/cop/rails/has_and_belongs_to_many.rb +3 -2
- data/lib/rubocop/cop/rails/has_many_or_has_one_dependent.rb +4 -7
- data/lib/rubocop/cop/rails/helper_instance_variable.rb +4 -2
- data/lib/rubocop/cop/rails/http_positional_arguments.rb +25 -21
- data/lib/rubocop/cop/rails/http_status.rb +7 -9
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +8 -6
- data/lib/rubocop/cop/rails/index_by.rb +11 -2
- data/lib/rubocop/cop/rails/index_with.rb +11 -2
- data/lib/rubocop/cop/rails/inquiry.rb +39 -0
- data/lib/rubocop/cop/rails/inverse_of.rb +3 -2
- data/lib/rubocop/cop/rails/lexically_scoped_action_filter.rb +17 -15
- data/lib/rubocop/cop/rails/link_to_blank.rb +20 -20
- data/lib/rubocop/cop/rails/mailer_name.rb +86 -0
- data/lib/rubocop/cop/rails/match_route.rb +120 -0
- data/lib/rubocop/cop/rails/negate_include.rb +41 -0
- data/lib/rubocop/cop/rails/not_null_column.rb +2 -1
- data/lib/rubocop/cop/rails/order_by_id.rb +52 -0
- data/lib/rubocop/cop/rails/output.rb +5 -2
- data/lib/rubocop/cop/rails/output_safety.rb +3 -2
- data/lib/rubocop/cop/rails/pick.rb +21 -15
- data/lib/rubocop/cop/rails/pluck.rb +56 -0
- data/lib/rubocop/cop/rails/pluck_id.rb +56 -0
- data/lib/rubocop/cop/rails/pluck_in_where.rb +70 -0
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +10 -14
- data/lib/rubocop/cop/rails/presence.rb +12 -13
- data/lib/rubocop/cop/rails/present.rb +30 -24
- data/lib/rubocop/cop/rails/rake_environment.rb +9 -11
- data/lib/rubocop/cop/rails/read_write_attribute.rb +12 -11
- data/lib/rubocop/cop/rails/redundant_allow_nil.rb +29 -31
- data/lib/rubocop/cop/rails/redundant_foreign_key.rb +9 -12
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +11 -10
- data/lib/rubocop/cop/rails/reflection_class_name.rb +4 -3
- data/lib/rubocop/cop/rails/refute_methods.rb +9 -10
- data/lib/rubocop/cop/rails/relative_date_constant.rb +20 -9
- data/lib/rubocop/cop/rails/render_inline.rb +41 -0
- data/lib/rubocop/cop/rails/render_plain_text.rb +71 -0
- data/lib/rubocop/cop/rails/request_referer.rb +7 -7
- data/lib/rubocop/cop/rails/reversible_migration.rb +82 -7
- data/lib/rubocop/cop/rails/safe_navigation.rb +12 -11
- data/lib/rubocop/cop/rails/safe_navigation_with_blank.rb +5 -10
- data/lib/rubocop/cop/rails/save_bang.rb +19 -22
- data/lib/rubocop/cop/rails/scope_args.rb +2 -1
- data/lib/rubocop/cop/rails/short_i18n.rb +74 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +46 -11
- data/lib/rubocop/cop/rails/squished_sql_heredocs.rb +82 -0
- data/lib/rubocop/cop/rails/time_zone.rb +22 -20
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +10 -10
- data/lib/rubocop/cop/rails/unique_validation_without_index.rb +18 -8
- data/lib/rubocop/cop/rails/unknown_env.rb +15 -4
- data/lib/rubocop/cop/rails/validation.rb +15 -14
- data/lib/rubocop/cop/rails/where_equals.rb +94 -0
- data/lib/rubocop/cop/rails/where_exists.rb +126 -0
- data/lib/rubocop/cop/rails/where_not.rb +97 -0
- data/lib/rubocop/cop/rails_cops.rb +22 -0
- data/lib/rubocop/rails/schema_loader.rb +4 -4
- data/lib/rubocop/rails/schema_loader/schema.rb +5 -5
- data/lib/rubocop/rails/version.rb +5 -1
- metadata +37 -9
@@ -43,8 +43,9 @@ module RuboCop
|
|
43
43
|
# # good
|
44
44
|
# Time.current
|
45
45
|
# Time.at(timestamp).in_time_zone
|
46
|
-
class TimeZone <
|
46
|
+
class TimeZone < Base
|
47
47
|
include ConfigurableEnforcedStyle
|
48
|
+
extend AutoCorrector
|
48
49
|
|
49
50
|
MSG = 'Do not use `%<current>s` without zone. Use `%<prefer>s` ' \
|
50
51
|
'instead.'
|
@@ -71,26 +72,24 @@ module RuboCop
|
|
71
72
|
check_time_node(klass, node.parent) if klass == :Time
|
72
73
|
end
|
73
74
|
|
74
|
-
|
75
|
-
lambda do |corrector|
|
76
|
-
# add `.zone`: `Time.at` => `Time.zone.at`
|
77
|
-
corrector.insert_after(node.children[0].source_range, '.zone')
|
75
|
+
private
|
78
76
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
corrector.replace(node.loc.selector, 'now')
|
83
|
-
when :new
|
84
|
-
autocorrect_time_new(node, corrector)
|
85
|
-
end
|
77
|
+
def autocorrect(corrector, node)
|
78
|
+
# add `.zone`: `Time.at` => `Time.zone.at`
|
79
|
+
corrector.insert_after(node.children[0].source_range, '.zone')
|
86
80
|
|
87
|
-
|
88
|
-
|
89
|
-
|
81
|
+
case node.method_name
|
82
|
+
when :current
|
83
|
+
# replace `Time.zone.current` => `Time.zone.now`
|
84
|
+
corrector.replace(node.loc.selector, 'now')
|
85
|
+
when :new
|
86
|
+
autocorrect_time_new(node, corrector)
|
90
87
|
end
|
91
|
-
end
|
92
88
|
|
93
|
-
|
89
|
+
# prefer `Time` over `DateTime` class
|
90
|
+
corrector.replace(node.children.first.source_range, 'Time') if strict?
|
91
|
+
remove_redundant_in_time_zone(corrector, node)
|
92
|
+
end
|
94
93
|
|
95
94
|
def autocorrect_time_new(node, corrector)
|
96
95
|
if node.arguments?
|
@@ -128,7 +127,9 @@ module RuboCop
|
|
128
127
|
|
129
128
|
message = build_message(klass, method_name, node)
|
130
129
|
|
131
|
-
add_offense(node
|
130
|
+
add_offense(node.loc.selector, message: message) do |corrector|
|
131
|
+
autocorrect(corrector, node)
|
132
|
+
end
|
132
133
|
end
|
133
134
|
|
134
135
|
def build_message(klass, method_name, node)
|
@@ -193,8 +194,9 @@ module RuboCop
|
|
193
194
|
|
194
195
|
return if node.arguments?
|
195
196
|
|
196
|
-
add_offense(selector_node,
|
197
|
-
|
197
|
+
add_offense(selector_node.loc.selector, message: MSG_LOCALTIME) do |corrector|
|
198
|
+
autocorrect(corrector, selector_node)
|
199
|
+
end
|
198
200
|
end
|
199
201
|
|
200
202
|
def not_danger_chain?(chain)
|
@@ -18,12 +18,14 @@ module RuboCop
|
|
18
18
|
# ActiveRecord::Relation vs a call to pluck on an
|
19
19
|
# ActiveRecord::Associations::CollectionProxy.
|
20
20
|
#
|
21
|
+
# This cop is unsafe because the behavior may change depending on the
|
22
|
+
# database collation.
|
21
23
|
# Autocorrect is disabled by default for this cop since it may generate
|
22
24
|
# false positives.
|
23
25
|
#
|
24
26
|
# @example EnforcedStyle: conservative (default)
|
25
27
|
# # bad
|
26
|
-
# Model.pluck(:id).
|
28
|
+
# Model.pluck(:id).uniq
|
27
29
|
#
|
28
30
|
# # good
|
29
31
|
# Model.distinct.pluck(:id)
|
@@ -31,23 +33,25 @@ module RuboCop
|
|
31
33
|
# @example EnforcedStyle: aggressive
|
32
34
|
# # bad
|
33
35
|
# # this will return a Relation that pluck is called on
|
34
|
-
# Model.where(cond: true).pluck(:id).
|
36
|
+
# Model.where(cond: true).pluck(:id).uniq
|
35
37
|
#
|
36
38
|
# # bad
|
37
39
|
# # an association on an instance will return a CollectionProxy
|
38
|
-
# instance.assoc.pluck(:id).
|
40
|
+
# instance.assoc.pluck(:id).uniq
|
39
41
|
#
|
40
42
|
# # bad
|
41
|
-
# Model.pluck(:id).
|
43
|
+
# Model.pluck(:id).uniq
|
42
44
|
#
|
43
45
|
# # good
|
44
46
|
# Model.distinct.pluck(:id)
|
45
47
|
#
|
46
|
-
class UniqBeforePluck <
|
48
|
+
class UniqBeforePluck < Base
|
47
49
|
include ConfigurableEnforcedStyle
|
48
50
|
include RangeHelp
|
51
|
+
extend AutoCorrector
|
49
52
|
|
50
53
|
MSG = 'Use `distinct` before `pluck`.'
|
54
|
+
RESTRICT_ON_SEND = %i[uniq distinct pluck].freeze
|
51
55
|
NEWLINE = "\n"
|
52
56
|
PATTERN = '[!^block (send (send %<type>s :pluck ...) ' \
|
53
57
|
'${:uniq :distinct} ...)]'
|
@@ -67,11 +71,7 @@ module RuboCop
|
|
67
71
|
|
68
72
|
return unless method
|
69
73
|
|
70
|
-
add_offense(node
|
71
|
-
end
|
72
|
-
|
73
|
-
def autocorrect(node)
|
74
|
-
lambda do |corrector|
|
74
|
+
add_offense(node.loc.selector) do |corrector|
|
75
75
|
method = node.method_name
|
76
76
|
|
77
77
|
corrector.remove(dot_method_with_whitespace(method, node))
|
@@ -24,33 +24,37 @@ module RuboCop
|
|
24
24
|
# # good - even if the schema does not have a unique index
|
25
25
|
# validates :account, length: { minimum: MIN_LENGTH }
|
26
26
|
#
|
27
|
-
class UniqueValidationWithoutIndex <
|
27
|
+
class UniqueValidationWithoutIndex < Base
|
28
28
|
include ActiveRecordHelper
|
29
29
|
|
30
30
|
MSG = 'Uniqueness validation should be with a unique index.'
|
31
|
+
RESTRICT_ON_SEND = %i[validates].freeze
|
31
32
|
|
32
33
|
def on_send(node)
|
33
|
-
return unless node.method?(:validates)
|
34
34
|
return unless uniqueness_part(node)
|
35
35
|
return if condition_part?(node)
|
36
36
|
return unless schema
|
37
|
-
|
37
|
+
|
38
|
+
klass, table, names = find_schema_information(node)
|
39
|
+
return unless names
|
40
|
+
return if with_index?(klass, table, names)
|
38
41
|
|
39
42
|
add_offense(node)
|
40
43
|
end
|
41
44
|
|
42
45
|
private
|
43
46
|
|
44
|
-
def
|
47
|
+
def find_schema_information(node)
|
45
48
|
klass = class_node(node)
|
46
|
-
return
|
49
|
+
return unless klass
|
47
50
|
|
48
51
|
table = schema.table_by(name: table_name(klass))
|
49
|
-
return true unless table # Skip analysis if it can't find the table
|
50
|
-
|
51
52
|
names = column_names(node)
|
52
|
-
return true unless names
|
53
53
|
|
54
|
+
[klass, table, names]
|
55
|
+
end
|
56
|
+
|
57
|
+
def with_index?(klass, table, names)
|
54
58
|
# Compatibility for Rails 4.2.
|
55
59
|
add_indicies = schema.add_indicies_by(table_name: table_name(klass))
|
56
60
|
|
@@ -95,6 +99,8 @@ module RuboCop
|
|
95
99
|
scope = find_scope(uniq)
|
96
100
|
return unless scope
|
97
101
|
|
102
|
+
scope = unfreeze_scope(scope)
|
103
|
+
|
98
104
|
case scope.type
|
99
105
|
when :sym, :str
|
100
106
|
[scope.value]
|
@@ -112,6 +118,10 @@ module RuboCop
|
|
112
118
|
end
|
113
119
|
end
|
114
120
|
|
121
|
+
def unfreeze_scope(scope)
|
122
|
+
scope.send_type? && scope.method?(:freeze) ? scope.children.first : scope
|
123
|
+
end
|
124
|
+
|
115
125
|
def class_node(node)
|
116
126
|
node.each_ancestor.find(&:class_type?)
|
117
127
|
end
|
@@ -5,6 +5,9 @@ module RuboCop
|
|
5
5
|
module Rails
|
6
6
|
# This cop checks that environments called with `Rails.env` predicates
|
7
7
|
# exist.
|
8
|
+
# By default the cop allows three environments which Rails ships with:
|
9
|
+
# `development`, `test`, and `production`.
|
10
|
+
# More can be added to the `Environments` config parameter.
|
8
11
|
#
|
9
12
|
# @example
|
10
13
|
# # bad
|
@@ -14,7 +17,7 @@ module RuboCop
|
|
14
17
|
# # good
|
15
18
|
# Rails.env.production?
|
16
19
|
# Rails.env == 'production'
|
17
|
-
class UnknownEnv <
|
20
|
+
class UnknownEnv < Base
|
18
21
|
MSG = 'Unknown environment `%<name>s`.'
|
19
22
|
MSG_SIMILAR = 'Unknown environment `%<name>s`. ' \
|
20
23
|
'Did you mean `%<similar>s`?'
|
@@ -38,7 +41,7 @@ module RuboCop
|
|
38
41
|
|
39
42
|
def on_send(node)
|
40
43
|
unknown_environment_predicate?(node) do |name|
|
41
|
-
add_offense(node
|
44
|
+
add_offense(node.loc.selector, message: message(name))
|
42
45
|
end
|
43
46
|
|
44
47
|
unknown_environment_equal?(node) do |str_node|
|
@@ -56,8 +59,16 @@ module RuboCop
|
|
56
59
|
def message(name)
|
57
60
|
name = name.to_s.chomp('?')
|
58
61
|
|
59
|
-
|
60
|
-
|
62
|
+
# DidYouMean::SpellChecker is not available in all versions of Ruby,
|
63
|
+
# and even on versions where it *is* available (>= 2.3), it is not
|
64
|
+
# always required correctly. So we do a feature check first. See:
|
65
|
+
# https://github.com/rubocop-hq/rubocop/issues/7979
|
66
|
+
similar_names = if defined?(DidYouMean::SpellChecker)
|
67
|
+
spell_checker = DidYouMean::SpellChecker.new(dictionary: environments)
|
68
|
+
spell_checker.correct(name)
|
69
|
+
else
|
70
|
+
[]
|
71
|
+
end
|
61
72
|
|
62
73
|
if similar_names.empty?
|
63
74
|
format(MSG, name: name)
|
@@ -32,7 +32,9 @@ module RuboCop
|
|
32
32
|
# validates :foo, size: true
|
33
33
|
# validates :foo, uniqueness: true
|
34
34
|
#
|
35
|
-
class Validation <
|
35
|
+
class Validation < Base
|
36
|
+
extend AutoCorrector
|
37
|
+
|
36
38
|
MSG = 'Prefer the new style validations `%<prefer>s` over ' \
|
37
39
|
'`%<current>s`.'
|
38
40
|
|
@@ -50,22 +52,20 @@ module RuboCop
|
|
50
52
|
uniqueness
|
51
53
|
].freeze
|
52
54
|
|
53
|
-
|
55
|
+
RESTRICT_ON_SEND = TYPES.map { |p| "validates_#{p}_of".to_sym }.freeze
|
54
56
|
ALLOWLIST = TYPES.map { |p| "validates :column, #{p}: value" }.freeze
|
55
57
|
|
56
58
|
def on_send(node)
|
57
|
-
return
|
59
|
+
return if node.receiver
|
58
60
|
|
59
|
-
|
60
|
-
end
|
61
|
+
range = node.loc.selector
|
61
62
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
add_offense(range, message: message(node)) do |corrector|
|
64
|
+
last_argument = node.arguments.last
|
65
|
+
return if !last_argument.literal? && !last_argument.splat_type? &&
|
66
|
+
!frozen_array_argument?(last_argument)
|
66
67
|
|
67
|
-
|
68
|
-
corrector.replace(node.loc.selector, 'validates')
|
68
|
+
corrector.replace(range, 'validates')
|
69
69
|
correct_validate_type(corrector, node)
|
70
70
|
end
|
71
71
|
end
|
@@ -73,12 +73,13 @@ module RuboCop
|
|
73
73
|
private
|
74
74
|
|
75
75
|
def message(node)
|
76
|
-
|
77
|
-
|
76
|
+
method_name = node.method_name
|
77
|
+
|
78
|
+
format(MSG, prefer: preferred_method(method_name), current: method_name)
|
78
79
|
end
|
79
80
|
|
80
81
|
def preferred_method(method)
|
81
|
-
ALLOWLIST[
|
82
|
+
ALLOWLIST[RESTRICT_ON_SEND.index(method.to_sym)]
|
82
83
|
end
|
83
84
|
|
84
85
|
def correct_validate_type(corrector, node)
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop identifies places where manually constructed SQL
|
7
|
+
# in `where` can be replaced with `where(attribute: value)`.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# User.where('name = ?', 'Gabe')
|
12
|
+
# User.where('name = :name', name: 'Gabe')
|
13
|
+
# User.where('name IS NULL')
|
14
|
+
# User.where('name IN (?)', ['john', 'jane'])
|
15
|
+
# User.where('name IN (:names)', names: ['john', 'jane'])
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# User.where(name: 'Gabe')
|
19
|
+
# User.where(name: nil)
|
20
|
+
# User.where(name: ['john', 'jane'])
|
21
|
+
class WhereEquals < Base
|
22
|
+
include RangeHelp
|
23
|
+
extend AutoCorrector
|
24
|
+
|
25
|
+
MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
|
26
|
+
RESTRICT_ON_SEND = %i[where].freeze
|
27
|
+
|
28
|
+
def_node_matcher :where_method_call?, <<~PATTERN
|
29
|
+
{
|
30
|
+
(send _ :where (array $str_type? $_ ?))
|
31
|
+
(send _ :where $str_type? $_ ?)
|
32
|
+
}
|
33
|
+
PATTERN
|
34
|
+
|
35
|
+
def on_send(node)
|
36
|
+
where_method_call?(node) do |template_node, value_node|
|
37
|
+
value_node = value_node.first
|
38
|
+
|
39
|
+
range = offense_range(node)
|
40
|
+
|
41
|
+
column_and_value = extract_column_and_value(template_node, value_node)
|
42
|
+
return unless column_and_value
|
43
|
+
|
44
|
+
good_method = build_good_method(*column_and_value)
|
45
|
+
message = format(MSG, good_method: good_method)
|
46
|
+
|
47
|
+
add_offense(range, message: message) do |corrector|
|
48
|
+
corrector.replace(range, good_method)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
EQ_ANONYMOUS_RE = /\A([\w.]+)\s+=\s+\?\z/.freeze # column = ?
|
54
|
+
IN_ANONYMOUS_RE = /\A([\w.]+)\s+IN\s+\(\?\)\z/i.freeze # column IN (?)
|
55
|
+
EQ_NAMED_RE = /\A([\w.]+)\s+=\s+:(\w+)\z/.freeze # column = :column
|
56
|
+
IN_NAMED_RE = /\A([\w.]+)\s+IN\s+\(:(\w+)\)\z/i.freeze # column IN (:column)
|
57
|
+
IS_NULL_RE = /\A([\w.]+)\s+IS\s+NULL\z/i.freeze # column IS NULL
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def offense_range(node)
|
62
|
+
range_between(node.loc.selector.begin_pos, node.loc.expression.end_pos)
|
63
|
+
end
|
64
|
+
|
65
|
+
def extract_column_and_value(template_node, value_node)
|
66
|
+
value =
|
67
|
+
case template_node.value
|
68
|
+
when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
|
69
|
+
value_node.source
|
70
|
+
when EQ_NAMED_RE, IN_NAMED_RE
|
71
|
+
return unless value_node.hash_type?
|
72
|
+
|
73
|
+
pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
|
74
|
+
pair.value.source
|
75
|
+
when IS_NULL_RE
|
76
|
+
'nil'
|
77
|
+
else
|
78
|
+
return
|
79
|
+
end
|
80
|
+
|
81
|
+
[Regexp.last_match(1), value]
|
82
|
+
end
|
83
|
+
|
84
|
+
def build_good_method(column, value)
|
85
|
+
if column.include?('.')
|
86
|
+
"where('#{column}' => #{value})"
|
87
|
+
else
|
88
|
+
"where(#{column}: #{value})"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Rails
|
6
|
+
# This cop enforces consistent style when using `exists?`.
|
7
|
+
#
|
8
|
+
# Two styles are supported for this cop. When EnforcedStyle is 'exists'
|
9
|
+
# then the cop enforces `exists?(...)` over `where(...).exists?`.
|
10
|
+
#
|
11
|
+
# When EnforcedStyle is 'where' then the cop enforces
|
12
|
+
# `where(...).exists?` over `exists?(...)`.
|
13
|
+
#
|
14
|
+
# @example EnforcedStyle: exists (default)
|
15
|
+
# # bad
|
16
|
+
# User.where(name: 'john').exists?
|
17
|
+
# User.where(['name = ?', 'john']).exists?
|
18
|
+
# User.where('name = ?', 'john').exists?
|
19
|
+
# user.posts.where(published: true).exists?
|
20
|
+
#
|
21
|
+
# # good
|
22
|
+
# User.exists?(name: 'john')
|
23
|
+
# User.where('length(name) > 10').exists?
|
24
|
+
# user.posts.exists?(published: true)
|
25
|
+
#
|
26
|
+
# @example EnforcedStyle: where
|
27
|
+
# # bad
|
28
|
+
# User.exists?(name: 'john')
|
29
|
+
# User.exists?(['name = ?', 'john'])
|
30
|
+
# User.exists?('name = ?', 'john')
|
31
|
+
# user.posts.exists?(published: true)
|
32
|
+
#
|
33
|
+
# # good
|
34
|
+
# User.where(name: 'john').exists?
|
35
|
+
# User.where(['name = ?', 'john']).exists?
|
36
|
+
# User.where('name = ?', 'john').exists?
|
37
|
+
# user.posts.where(published: true).exists?
|
38
|
+
# User.where('length(name) > 10').exists?
|
39
|
+
class WhereExists < Base
|
40
|
+
include ConfigurableEnforcedStyle
|
41
|
+
extend AutoCorrector
|
42
|
+
|
43
|
+
MSG = 'Prefer `%<good_method>s` over `%<bad_method>s`.'
|
44
|
+
RESTRICT_ON_SEND = %i[exists?].freeze
|
45
|
+
|
46
|
+
def_node_matcher :where_exists_call?, <<~PATTERN
|
47
|
+
(send (send _ :where $...) :exists?)
|
48
|
+
PATTERN
|
49
|
+
|
50
|
+
def_node_matcher :exists_with_args?, <<~PATTERN
|
51
|
+
(send _ :exists? $...)
|
52
|
+
PATTERN
|
53
|
+
|
54
|
+
def on_send(node)
|
55
|
+
find_offenses(node) do |args|
|
56
|
+
return unless convertable_args?(args)
|
57
|
+
|
58
|
+
range = correction_range(node)
|
59
|
+
good_method = build_good_method(args)
|
60
|
+
message = format(MSG, good_method: good_method, bad_method: range.source)
|
61
|
+
|
62
|
+
add_offense(range, message: message) do |corrector|
|
63
|
+
corrector.replace(range, good_method)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def where_style?
|
71
|
+
style == :where
|
72
|
+
end
|
73
|
+
|
74
|
+
def exists_style?
|
75
|
+
style == :exists
|
76
|
+
end
|
77
|
+
|
78
|
+
def find_offenses(node, &block)
|
79
|
+
if exists_style?
|
80
|
+
where_exists_call?(node, &block)
|
81
|
+
elsif where_style?
|
82
|
+
exists_with_args?(node, &block)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def convertable_args?(args)
|
87
|
+
return false if args.empty?
|
88
|
+
|
89
|
+
args.size > 1 || args[0].hash_type? || args[0].array_type?
|
90
|
+
end
|
91
|
+
|
92
|
+
def correction_range(node)
|
93
|
+
if exists_style?
|
94
|
+
node.receiver.loc.selector.join(node.loc.selector)
|
95
|
+
elsif where_style?
|
96
|
+
node.loc.selector.with(end_pos: node.loc.expression.end_pos)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def build_good_method(args)
|
101
|
+
if exists_style?
|
102
|
+
build_good_method_exists(args)
|
103
|
+
elsif where_style?
|
104
|
+
build_good_method_where(args)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def build_good_method_exists(args)
|
109
|
+
if args.size > 1
|
110
|
+
"exists?([#{args.map(&:source).join(', ')}])"
|
111
|
+
else
|
112
|
+
"exists?(#{args[0].source})"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def build_good_method_where(args)
|
117
|
+
if args.size > 1
|
118
|
+
"where(#{args.map(&:source).join(', ')}).exists?"
|
119
|
+
else
|
120
|
+
"where(#{args[0].source}).exists?"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|