rubocop-performance 0.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 +1 -0
- data/lib/rubocop-performance.rb +5 -0
- data/lib/rubocop/cop/performance/caller.rb +69 -0
- data/lib/rubocop/cop/performance/case_when_splat.rb +173 -0
- data/lib/rubocop/cop/performance/casecmp.rb +117 -0
- data/lib/rubocop/cop/performance/compare_with_block.rb +119 -0
- data/lib/rubocop/cop/performance/count.rb +102 -0
- data/lib/rubocop/cop/performance/detect.rb +110 -0
- data/lib/rubocop/cop/performance/double_start_end_with.rb +94 -0
- data/lib/rubocop/cop/performance/end_with.rb +56 -0
- data/lib/rubocop/cop/performance/fixed_size.rb +97 -0
- data/lib/rubocop/cop/performance/flat_map.rb +78 -0
- data/lib/rubocop/cop/performance/inefficient_hash_search.rb +99 -0
- data/lib/rubocop/cop/performance/lstrip_rstrip.rb +46 -0
- data/lib/rubocop/cop/performance/range_include.rb +47 -0
- data/lib/rubocop/cop/performance/redundant_block_call.rb +93 -0
- data/lib/rubocop/cop/performance/redundant_match.rb +56 -0
- data/lib/rubocop/cop/performance/redundant_merge.rb +169 -0
- data/lib/rubocop/cop/performance/redundant_sort_by.rb +50 -0
- data/lib/rubocop/cop/performance/regexp_match.rb +264 -0
- data/lib/rubocop/cop/performance/reverse_each.rb +42 -0
- data/lib/rubocop/cop/performance/sample.rb +142 -0
- data/lib/rubocop/cop/performance/size.rb +71 -0
- data/lib/rubocop/cop/performance/start_with.rb +59 -0
- data/lib/rubocop/cop/performance/string_replacement.rb +172 -0
- data/lib/rubocop/cop/performance/times_map.rb +71 -0
- data/lib/rubocop/cop/performance/unfreeze_string.rb +50 -0
- data/lib/rubocop/cop/performance/unneeded_sort.rb +165 -0
- data/lib/rubocop/cop/performance/uri_default_parser.rb +47 -0
- data/lib/rubocop/cop/performance_cops.rb +37 -0
- data/lib/rubocop/performance/version.rb +9 -0
- metadata +114 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop is used to identify usages of `count` on an
|
7
|
+
# `Array` and `Hash` and change them to `size`.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# [1, 2, 3].count
|
12
|
+
#
|
13
|
+
# # bad
|
14
|
+
# {a: 1, b: 2, c: 3}.count
|
15
|
+
#
|
16
|
+
# # good
|
17
|
+
# [1, 2, 3].size
|
18
|
+
#
|
19
|
+
# # good
|
20
|
+
# {a: 1, b: 2, c: 3}.size
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# [1, 2, 3].count { |e| e > 2 }
|
24
|
+
# TODO: Add advanced detection of variables that could
|
25
|
+
# have been assigned to an array or a hash.
|
26
|
+
class Size < Cop
|
27
|
+
MSG = 'Use `size` instead of `count`.'.freeze
|
28
|
+
|
29
|
+
def on_send(node)
|
30
|
+
return unless eligible_node?(node)
|
31
|
+
|
32
|
+
add_offense(node, location: :selector)
|
33
|
+
end
|
34
|
+
|
35
|
+
def autocorrect(node)
|
36
|
+
->(corrector) { corrector.replace(node.loc.selector, 'size') }
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def eligible_node?(node)
|
42
|
+
return false unless node.method?(:count) && !node.arguments?
|
43
|
+
|
44
|
+
eligible_receiver?(node.receiver) && !allowed_parent?(node.parent)
|
45
|
+
end
|
46
|
+
|
47
|
+
def eligible_receiver?(node)
|
48
|
+
return false unless node
|
49
|
+
|
50
|
+
array?(node) || hash?(node)
|
51
|
+
end
|
52
|
+
|
53
|
+
def allowed_parent?(node)
|
54
|
+
node && node.block_type?
|
55
|
+
end
|
56
|
+
|
57
|
+
def array?(node)
|
58
|
+
_, constant = *node.receiver
|
59
|
+
|
60
|
+
node.array_type? || constant == :Array || node.method_name == :to_a
|
61
|
+
end
|
62
|
+
|
63
|
+
def hash?(node)
|
64
|
+
_, constant = *node.receiver
|
65
|
+
|
66
|
+
node.hash_type? || constant == :Hash || node.method_name == :to_h
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop identifies unnecessary use of a regex where
|
7
|
+
# `String#start_with?` would suffice.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# 'abc'.match?(/\Aab/)
|
12
|
+
# 'abc' =~ /\Aab/
|
13
|
+
# 'abc'.match(/\Aab/)
|
14
|
+
#
|
15
|
+
# # good
|
16
|
+
# 'abc'.start_with?('ab')
|
17
|
+
class StartWith < Cop
|
18
|
+
MSG = 'Use `String#start_with?` instead of a regex match anchored to ' \
|
19
|
+
'the beginning of the string.'.freeze
|
20
|
+
SINGLE_QUOTE = "'".freeze
|
21
|
+
|
22
|
+
def_node_matcher :redundant_regex?, <<-PATTERN
|
23
|
+
{(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_start?) (regopt)))
|
24
|
+
(send (regexp (str $#literal_at_start?) (regopt)) {:match :=~} $_)}
|
25
|
+
PATTERN
|
26
|
+
|
27
|
+
def literal_at_start?(regex_str)
|
28
|
+
# is this regexp 'literal' in the sense of only matching literal
|
29
|
+
# chars, rather than using metachars like . and * and so on?
|
30
|
+
# also, is it anchored at the start of the string?
|
31
|
+
# (tricky: \s, \d, and so on are metacharacters, but other characters
|
32
|
+
# escaped with a slash are just literals. LITERAL_REGEX takes all
|
33
|
+
# that into account.)
|
34
|
+
regex_str =~ /\A\\A(?:#{LITERAL_REGEX})+\z/
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_send(node)
|
38
|
+
return unless redundant_regex?(node)
|
39
|
+
|
40
|
+
add_offense(node)
|
41
|
+
end
|
42
|
+
|
43
|
+
def autocorrect(node)
|
44
|
+
redundant_regex?(node) do |receiver, regex_str|
|
45
|
+
receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
|
46
|
+
regex_str = regex_str[2..-1] # drop \A anchor
|
47
|
+
regex_str = interpret_string_escapes(regex_str)
|
48
|
+
|
49
|
+
lambda do |corrector|
|
50
|
+
new_source = receiver.source + '.start_with?(' +
|
51
|
+
to_string_literal(regex_str) + ')'
|
52
|
+
corrector.replace(node.source_range, new_source)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop identifies places where `gsub` can be replaced by
|
7
|
+
# `tr` or `delete`.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# 'abc'.gsub('b', 'd')
|
12
|
+
# 'abc'.gsub('a', '')
|
13
|
+
# 'abc'.gsub(/a/, 'd')
|
14
|
+
# 'abc'.gsub!('a', 'd')
|
15
|
+
#
|
16
|
+
# # good
|
17
|
+
# 'abc'.gsub(/.*/, 'a')
|
18
|
+
# 'abc'.gsub(/a+/, 'd')
|
19
|
+
# 'abc'.tr('b', 'd')
|
20
|
+
# 'a b c'.delete(' ')
|
21
|
+
class StringReplacement < Cop
|
22
|
+
include RangeHelp
|
23
|
+
|
24
|
+
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'.freeze
|
25
|
+
DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/
|
26
|
+
DELETE = 'delete'.freeze
|
27
|
+
TR = 'tr'.freeze
|
28
|
+
BANG = '!'.freeze
|
29
|
+
SINGLE_QUOTE = "'".freeze
|
30
|
+
|
31
|
+
def_node_matcher :string_replacement?, <<-PATTERN
|
32
|
+
(send _ {:gsub :gsub!}
|
33
|
+
${regexp str (send (const nil? :Regexp) {:new :compile} _)}
|
34
|
+
$str)
|
35
|
+
PATTERN
|
36
|
+
|
37
|
+
def on_send(node)
|
38
|
+
string_replacement?(node) do |first_param, second_param|
|
39
|
+
return if accept_second_param?(second_param)
|
40
|
+
return if accept_first_param?(first_param)
|
41
|
+
|
42
|
+
offense(node, first_param, second_param)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def autocorrect(node)
|
47
|
+
_string, _method, first_param, second_param = *node
|
48
|
+
first_source, = first_source(first_param)
|
49
|
+
second_source, = *second_param
|
50
|
+
|
51
|
+
unless first_param.str_type?
|
52
|
+
first_source = interpret_string_escapes(first_source)
|
53
|
+
end
|
54
|
+
|
55
|
+
replacement_method =
|
56
|
+
replacement_method(node, first_source, second_source)
|
57
|
+
|
58
|
+
replace_method(node, first_source, second_source, first_param,
|
59
|
+
replacement_method)
|
60
|
+
end
|
61
|
+
|
62
|
+
def replace_method(node, first, second, first_param, replacement)
|
63
|
+
lambda do |corrector|
|
64
|
+
corrector.replace(node.loc.selector, replacement)
|
65
|
+
unless first_param.str_type?
|
66
|
+
corrector.replace(first_param.source_range,
|
67
|
+
to_string_literal(first))
|
68
|
+
end
|
69
|
+
|
70
|
+
if second.empty? && first.length == 1
|
71
|
+
remove_second_param(corrector, node, first_param)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def accept_second_param?(second_param)
|
79
|
+
second_source, = *second_param
|
80
|
+
second_source.length > 1
|
81
|
+
end
|
82
|
+
|
83
|
+
def accept_first_param?(first_param)
|
84
|
+
first_source, options = first_source(first_param)
|
85
|
+
return true if first_source.nil?
|
86
|
+
|
87
|
+
unless first_param.str_type?
|
88
|
+
return true if options
|
89
|
+
return true unless first_source =~ DETERMINISTIC_REGEX
|
90
|
+
# This must be done after checking DETERMINISTIC_REGEX
|
91
|
+
# Otherwise things like \s will trip us up
|
92
|
+
first_source = interpret_string_escapes(first_source)
|
93
|
+
end
|
94
|
+
|
95
|
+
first_source.length != 1
|
96
|
+
end
|
97
|
+
|
98
|
+
def offense(node, first_param, second_param)
|
99
|
+
first_source, = first_source(first_param)
|
100
|
+
unless first_param.str_type?
|
101
|
+
first_source = interpret_string_escapes(first_source)
|
102
|
+
end
|
103
|
+
second_source, = *second_param
|
104
|
+
message = message(node, first_source, second_source)
|
105
|
+
|
106
|
+
add_offense(node, location: range(node), message: message)
|
107
|
+
end
|
108
|
+
|
109
|
+
def first_source(first_param)
|
110
|
+
case first_param.type
|
111
|
+
when :regexp
|
112
|
+
source_from_regex_literal(first_param)
|
113
|
+
when :send
|
114
|
+
source_from_regex_constructor(first_param)
|
115
|
+
when :str
|
116
|
+
first_param.children.first
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def source_from_regex_literal(node)
|
121
|
+
regex, options = *node
|
122
|
+
source, = *regex
|
123
|
+
options, = *options
|
124
|
+
[source, options]
|
125
|
+
end
|
126
|
+
|
127
|
+
def source_from_regex_constructor(node)
|
128
|
+
_const, _init, regex = *node
|
129
|
+
case regex.type
|
130
|
+
when :regexp
|
131
|
+
source_from_regex_literal(regex)
|
132
|
+
when :str
|
133
|
+
source, = *regex
|
134
|
+
source
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def range(node)
|
139
|
+
range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
|
140
|
+
end
|
141
|
+
|
142
|
+
def replacement_method(node, first_source, second_source)
|
143
|
+
replacement = if second_source.empty? && first_source.length == 1
|
144
|
+
DELETE
|
145
|
+
else
|
146
|
+
TR
|
147
|
+
end
|
148
|
+
|
149
|
+
"#{replacement}#{BANG if node.bang_method?}"
|
150
|
+
end
|
151
|
+
|
152
|
+
def message(node, first_source, second_source)
|
153
|
+
replacement_method =
|
154
|
+
replacement_method(node, first_source, second_source)
|
155
|
+
|
156
|
+
format(MSG, prefer: replacement_method, current: node.method_name)
|
157
|
+
end
|
158
|
+
|
159
|
+
def method_suffix(node)
|
160
|
+
node.loc.end ? node.loc.end.source : ''
|
161
|
+
end
|
162
|
+
|
163
|
+
def remove_second_param(corrector, node, first_param)
|
164
|
+
end_range = range_between(first_param.source_range.end_pos,
|
165
|
+
node.source_range.end_pos)
|
166
|
+
|
167
|
+
corrector.replace(end_range, method_suffix(node))
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop checks for .times.map calls.
|
7
|
+
# In most cases such calls can be replaced
|
8
|
+
# with an explicit array creation.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# 9.times.map do |i|
|
13
|
+
# i.to_s
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# # good
|
17
|
+
# Array.new(9) do |i|
|
18
|
+
# i.to_s
|
19
|
+
# end
|
20
|
+
class TimesMap < Cop
|
21
|
+
MESSAGE = 'Use `Array.new(%<count>s)` with a block ' \
|
22
|
+
'instead of `.times.%<map_or_collect>s`'.freeze
|
23
|
+
MESSAGE_ONLY_IF = 'only if `%<count>s` is always 0 or more'.freeze
|
24
|
+
|
25
|
+
def on_send(node)
|
26
|
+
check(node)
|
27
|
+
end
|
28
|
+
|
29
|
+
def on_block(node)
|
30
|
+
check(node)
|
31
|
+
end
|
32
|
+
|
33
|
+
def autocorrect(node)
|
34
|
+
map_or_collect, count = times_map_call(node)
|
35
|
+
|
36
|
+
replacement =
|
37
|
+
"Array.new(#{count.source}" \
|
38
|
+
"#{map_or_collect.arguments.map { |arg| ", #{arg.source}" }.join})"
|
39
|
+
|
40
|
+
lambda do |corrector|
|
41
|
+
corrector.replace(map_or_collect.loc.expression, replacement)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def check(node)
|
48
|
+
times_map_call(node) do |map_or_collect, count|
|
49
|
+
add_offense(node, message: message(map_or_collect, count))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def message(map_or_collect, count)
|
54
|
+
template = if count.literal?
|
55
|
+
MESSAGE + '.'
|
56
|
+
else
|
57
|
+
"#{MESSAGE} #{MESSAGE_ONLY_IF}."
|
58
|
+
end
|
59
|
+
format(template,
|
60
|
+
count: count.source,
|
61
|
+
map_or_collect: map_or_collect.method_name)
|
62
|
+
end
|
63
|
+
|
64
|
+
def_node_matcher :times_map_call, <<-PATTERN
|
65
|
+
{(block $(send (send $!nil? :times) {:map :collect}) ...)
|
66
|
+
$(send (send $!nil? :times) {:map :collect} (block_pass ...))}
|
67
|
+
PATTERN
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# In Ruby 2.3 or later, use unary plus operator to unfreeze a string
|
7
|
+
# literal instead of `String#dup` and `String.new`.
|
8
|
+
# Unary plus operator is faster than `String#dup`.
|
9
|
+
#
|
10
|
+
# Note: `String.new` (without operator) is not exactly the same as `+''`.
|
11
|
+
# These differ in encoding. `String.new.encoding` is always `ASCII-8BIT`.
|
12
|
+
# However, `(+'').encoding` is the same as script encoding(e.g. `UTF-8`).
|
13
|
+
# So, if you expect `ASCII-8BIT` encoding, disable this cop.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # bad
|
17
|
+
# ''.dup
|
18
|
+
# "something".dup
|
19
|
+
# String.new
|
20
|
+
# String.new('')
|
21
|
+
# String.new('something')
|
22
|
+
#
|
23
|
+
# # good
|
24
|
+
# +'something'
|
25
|
+
# +''
|
26
|
+
class UnfreezeString < Cop
|
27
|
+
extend TargetRubyVersion
|
28
|
+
|
29
|
+
minimum_target_ruby_version 2.3
|
30
|
+
|
31
|
+
MSG = 'Use unary plus to get an unfrozen string literal.'.freeze
|
32
|
+
|
33
|
+
def_node_matcher :dup_string?, <<-PATTERN
|
34
|
+
(send {str dstr} :dup)
|
35
|
+
PATTERN
|
36
|
+
|
37
|
+
def_node_matcher :string_new?, <<-PATTERN
|
38
|
+
{
|
39
|
+
(send (const nil? :String) :new {str dstr})
|
40
|
+
(send (const nil? :String) :new)
|
41
|
+
}
|
42
|
+
PATTERN
|
43
|
+
|
44
|
+
def on_send(node)
|
45
|
+
add_offense(node) if dup_string?(node) || string_new?(node)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop is used to identify instances of sorting and then
|
7
|
+
# taking only the first or last element. The same behavior can
|
8
|
+
# be accomplished without a relatively expensive sort by using
|
9
|
+
# `Enumerable#min` instead of sorting and taking the first
|
10
|
+
# element and `Enumerable#max` instead of sorting and taking the
|
11
|
+
# last element. Similarly, `Enumerable#min_by` and
|
12
|
+
# `Enumerable#max_by` can replace `Enumerable#sort_by` calls
|
13
|
+
# after which only the first or last element is used.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # bad
|
17
|
+
# [2, 1, 3].sort.first
|
18
|
+
# [2, 1, 3].sort[0]
|
19
|
+
# [2, 1, 3].sort.at(0)
|
20
|
+
# [2, 1, 3].sort.slice(0)
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# [2, 1, 3].min
|
24
|
+
#
|
25
|
+
# # bad
|
26
|
+
# [2, 1, 3].sort.last
|
27
|
+
# [2, 1, 3].sort[-1]
|
28
|
+
# [2, 1, 3].sort.at(-1)
|
29
|
+
# [2, 1, 3].sort.slice(-1)
|
30
|
+
#
|
31
|
+
# # good
|
32
|
+
# [2, 1, 3].max
|
33
|
+
#
|
34
|
+
# # bad
|
35
|
+
# arr.sort_by(&:foo).first
|
36
|
+
# arr.sort_by(&:foo)[0]
|
37
|
+
# arr.sort_by(&:foo).at(0)
|
38
|
+
# arr.sort_by(&:foo).slice(0)
|
39
|
+
#
|
40
|
+
# # good
|
41
|
+
# arr.min_by(&:foo)
|
42
|
+
#
|
43
|
+
# # bad
|
44
|
+
# arr.sort_by(&:foo).last
|
45
|
+
# arr.sort_by(&:foo)[-1]
|
46
|
+
# arr.sort_by(&:foo).at(-1)
|
47
|
+
# arr.sort_by(&:foo).slice(-1)
|
48
|
+
#
|
49
|
+
# # good
|
50
|
+
# arr.max_by(&:foo)
|
51
|
+
#
|
52
|
+
class UnneededSort < Cop
|
53
|
+
include RangeHelp
|
54
|
+
|
55
|
+
MSG = 'Use `%<suggestion>s` instead of '\
|
56
|
+
'`%<sorter>s...%<accessor_source>s`.'.freeze
|
57
|
+
|
58
|
+
def_node_matcher :unneeded_sort?, <<-MATCHER
|
59
|
+
{
|
60
|
+
(send $(send _ $:sort ...) ${:last :first})
|
61
|
+
(send $(send _ $:sort ...) ${:[] :at :slice} {(int 0) (int -1)})
|
62
|
+
|
63
|
+
(send $(send _ $:sort_by _) ${:last :first})
|
64
|
+
(send $(send _ $:sort_by _) ${:[] :at :slice} {(int 0) (int -1)})
|
65
|
+
|
66
|
+
(send (block $(send _ ${:sort_by :sort}) ...) ${:last :first})
|
67
|
+
(send
|
68
|
+
(block $(send _ ${:sort_by :sort}) ...)
|
69
|
+
${:[] :at :slice} {(int 0) (int -1)}
|
70
|
+
)
|
71
|
+
}
|
72
|
+
MATCHER
|
73
|
+
|
74
|
+
def on_send(node)
|
75
|
+
unneeded_sort?(node) do |sort_node, sorter, accessor|
|
76
|
+
range = range_between(
|
77
|
+
sort_node.loc.selector.begin_pos,
|
78
|
+
node.loc.expression.end_pos
|
79
|
+
)
|
80
|
+
|
81
|
+
add_offense(node,
|
82
|
+
location: range,
|
83
|
+
message: message(node,
|
84
|
+
sorter,
|
85
|
+
accessor))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def autocorrect(node)
|
90
|
+
sort_node, sorter, accessor = unneeded_sort?(node)
|
91
|
+
|
92
|
+
lambda do |corrector|
|
93
|
+
# Remove accessor, e.g. `first` or `[-1]`.
|
94
|
+
corrector.remove(
|
95
|
+
range_between(
|
96
|
+
accessor_start(node),
|
97
|
+
node.loc.expression.end_pos
|
98
|
+
)
|
99
|
+
)
|
100
|
+
|
101
|
+
# Replace "sort" or "sort_by" with the appropriate min/max method.
|
102
|
+
corrector.replace(
|
103
|
+
sort_node.loc.selector,
|
104
|
+
suggestion(sorter, accessor, arg_value(node))
|
105
|
+
)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def message(node, sorter, accessor)
|
112
|
+
accessor_source = range_between(
|
113
|
+
node.loc.selector.begin_pos,
|
114
|
+
node.loc.expression.end_pos
|
115
|
+
).source
|
116
|
+
|
117
|
+
format(MSG,
|
118
|
+
suggestion: suggestion(sorter,
|
119
|
+
accessor,
|
120
|
+
arg_value(node)),
|
121
|
+
sorter: sorter,
|
122
|
+
accessor_source: accessor_source)
|
123
|
+
end
|
124
|
+
|
125
|
+
def suggestion(sorter, accessor, arg)
|
126
|
+
base(accessor, arg) + suffix(sorter)
|
127
|
+
end
|
128
|
+
|
129
|
+
def base(accessor, arg)
|
130
|
+
if accessor == :first || (arg && arg.zero?)
|
131
|
+
'min'
|
132
|
+
elsif accessor == :last || arg == -1
|
133
|
+
'max'
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def suffix(sorter)
|
138
|
+
if sorter == :sort
|
139
|
+
''
|
140
|
+
elsif sorter == :sort_by
|
141
|
+
'_by'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def arg_node(node)
|
146
|
+
node.arguments.first
|
147
|
+
end
|
148
|
+
|
149
|
+
def arg_value(node)
|
150
|
+
arg_node(node).nil? ? nil : arg_node(node).node_parts.first
|
151
|
+
end
|
152
|
+
|
153
|
+
# This gets the start of the accessor whether it has a dot
|
154
|
+
# (e.g. `.first`) or doesn't (e.g. `[0]`)
|
155
|
+
def accessor_start(node)
|
156
|
+
if node.loc.dot
|
157
|
+
node.loc.dot.begin_pos
|
158
|
+
else
|
159
|
+
node.loc.selector.begin_pos
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|