rubocop-rails 2.23.1 → 2.26.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/README.md +16 -2
- data/config/default.yml +32 -7
- data/lib/rubocop/cop/mixin/active_record_helper.rb +6 -1
- data/lib/rubocop/cop/mixin/target_rails_version.rb +29 -2
- data/lib/rubocop/cop/rails/action_controller_flash_before_render.rb +2 -0
- data/lib/rubocop/cop/rails/action_order.rb +1 -5
- data/lib/rubocop/cop/rails/active_record_callbacks_order.rb +1 -5
- data/lib/rubocop/cop/rails/active_support_on_load.rb +21 -1
- data/lib/rubocop/cop/rails/application_record.rb +4 -0
- data/lib/rubocop/cop/rails/bulk_change_table.rb +10 -4
- data/lib/rubocop/cop/rails/compact_blank.rb +29 -8
- data/lib/rubocop/cop/rails/dangerous_column_names.rb +1 -2
- data/lib/rubocop/cop/rails/date.rb +2 -2
- data/lib/rubocop/cop/rails/enum_hash.rb +31 -8
- data/lib/rubocop/cop/rails/enum_syntax.rb +128 -0
- data/lib/rubocop/cop/rails/enum_uniqueness.rb +29 -7
- data/lib/rubocop/cop/rails/expanded_date_range.rb +1 -1
- data/lib/rubocop/cop/rails/file_path.rb +1 -1
- data/lib/rubocop/cop/rails/find_by.rb +1 -1
- data/lib/rubocop/cop/rails/http_status.rb +12 -2
- data/lib/rubocop/cop/rails/ignored_skip_action_filter_option.rb +1 -1
- data/lib/rubocop/cop/rails/link_to_blank.rb +2 -2
- data/lib/rubocop/cop/rails/not_null_column.rb +93 -13
- data/lib/rubocop/cop/rails/pick.rb +4 -0
- data/lib/rubocop/cop/rails/pluck_in_where.rb +17 -8
- data/lib/rubocop/cop/rails/pluralization_grammar.rb +29 -15
- data/lib/rubocop/cop/rails/present.rb +0 -2
- data/lib/rubocop/cop/rails/redundant_active_record_all_method.rb +0 -29
- data/lib/rubocop/cop/rails/redundant_foreign_key.rb +1 -1
- data/lib/rubocop/cop/rails/redundant_presence_validation_on_belongs_to.rb +9 -0
- data/lib/rubocop/cop/rails/redundant_receiver_in_with_options.rb +1 -1
- data/lib/rubocop/cop/rails/reflection_class_name.rb +1 -1
- data/lib/rubocop/cop/rails/render_plain_text.rb +6 -3
- data/lib/rubocop/cop/rails/request_referer.rb +1 -1
- data/lib/rubocop/cop/rails/reversible_migration.rb +1 -1
- data/lib/rubocop/cop/rails/root_pathname_methods.rb +15 -11
- data/lib/rubocop/cop/rails/save_bang.rb +2 -0
- data/lib/rubocop/cop/rails/skips_model_validations.rb +8 -3
- data/lib/rubocop/cop/rails/time_zone.rb +2 -1
- data/lib/rubocop/cop/rails/uniq_before_pluck.rb +12 -4
- data/lib/rubocop/cop/rails/unknown_env.rb +1 -1
- data/lib/rubocop/cop/rails/unused_ignored_columns.rb +6 -0
- data/lib/rubocop/cop/rails/validation.rb +8 -3
- data/lib/rubocop/cop/rails/where_equals.rb +28 -12
- data/lib/rubocop/cop/rails/where_exists.rb +3 -3
- data/lib/rubocop/cop/rails/where_missing.rb +5 -1
- data/lib/rubocop/cop/rails/where_not.rb +11 -6
- data/lib/rubocop/cop/rails/where_range.rb +203 -0
- data/lib/rubocop/cop/rails_cops.rb +2 -0
- data/lib/rubocop/rails/schema_loader/schema.rb +2 -1
- data/lib/rubocop/rails/schema_loader.rb +5 -15
- data/lib/rubocop/rails/version.rb +1 -1
- metadata +10 -8
|
@@ -53,9 +53,12 @@ module RuboCop
|
|
|
53
53
|
node.pairs.find { |p| p.key.value.to_sym == :content_type }
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def compatible_content_type?(
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
def compatible_content_type?(pair_node)
|
|
57
|
+
if pair_node.nil?
|
|
58
|
+
!cop_config['ContentTypeCompatibility']
|
|
59
|
+
elsif pair_node.value.respond_to?(:value)
|
|
60
|
+
pair_node.value.value == 'text/plain'
|
|
61
|
+
end
|
|
59
62
|
end
|
|
60
63
|
|
|
61
64
|
def replacement(rest_options, option_value)
|
|
@@ -23,6 +23,8 @@ module RuboCop
|
|
|
23
23
|
# File.binread(Rails.root.join('db', 'schema.rb'))
|
|
24
24
|
# File.write(Rails.root.join('db', 'schema.rb'), content)
|
|
25
25
|
# File.binwrite(Rails.root.join('db', 'schema.rb'), content)
|
|
26
|
+
# Dir.glob(Rails.root.join('db', 'schema.rb'))
|
|
27
|
+
# Dir[Rails.root.join('db', 'schema.rb')]
|
|
26
28
|
#
|
|
27
29
|
# # good
|
|
28
30
|
# Rails.root.join('db', 'schema.rb').open
|
|
@@ -31,14 +33,15 @@ module RuboCop
|
|
|
31
33
|
# Rails.root.join('db', 'schema.rb').binread
|
|
32
34
|
# Rails.root.join('db', 'schema.rb').write(content)
|
|
33
35
|
# Rails.root.join('db', 'schema.rb').binwrite(content)
|
|
36
|
+
# Rails.root.glob("db/schema.rb")
|
|
34
37
|
#
|
|
35
38
|
class RootPathnameMethods < Base # rubocop:disable Metrics/ClassLength
|
|
36
39
|
extend AutoCorrector
|
|
37
40
|
include RangeHelp
|
|
38
41
|
|
|
39
|
-
MSG = '`%<rails_root>s` is a `Pathname
|
|
42
|
+
MSG = '`%<rails_root>s` is a `Pathname`, so you can use `%<replacement>s`.'
|
|
40
43
|
|
|
41
|
-
DIR_GLOB_METHODS = %i[glob].to_set.freeze
|
|
44
|
+
DIR_GLOB_METHODS = %i[[] glob].to_set.freeze
|
|
42
45
|
|
|
43
46
|
DIR_NON_GLOB_METHODS = %i[
|
|
44
47
|
children
|
|
@@ -171,7 +174,7 @@ module RuboCop
|
|
|
171
174
|
|
|
172
175
|
def_node_matcher :dir_glob?, <<~PATTERN
|
|
173
176
|
(send
|
|
174
|
-
(const {cbase nil?} :Dir)
|
|
177
|
+
(const {cbase nil?} :Dir) DIR_GLOB_METHODS ...)
|
|
175
178
|
PATTERN
|
|
176
179
|
|
|
177
180
|
def_node_matcher :rails_root_pathname?, <<~PATTERN
|
|
@@ -188,13 +191,14 @@ module RuboCop
|
|
|
188
191
|
|
|
189
192
|
def on_send(node)
|
|
190
193
|
evidence(node) do |method, path, args, rails_root|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
end
|
|
194
|
+
replacement = if dir_glob?(node)
|
|
195
|
+
build_path_glob_replacement(path)
|
|
196
|
+
else
|
|
197
|
+
build_path_replacement(path, method, args)
|
|
198
|
+
end
|
|
197
199
|
|
|
200
|
+
message = format(MSG, rails_root: rails_root.source, replacement: replacement)
|
|
201
|
+
add_offense(node, message: message) do |corrector|
|
|
198
202
|
corrector.replace(node, replacement)
|
|
199
203
|
end
|
|
200
204
|
end
|
|
@@ -217,12 +221,12 @@ module RuboCop
|
|
|
217
221
|
end
|
|
218
222
|
end
|
|
219
223
|
|
|
220
|
-
def build_path_glob_replacement(path
|
|
224
|
+
def build_path_glob_replacement(path)
|
|
221
225
|
receiver = range_between(path.source_range.begin_pos, path.children.first.loc.selector.end_pos).source
|
|
222
226
|
|
|
223
227
|
argument = path.arguments.one? ? path.first_argument.source : join_arguments(path.arguments)
|
|
224
228
|
|
|
225
|
-
"#{receiver}
|
|
229
|
+
"#{receiver}.glob(#{argument})"
|
|
226
230
|
end
|
|
227
231
|
|
|
228
232
|
def build_path_replacement(path, method, args)
|
|
@@ -9,6 +9,9 @@ module RuboCop
|
|
|
9
9
|
#
|
|
10
10
|
# Methods may be ignored from this rule by configuring a `AllowedMethods`.
|
|
11
11
|
#
|
|
12
|
+
# @safety
|
|
13
|
+
# This cop is unsafe if the receiver object is not an Active Record object.
|
|
14
|
+
#
|
|
12
15
|
# @example
|
|
13
16
|
# # bad
|
|
14
17
|
# Article.first.decrement!(:view_count)
|
|
@@ -63,7 +66,7 @@ module RuboCop
|
|
|
63
66
|
PATTERN
|
|
64
67
|
|
|
65
68
|
def_node_matcher :good_insert?, <<~PATTERN
|
|
66
|
-
(
|
|
69
|
+
(call _ {:insert :insert!} _ {
|
|
67
70
|
!(hash ...)
|
|
68
71
|
(hash <(pair (sym !{:returning :unique_by}) _) ...>)
|
|
69
72
|
} ...)
|
|
@@ -97,7 +100,8 @@ module RuboCop
|
|
|
97
100
|
end
|
|
98
101
|
|
|
99
102
|
def forbidden_methods
|
|
100
|
-
|
|
103
|
+
# TODO: Remove when RuboCop Rails 3 releases.
|
|
104
|
+
obsolete_result = cop_config['Blacklist'] # rubocop:disable InternalAffairs/UndefinedConfig
|
|
101
105
|
if obsolete_result
|
|
102
106
|
warn '`Blacklist` has been renamed to `ForbiddenMethods`.' unless @displayed_forbidden_warning
|
|
103
107
|
@displayed_forbidden_warning = true
|
|
@@ -108,7 +112,8 @@ module RuboCop
|
|
|
108
112
|
end
|
|
109
113
|
|
|
110
114
|
def allowed_methods
|
|
111
|
-
|
|
115
|
+
# TODO: Remove when RuboCop Rails 3 releases.
|
|
116
|
+
obsolete_result = cop_config['Whitelist'] # rubocop:disable InternalAffairs/UndefinedConfig
|
|
112
117
|
if obsolete_result
|
|
113
118
|
warn '`Whitelist` has been renamed to `AllowedMethods`.' unless @displayed_allowed_warning
|
|
114
119
|
@displayed_allowed_warning = true
|
|
@@ -69,9 +69,10 @@ module RuboCop
|
|
|
69
69
|
return if !node.receiver&.str_type? || !node.method?(:to_time)
|
|
70
70
|
|
|
71
71
|
add_offense(node.loc.selector, message: MSG_STRING_TO_TIME) do |corrector|
|
|
72
|
-
corrector.replace(node, "Time.zone.parse(#{node.receiver.source})")
|
|
72
|
+
corrector.replace(node, "Time.zone.parse(#{node.receiver.source})") unless node.csend_type?
|
|
73
73
|
end
|
|
74
74
|
end
|
|
75
|
+
alias on_csend on_send
|
|
75
76
|
|
|
76
77
|
private
|
|
77
78
|
|
|
@@ -68,15 +68,23 @@ module RuboCop
|
|
|
68
68
|
return unless uniq
|
|
69
69
|
|
|
70
70
|
add_offense(node.loc.selector) do |corrector|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
corrector.remove(dot_method_with_whitespace(method, node))
|
|
74
|
-
corrector.insert_before(node.receiver.loc.dot.begin, '.distinct')
|
|
71
|
+
autocorrect(corrector, node)
|
|
75
72
|
end
|
|
76
73
|
end
|
|
77
74
|
|
|
78
75
|
private
|
|
79
76
|
|
|
77
|
+
def autocorrect(corrector, node)
|
|
78
|
+
method = node.method_name
|
|
79
|
+
|
|
80
|
+
corrector.remove(dot_method_with_whitespace(method, node))
|
|
81
|
+
if (dot = node.receiver.loc.dot)
|
|
82
|
+
corrector.insert_before(dot.begin, '.distinct')
|
|
83
|
+
else
|
|
84
|
+
corrector.insert_before(node.receiver, 'distinct.')
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
80
88
|
def dot_method_with_whitespace(method, node)
|
|
81
89
|
range_between(dot_method_begin_pos(method, node), node.loc.selector.end_pos)
|
|
82
90
|
end
|
|
@@ -7,6 +7,12 @@ module RuboCop
|
|
|
7
7
|
# `ignored_columns` is necessary to drop a column from RDBMS, but you don't need it after the migration
|
|
8
8
|
# to drop the column. You avoid forgetting to remove `ignored_columns` by this cop.
|
|
9
9
|
#
|
|
10
|
+
# IMPORTANT: This cop can't be used to effectively check for unused columns because the development
|
|
11
|
+
# and production schema can be out of sync until the migration has been run on production. As such,
|
|
12
|
+
# this cop can cause `ignored_columns` to be removed even though the production schema still contains
|
|
13
|
+
# the column, which can lead to downtime when the migration is actually executed. Only enable this cop
|
|
14
|
+
# if you know your migrations will be run before any of your Rails applications boot with the modified code.
|
|
15
|
+
#
|
|
10
16
|
# @example
|
|
11
17
|
# # bad
|
|
12
18
|
# class User < ApplicationRecord
|
|
@@ -8,6 +8,7 @@ module RuboCop
|
|
|
8
8
|
# @example
|
|
9
9
|
# # bad
|
|
10
10
|
# validates_acceptance_of :foo
|
|
11
|
+
# validates_comparison_of :foo
|
|
11
12
|
# validates_confirmation_of :foo
|
|
12
13
|
# validates_exclusion_of :foo
|
|
13
14
|
# validates_format_of :foo
|
|
@@ -22,6 +23,7 @@ module RuboCop
|
|
|
22
23
|
# # good
|
|
23
24
|
# validates :foo, acceptance: true
|
|
24
25
|
# validates :foo, confirmation: true
|
|
26
|
+
# validates :foo, comparison: true
|
|
25
27
|
# validates :foo, exclusion: true
|
|
26
28
|
# validates :foo, format: true
|
|
27
29
|
# validates :foo, inclusion: true
|
|
@@ -29,7 +31,7 @@ module RuboCop
|
|
|
29
31
|
# validates :foo, numericality: true
|
|
30
32
|
# validates :foo, presence: true
|
|
31
33
|
# validates :foo, absence: true
|
|
32
|
-
# validates :foo,
|
|
34
|
+
# validates :foo, length: true
|
|
33
35
|
# validates :foo, uniqueness: true
|
|
34
36
|
#
|
|
35
37
|
class Validation < Base
|
|
@@ -39,6 +41,7 @@ module RuboCop
|
|
|
39
41
|
|
|
40
42
|
TYPES = %w[
|
|
41
43
|
acceptance
|
|
44
|
+
comparison
|
|
42
45
|
confirmation
|
|
43
46
|
exclusion
|
|
44
47
|
format
|
|
@@ -56,11 +59,11 @@ module RuboCop
|
|
|
56
59
|
|
|
57
60
|
def on_send(node)
|
|
58
61
|
return if node.receiver
|
|
62
|
+
return unless (last_argument = node.last_argument)
|
|
59
63
|
|
|
60
64
|
range = node.loc.selector
|
|
61
65
|
|
|
62
66
|
add_offense(range, message: message(node)) do |corrector|
|
|
63
|
-
last_argument = node.last_argument
|
|
64
67
|
return if !last_argument.literal? && !last_argument.splat_type? && !frozen_array_argument?(last_argument)
|
|
65
68
|
|
|
66
69
|
corrector.replace(range, 'validates')
|
|
@@ -120,7 +123,9 @@ module RuboCop
|
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
def validate_type(node)
|
|
123
|
-
node.method_name.to_s.split('_')[1]
|
|
126
|
+
type = node.method_name.to_s.split('_')[1]
|
|
127
|
+
|
|
128
|
+
type == 'size' ? 'length' : type
|
|
124
129
|
end
|
|
125
130
|
|
|
126
131
|
def frozen_array_argument?(argument)
|
|
@@ -4,7 +4,8 @@ module RuboCop
|
|
|
4
4
|
module Cop
|
|
5
5
|
module Rails
|
|
6
6
|
# Identifies places where manually constructed SQL
|
|
7
|
-
# in `where` can be replaced with
|
|
7
|
+
# in `where` and `where.not` can be replaced with
|
|
8
|
+
# `where(attribute: value)` and `where.not(attribute: value)`.
|
|
8
9
|
#
|
|
9
10
|
# @safety
|
|
10
11
|
# This cop's autocorrection is unsafe because is may change SQL.
|
|
@@ -13,6 +14,7 @@ module RuboCop
|
|
|
13
14
|
# @example
|
|
14
15
|
# # bad
|
|
15
16
|
# User.where('name = ?', 'Gabe')
|
|
17
|
+
# User.where.not('name = ?', 'Gabe')
|
|
16
18
|
# User.where('name = :name', name: 'Gabe')
|
|
17
19
|
# User.where('name IS NULL')
|
|
18
20
|
# User.where('name IN (?)', ['john', 'jane'])
|
|
@@ -21,6 +23,7 @@ module RuboCop
|
|
|
21
23
|
#
|
|
22
24
|
# # good
|
|
23
25
|
# User.where(name: 'Gabe')
|
|
26
|
+
# User.where.not(name: 'Gabe')
|
|
24
27
|
# User.where(name: nil)
|
|
25
28
|
# User.where(name: ['john', 'jane'])
|
|
26
29
|
# User.where(users: { name: 'Gabe' })
|
|
@@ -29,25 +32,27 @@ module RuboCop
|
|
|
29
32
|
extend AutoCorrector
|
|
30
33
|
|
|
31
34
|
MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
|
|
32
|
-
RESTRICT_ON_SEND = %i[where].freeze
|
|
35
|
+
RESTRICT_ON_SEND = %i[where not].freeze
|
|
33
36
|
|
|
34
37
|
def_node_matcher :where_method_call?, <<~PATTERN
|
|
35
38
|
{
|
|
36
|
-
(call _ :where (array $str_type? $_ ?))
|
|
37
|
-
(call _ :where $str_type? $_ ?)
|
|
39
|
+
(call _ {:where :not} (array $str_type? $_ ?))
|
|
40
|
+
(call _ {:where :not} $str_type? $_ ?)
|
|
38
41
|
}
|
|
39
42
|
PATTERN
|
|
40
43
|
|
|
41
44
|
def on_send(node)
|
|
45
|
+
return if node.method?(:not) && !where_not?(node)
|
|
46
|
+
|
|
42
47
|
where_method_call?(node) do |template_node, value_node|
|
|
43
48
|
value_node = value_node.first
|
|
44
49
|
|
|
45
50
|
range = offense_range(node)
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
return unless
|
|
52
|
+
column, value = extract_column_and_value(template_node, value_node)
|
|
53
|
+
return unless value
|
|
49
54
|
|
|
50
|
-
good_method = build_good_method(
|
|
55
|
+
good_method = build_good_method(node.method_name, column, value)
|
|
51
56
|
message = format(MSG, good_method: good_method)
|
|
52
57
|
|
|
53
58
|
add_offense(range, message: message) do |corrector|
|
|
@@ -69,11 +74,12 @@ module RuboCop
|
|
|
69
74
|
range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
|
|
70
75
|
end
|
|
71
76
|
|
|
77
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
72
78
|
def extract_column_and_value(template_node, value_node)
|
|
73
79
|
value =
|
|
74
80
|
case template_node.value
|
|
75
81
|
when EQ_ANONYMOUS_RE, IN_ANONYMOUS_RE
|
|
76
|
-
value_node
|
|
82
|
+
value_node&.source
|
|
77
83
|
when EQ_NAMED_RE, IN_NAMED_RE
|
|
78
84
|
return unless value_node&.hash_type?
|
|
79
85
|
|
|
@@ -85,18 +91,28 @@ module RuboCop
|
|
|
85
91
|
return
|
|
86
92
|
end
|
|
87
93
|
|
|
88
|
-
|
|
94
|
+
column_qualifier = Regexp.last_match(1)
|
|
95
|
+
return if column_qualifier.count('.') > 1
|
|
96
|
+
|
|
97
|
+
[column_qualifier, value]
|
|
89
98
|
end
|
|
99
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
90
100
|
|
|
91
|
-
def build_good_method(column, value)
|
|
101
|
+
def build_good_method(method_name, column, value)
|
|
92
102
|
if column.include?('.')
|
|
93
103
|
table, column = column.split('.')
|
|
94
104
|
|
|
95
|
-
"
|
|
105
|
+
"#{method_name}(#{table}: { #{column}: #{value} })"
|
|
96
106
|
else
|
|
97
|
-
"
|
|
107
|
+
"#{method_name}(#{column}: #{value})"
|
|
98
108
|
end
|
|
99
109
|
end
|
|
110
|
+
|
|
111
|
+
def where_not?(node)
|
|
112
|
+
return false unless (receiver = node.receiver)
|
|
113
|
+
|
|
114
|
+
receiver.send_type? && receiver.method?(:where)
|
|
115
|
+
end
|
|
100
116
|
end
|
|
101
117
|
end
|
|
102
118
|
end
|
|
@@ -67,7 +67,7 @@ module RuboCop
|
|
|
67
67
|
return unless convertable_args?(args)
|
|
68
68
|
|
|
69
69
|
range = correction_range(node)
|
|
70
|
-
good_method = build_good_method(args,
|
|
70
|
+
good_method = build_good_method(args, dot: node.loc.dot)
|
|
71
71
|
message = format(MSG, good_method: good_method, bad_method: range.source)
|
|
72
72
|
|
|
73
73
|
add_offense(range, message: message) do |corrector|
|
|
@@ -109,11 +109,11 @@ module RuboCop
|
|
|
109
109
|
end
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
def build_good_method(args,
|
|
112
|
+
def build_good_method(args, dot:)
|
|
113
113
|
if exists_style?
|
|
114
114
|
build_good_method_exists(args)
|
|
115
115
|
elsif where_style?
|
|
116
|
-
build_good_method_where(args,
|
|
116
|
+
build_good_method_where(args, dot&.source || '.')
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
119
|
|
|
@@ -89,16 +89,20 @@ module RuboCop
|
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
# rubocop:disable Metrics/AbcSize
|
|
92
93
|
def remove_where_method(corrector, node, where_node)
|
|
93
94
|
range = range_between(where_node.loc.selector.begin_pos, where_node.loc.end.end_pos)
|
|
94
95
|
if node.multiline? && !same_line?(node, where_node)
|
|
95
96
|
range = range_by_whole_lines(range, include_final_newline: true)
|
|
96
|
-
|
|
97
|
+
elsif where_node.receiver
|
|
97
98
|
corrector.remove(where_node.loc.dot)
|
|
99
|
+
else
|
|
100
|
+
corrector.remove(node.loc.dot)
|
|
98
101
|
end
|
|
99
102
|
|
|
100
103
|
corrector.remove(range)
|
|
101
104
|
end
|
|
105
|
+
# rubocop:enable Metrics/AbcSize
|
|
102
106
|
|
|
103
107
|
def same_line?(left_joins_node, where_node)
|
|
104
108
|
left_joins_node.loc.selector.line == where_node.loc.selector.line
|
|
@@ -43,10 +43,10 @@ module RuboCop
|
|
|
43
43
|
|
|
44
44
|
range = offense_range(node)
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
return unless
|
|
46
|
+
column, value = extract_column_and_value(template_node, value_node)
|
|
47
|
+
return unless value
|
|
48
48
|
|
|
49
|
-
good_method = build_good_method(node.loc.dot&.source,
|
|
49
|
+
good_method = build_good_method(node.loc.dot&.source, column, value)
|
|
50
50
|
message = format(MSG, good_method: good_method)
|
|
51
51
|
|
|
52
52
|
add_offense(range, message: message) do |corrector|
|
|
@@ -68,13 +68,14 @@ module RuboCop
|
|
|
68
68
|
range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
71
72
|
def extract_column_and_value(template_node, value_node)
|
|
72
73
|
value =
|
|
73
74
|
case template_node.value
|
|
74
75
|
when NOT_EQ_ANONYMOUS_RE, NOT_IN_ANONYMOUS_RE
|
|
75
|
-
value_node
|
|
76
|
+
value_node&.source
|
|
76
77
|
when NOT_EQ_NAMED_RE, NOT_IN_NAMED_RE
|
|
77
|
-
return unless value_node
|
|
78
|
+
return unless value_node&.hash_type?
|
|
78
79
|
|
|
79
80
|
pair = value_node.pairs.find { |p| p.key.value.to_sym == Regexp.last_match(2).to_sym }
|
|
80
81
|
pair.value.source
|
|
@@ -84,8 +85,12 @@ module RuboCop
|
|
|
84
85
|
return
|
|
85
86
|
end
|
|
86
87
|
|
|
87
|
-
|
|
88
|
+
column_qualifier = Regexp.last_match(1)
|
|
89
|
+
return if column_qualifier.count('.') > 1
|
|
90
|
+
|
|
91
|
+
[column_qualifier, value]
|
|
88
92
|
end
|
|
93
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
89
94
|
|
|
90
95
|
def build_good_method(dot, column, value)
|
|
91
96
|
dot ||= '.'
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RuboCop
|
|
4
|
+
module Cop
|
|
5
|
+
module Rails
|
|
6
|
+
# Identifies places where manually constructed SQL
|
|
7
|
+
# in `where` can be replaced with ranges.
|
|
8
|
+
#
|
|
9
|
+
# @safety
|
|
10
|
+
# This cop's autocorrection is unsafe because it can change the query
|
|
11
|
+
# by explicitly attaching the column to the wrong table.
|
|
12
|
+
# For example, `Booking.joins(:events).where('end_at < ?', Time.current)` will correctly
|
|
13
|
+
# implicitly attach the `end_at` column to the `events` table. But when autocorrected to
|
|
14
|
+
# `Booking.joins(:events).where(end_at: ...Time.current)`, it will now be incorrectly
|
|
15
|
+
# explicitly attached to the `bookings` table.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# # bad
|
|
19
|
+
# User.where('age >= ?', 18)
|
|
20
|
+
# User.where.not('age >= ?', 18)
|
|
21
|
+
# User.where('age < ?', 18)
|
|
22
|
+
# User.where('age >= ? AND age < ?', 18, 21)
|
|
23
|
+
# User.where('age >= :start', start: 18)
|
|
24
|
+
# User.where('users.age >= ?', 18)
|
|
25
|
+
#
|
|
26
|
+
# # good
|
|
27
|
+
# User.where(age: 18..)
|
|
28
|
+
# User.where.not(age: 18..)
|
|
29
|
+
# User.where(age: ...18)
|
|
30
|
+
# User.where(age: 18...21)
|
|
31
|
+
# User.where(users: { age: 18.. })
|
|
32
|
+
#
|
|
33
|
+
# # good
|
|
34
|
+
# # There are no beginless ranges in ruby.
|
|
35
|
+
# User.where('age > ?', 18)
|
|
36
|
+
#
|
|
37
|
+
class WhereRange < Base
|
|
38
|
+
include RangeHelp
|
|
39
|
+
extend AutoCorrector
|
|
40
|
+
extend TargetRubyVersion
|
|
41
|
+
extend TargetRailsVersion
|
|
42
|
+
|
|
43
|
+
MSG = 'Use `%<good_method>s` instead of manually constructing SQL.'
|
|
44
|
+
|
|
45
|
+
RESTRICT_ON_SEND = %i[where not].freeze
|
|
46
|
+
|
|
47
|
+
# column >= ?
|
|
48
|
+
GTEQ_ANONYMOUS_RE = /\A\s*([\w.]+)\s+>=\s+\?\s*\z/.freeze
|
|
49
|
+
# column <[=] ?
|
|
50
|
+
LTEQ_ANONYMOUS_RE = /\A\s*([\w.]+)\s+(<=?)\s+\?\s*\z/.freeze
|
|
51
|
+
# column >= ? AND column <[=] ?
|
|
52
|
+
RANGE_ANONYMOUS_RE = /\A\s*([\w.]+)\s+>=\s+\?\s+AND\s+\1\s+(<=?)\s+\?\s*\z/i.freeze
|
|
53
|
+
# column >= :value
|
|
54
|
+
GTEQ_NAMED_RE = /\A\s*([\w.]+)\s+>=\s+:(\w+)\s*\z/.freeze
|
|
55
|
+
# column <[=] :value
|
|
56
|
+
LTEQ_NAMED_RE = /\A\s*([\w.]+)\s+(<=?)\s+:(\w+)\s*\z/.freeze
|
|
57
|
+
# column >= :value1 AND column <[=] :value2
|
|
58
|
+
RANGE_NAMED_RE = /\A\s*([\w.]+)\s+>=\s+:(\w+)\s+AND\s+\1\s+(<=?)\s+:(\w+)\s*\z/i.freeze
|
|
59
|
+
|
|
60
|
+
minimum_target_ruby_version 2.6
|
|
61
|
+
minimum_target_rails_version 6.0
|
|
62
|
+
|
|
63
|
+
def_node_matcher :where_range_call?, <<~PATTERN
|
|
64
|
+
{
|
|
65
|
+
(call _ {:where :not} (array $str_type? $_ +))
|
|
66
|
+
(call _ {:where :not} $str_type? $_ +)
|
|
67
|
+
}
|
|
68
|
+
PATTERN
|
|
69
|
+
|
|
70
|
+
def on_send(node)
|
|
71
|
+
return if node.method?(:not) && !where_not?(node)
|
|
72
|
+
|
|
73
|
+
where_range_call?(node) do |template_node, values_node|
|
|
74
|
+
column, value = extract_column_and_value(template_node, values_node)
|
|
75
|
+
|
|
76
|
+
return unless column
|
|
77
|
+
|
|
78
|
+
range = offense_range(node)
|
|
79
|
+
good_method = build_good_method(node.method_name, column, value)
|
|
80
|
+
message = format(MSG, good_method: good_method)
|
|
81
|
+
|
|
82
|
+
add_offense(range, message: message) do |corrector|
|
|
83
|
+
corrector.replace(range, good_method)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def where_not?(node)
|
|
91
|
+
receiver = node.receiver
|
|
92
|
+
receiver&.send_type? && receiver&.method?(:where)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# rubocop:disable Metrics
|
|
96
|
+
def extract_column_and_value(template_node, values_node)
|
|
97
|
+
case template_node.value
|
|
98
|
+
when GTEQ_ANONYMOUS_RE
|
|
99
|
+
lhs = values_node[0]
|
|
100
|
+
operator = '..'
|
|
101
|
+
when LTEQ_ANONYMOUS_RE
|
|
102
|
+
if target_ruby_version >= 2.7
|
|
103
|
+
operator = range_operator(Regexp.last_match(2))
|
|
104
|
+
rhs = values_node[0]
|
|
105
|
+
end
|
|
106
|
+
when RANGE_ANONYMOUS_RE
|
|
107
|
+
if values_node.size >= 2
|
|
108
|
+
lhs = values_node[0]
|
|
109
|
+
operator = range_operator(Regexp.last_match(2))
|
|
110
|
+
rhs = values_node[1]
|
|
111
|
+
end
|
|
112
|
+
when GTEQ_NAMED_RE
|
|
113
|
+
value_node = values_node[0]
|
|
114
|
+
|
|
115
|
+
if value_node.hash_type?
|
|
116
|
+
pair = find_pair(value_node, Regexp.last_match(2))
|
|
117
|
+
lhs = pair.value
|
|
118
|
+
operator = '..'
|
|
119
|
+
end
|
|
120
|
+
when LTEQ_NAMED_RE
|
|
121
|
+
value_node = values_node[0]
|
|
122
|
+
|
|
123
|
+
if value_node.hash_type?
|
|
124
|
+
pair = find_pair(value_node, Regexp.last_match(2))
|
|
125
|
+
if pair && target_ruby_version >= 2.7
|
|
126
|
+
operator = range_operator(Regexp.last_match(2))
|
|
127
|
+
rhs = pair.value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
when RANGE_NAMED_RE
|
|
131
|
+
value_node = values_node[0]
|
|
132
|
+
|
|
133
|
+
if value_node.hash_type?
|
|
134
|
+
pair1 = find_pair(value_node, Regexp.last_match(2))
|
|
135
|
+
pair2 = find_pair(value_node, Regexp.last_match(4))
|
|
136
|
+
|
|
137
|
+
if pair1 && pair2
|
|
138
|
+
lhs = pair1.value
|
|
139
|
+
operator = range_operator(Regexp.last_match(3))
|
|
140
|
+
rhs = pair2.value
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
else
|
|
144
|
+
return
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
if lhs
|
|
148
|
+
lhs_source = parentheses_needed?(lhs) ? "(#{lhs.source})" : lhs.source
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
if rhs
|
|
152
|
+
rhs_source = parentheses_needed?(rhs) ? "(#{rhs.source})" : rhs.source
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
column_qualifier = Regexp.last_match(1)
|
|
156
|
+
return if column_qualifier.count('.') > 1
|
|
157
|
+
|
|
158
|
+
[column_qualifier, "#{lhs_source}#{operator}#{rhs_source}"] if operator
|
|
159
|
+
end
|
|
160
|
+
# rubocop:enable Metrics
|
|
161
|
+
|
|
162
|
+
def range_operator(comparison_operator)
|
|
163
|
+
comparison_operator == '<' ? '...' : '..'
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def find_pair(hash_node, value)
|
|
167
|
+
hash_node.pairs.find { |pair| pair.key.value.to_sym == value.to_sym }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def offense_range(node)
|
|
171
|
+
range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_good_method(method_name, column, value)
|
|
175
|
+
if column.include?('.')
|
|
176
|
+
table, column = column.split('.')
|
|
177
|
+
|
|
178
|
+
"#{method_name}(#{table}: { #{column}: #{value} })"
|
|
179
|
+
else
|
|
180
|
+
"#{method_name}(#{column}: #{value})"
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def parentheses_needed?(node)
|
|
185
|
+
!parentheses_not_needed?(node)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def parentheses_not_needed?(node)
|
|
189
|
+
node.variable? ||
|
|
190
|
+
node.literal? ||
|
|
191
|
+
node.reference? ||
|
|
192
|
+
node.const_type? ||
|
|
193
|
+
node.begin_type? ||
|
|
194
|
+
parenthesized_call_node?(node)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def parenthesized_call_node?(node)
|
|
198
|
+
node.call_type? && (node.arguments.empty? || node.parenthesized_call?)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|