rubocop-performance 1.6.1 → 1.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 +8 -0
- data/config/default.yml +95 -8
- data/lib/rubocop/cop/mixin/regexp_metacharacter.rb +4 -4
- data/lib/rubocop/cop/mixin/sort_block.rb +28 -0
- data/lib/rubocop/cop/performance/ancestors_include.rb +49 -0
- data/lib/rubocop/cop/performance/array_semi_infinite_range_slice.rb +74 -0
- data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +46 -0
- data/lib/rubocop/cop/performance/bind_call.rb +9 -18
- data/lib/rubocop/cop/performance/block_given_with_explicit_block.rb +52 -0
- data/lib/rubocop/cop/performance/caller.rb +14 -15
- data/lib/rubocop/cop/performance/case_when_splat.rb +18 -11
- data/lib/rubocop/cop/performance/casecmp.rb +13 -20
- data/lib/rubocop/cop/performance/chain_array_allocation.rb +4 -10
- data/lib/rubocop/cop/performance/collection_literal_in_loop.rb +140 -0
- data/lib/rubocop/cop/performance/compare_with_block.rb +10 -21
- data/lib/rubocop/cop/performance/constant_regexp.rb +68 -0
- data/lib/rubocop/cop/performance/count.rb +14 -16
- data/lib/rubocop/cop/performance/delete_prefix.rb +14 -22
- data/lib/rubocop/cop/performance/delete_suffix.rb +14 -22
- data/lib/rubocop/cop/performance/detect.rb +65 -32
- data/lib/rubocop/cop/performance/double_start_end_with.rb +16 -24
- data/lib/rubocop/cop/performance/end_with.rb +9 -13
- data/lib/rubocop/cop/performance/fixed_size.rb +2 -1
- data/lib/rubocop/cop/performance/flat_map.rb +21 -22
- data/lib/rubocop/cop/performance/inefficient_hash_search.rb +15 -14
- data/lib/rubocop/cop/performance/io_readlines.rb +112 -0
- data/lib/rubocop/cop/performance/method_object_as_block.rb +32 -0
- data/lib/rubocop/cop/performance/open_struct.rb +3 -2
- data/lib/rubocop/cop/performance/range_include.rb +15 -11
- data/lib/rubocop/cop/performance/redundant_block_call.rb +15 -10
- data/lib/rubocop/cop/performance/redundant_match.rb +12 -6
- data/lib/rubocop/cop/performance/redundant_merge.rb +19 -17
- data/lib/rubocop/cop/performance/redundant_sort_block.rb +43 -0
- data/lib/rubocop/cop/performance/redundant_string_chars.rb +129 -0
- data/lib/rubocop/cop/performance/regexp_match.rb +20 -20
- data/lib/rubocop/cop/performance/reverse_each.rb +10 -5
- data/lib/rubocop/cop/performance/reverse_first.rb +73 -0
- data/lib/rubocop/cop/performance/size.rb +42 -43
- data/lib/rubocop/cop/performance/sort_reverse.rb +45 -0
- data/lib/rubocop/cop/performance/squeeze.rb +67 -0
- data/lib/rubocop/cop/performance/start_with.rb +9 -13
- data/lib/rubocop/cop/performance/string_include.rb +56 -0
- data/lib/rubocop/cop/performance/string_replacement.rb +24 -27
- data/lib/rubocop/cop/performance/sum.rb +236 -0
- data/lib/rubocop/cop/performance/times_map.rb +12 -18
- data/lib/rubocop/cop/performance/unfreeze_string.rb +20 -2
- data/lib/rubocop/cop/performance/uri_default_parser.rb +7 -12
- data/lib/rubocop/cop/performance_cops.rb +16 -0
- data/lib/rubocop/performance/version.rb +6 -1
- metadata +35 -13
@@ -9,67 +9,66 @@ module RuboCop
|
|
9
9
|
# @example
|
10
10
|
# # bad
|
11
11
|
# [1, 2, 3].count
|
12
|
+
# (1..3).to_a.count
|
13
|
+
# Array[*1..3].count
|
14
|
+
# Array(1..3).count
|
12
15
|
#
|
13
16
|
# # bad
|
14
17
|
# {a: 1, b: 2, c: 3}.count
|
18
|
+
# [[:foo, :bar], [1, 2]].to_h.count
|
19
|
+
# Hash[*('a'..'z')].count
|
20
|
+
# Hash(key: :value).count
|
15
21
|
#
|
16
22
|
# # good
|
17
23
|
# [1, 2, 3].size
|
24
|
+
# (1..3).to_a.size
|
25
|
+
# Array[*1..3].size
|
26
|
+
# Array(1..3).size
|
18
27
|
#
|
19
28
|
# # good
|
20
29
|
# {a: 1, b: 2, c: 3}.size
|
30
|
+
# [[:foo, :bar], [1, 2]].to_h.size
|
31
|
+
# Hash[*('a'..'z')].size
|
32
|
+
# Hash(key: :value).size
|
21
33
|
#
|
22
34
|
# # good
|
23
35
|
# [1, 2, 3].count { |e| e > 2 }
|
24
36
|
# TODO: Add advanced detection of variables that could
|
25
37
|
# have been assigned to an array or a hash.
|
26
|
-
class Size <
|
38
|
+
class Size < Base
|
39
|
+
extend AutoCorrector
|
40
|
+
|
27
41
|
MSG = 'Use `size` instead of `count`.'
|
42
|
+
RESTRICT_ON_SEND = %i[count].freeze
|
43
|
+
|
44
|
+
def_node_matcher :array?, <<~PATTERN
|
45
|
+
{
|
46
|
+
[!nil? array_type?]
|
47
|
+
(send _ :to_a)
|
48
|
+
(send (const nil? :Array) :[] _)
|
49
|
+
(send nil? :Array _)
|
50
|
+
}
|
51
|
+
PATTERN
|
52
|
+
|
53
|
+
def_node_matcher :hash?, <<~PATTERN
|
54
|
+
{
|
55
|
+
[!nil? hash_type?]
|
56
|
+
(send _ :to_h)
|
57
|
+
(send (const nil? :Hash) :[] _)
|
58
|
+
(send nil? :Hash _)
|
59
|
+
}
|
60
|
+
PATTERN
|
61
|
+
|
62
|
+
def_node_matcher :count?, <<~PATTERN
|
63
|
+
(send {#array? #hash?} :count)
|
64
|
+
PATTERN
|
28
65
|
|
29
66
|
def on_send(node)
|
30
|
-
return
|
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&.block_type?
|
55
|
-
end
|
56
|
-
|
57
|
-
def array?(node)
|
58
|
-
return true if node.array_type?
|
59
|
-
return false unless node.send_type?
|
60
|
-
|
61
|
-
_, constant = *node.receiver
|
62
|
-
|
63
|
-
constant == :Array || node.method?(:to_a)
|
64
|
-
end
|
65
|
-
|
66
|
-
def hash?(node)
|
67
|
-
return true if node.hash_type?
|
68
|
-
return false unless node.send_type?
|
69
|
-
|
70
|
-
_, constant = *node.receiver
|
67
|
+
return if node.parent&.block_type? || !count?(node)
|
71
68
|
|
72
|
-
|
69
|
+
add_offense(node.loc.selector) do |corrector|
|
70
|
+
corrector.replace(node.loc.selector, 'size')
|
71
|
+
end
|
73
72
|
end
|
74
73
|
end
|
75
74
|
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop identifies places where `sort { |a, b| b <=> a }`
|
7
|
+
# can be replaced by a faster `sort.reverse`.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# array.sort { |a, b| b <=> a }
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
# array.sort.reverse
|
15
|
+
#
|
16
|
+
class SortReverse < Base
|
17
|
+
include SortBlock
|
18
|
+
extend AutoCorrector
|
19
|
+
|
20
|
+
MSG = 'Use `sort.reverse` instead of `%<bad_method>s`.'
|
21
|
+
|
22
|
+
def on_block(node)
|
23
|
+
sort_with_block?(node) do |send, var_a, var_b, body|
|
24
|
+
replaceable_body?(body, var_b, var_a) do
|
25
|
+
range = sort_range(send, node)
|
26
|
+
|
27
|
+
add_offense(range, message: message(var_a, var_b)) do |corrector|
|
28
|
+
replacement = 'sort.reverse'
|
29
|
+
|
30
|
+
corrector.replace(range, replacement)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def message(var_a, var_b)
|
39
|
+
bad_method = "sort { |#{var_a}, #{var_b}| #{var_b} <=> #{var_a} }"
|
40
|
+
format(MSG, bad_method: bad_method)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop identifies places where `gsub(/a+/, 'a')` and `gsub!(/a+/, 'a')`
|
7
|
+
# can be replaced by `squeeze('a')` and `squeeze!('a')`.
|
8
|
+
#
|
9
|
+
# The `squeeze('a')` method is faster than `gsub(/a+/, 'a')`.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# # bad
|
14
|
+
# str.gsub(/a+/, 'a')
|
15
|
+
# str.gsub!(/a+/, 'a')
|
16
|
+
#
|
17
|
+
# # good
|
18
|
+
# str.squeeze('a')
|
19
|
+
# str.squeeze!('a')
|
20
|
+
#
|
21
|
+
class Squeeze < Base
|
22
|
+
extend AutoCorrector
|
23
|
+
|
24
|
+
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
|
25
|
+
RESTRICT_ON_SEND = %i[gsub gsub!].freeze
|
26
|
+
|
27
|
+
PREFERRED_METHODS = {
|
28
|
+
gsub: :squeeze,
|
29
|
+
gsub!: :squeeze!
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
def_node_matcher :squeeze_candidate?, <<~PATTERN
|
33
|
+
(send
|
34
|
+
$!nil? ${:gsub :gsub!}
|
35
|
+
(regexp
|
36
|
+
(str $#repeating_literal?)
|
37
|
+
(regopt))
|
38
|
+
(str $_))
|
39
|
+
PATTERN
|
40
|
+
|
41
|
+
def on_send(node)
|
42
|
+
squeeze_candidate?(node) do |receiver, bad_method, regexp_str, replace_str|
|
43
|
+
regexp_str = regexp_str[0..-2] # delete '+' from the end
|
44
|
+
regexp_str = interpret_string_escapes(regexp_str)
|
45
|
+
return unless replace_str == regexp_str
|
46
|
+
|
47
|
+
good_method = PREFERRED_METHODS[bad_method]
|
48
|
+
message = format(MSG, current: bad_method, prefer: good_method)
|
49
|
+
|
50
|
+
add_offense(node.loc.selector, message: message) do |corrector|
|
51
|
+
string_literal = to_string_literal(replace_str)
|
52
|
+
new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
|
53
|
+
|
54
|
+
corrector.replace(node.source_range, new_code)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def repeating_literal?(regex_str)
|
62
|
+
regex_str.match?(/\A(?:#{Util::LITERAL_REGEX})\+\z/o)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -41,11 +41,13 @@ module RuboCop
|
|
41
41
|
# 'abc'.match(/^ab/)
|
42
42
|
# /^ab/.match('abc')
|
43
43
|
#
|
44
|
-
class StartWith <
|
44
|
+
class StartWith < Base
|
45
45
|
include RegexpMetacharacter
|
46
|
+
extend AutoCorrector
|
46
47
|
|
47
48
|
MSG = 'Use `String#start_with?` instead of a regex match anchored to ' \
|
48
49
|
'the beginning of the string.'
|
50
|
+
RESTRICT_ON_SEND = %i[match =~ match?].freeze
|
49
51
|
|
50
52
|
def_node_matcher :redundant_regex?, <<~PATTERN
|
51
53
|
{(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_start?) (regopt)))
|
@@ -54,25 +56,19 @@ module RuboCop
|
|
54
56
|
PATTERN
|
55
57
|
|
56
58
|
def on_send(node)
|
57
|
-
return unless redundant_regex?(node)
|
59
|
+
return unless (receiver, regex_str = redundant_regex?(node))
|
58
60
|
|
59
|
-
add_offense(node)
|
60
|
-
end
|
61
|
-
alias on_match_with_lvasgn on_send
|
62
|
-
|
63
|
-
def autocorrect(node)
|
64
|
-
redundant_regex?(node) do |receiver, regex_str|
|
61
|
+
add_offense(node) do |corrector|
|
65
62
|
receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
|
66
63
|
regex_str = drop_start_metacharacter(regex_str)
|
67
64
|
regex_str = interpret_string_escapes(regex_str)
|
68
65
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
corrector.replace(node.source_range, new_source)
|
73
|
-
end
|
66
|
+
new_source = "#{receiver.source}.start_with?(#{to_string_literal(regex_str)})"
|
67
|
+
|
68
|
+
corrector.replace(node.source_range, new_source)
|
74
69
|
end
|
75
70
|
end
|
71
|
+
alias on_match_with_lvasgn on_send
|
76
72
|
end
|
77
73
|
end
|
78
74
|
end
|
@@ -0,0 +1,56 @@
|
|
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#include?` would suffice.
|
8
|
+
#
|
9
|
+
# This cop's offenses are not safe to auto-correct if a receiver is nil.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad
|
13
|
+
# 'abc'.match?(/ab/)
|
14
|
+
# /ab/.match?('abc')
|
15
|
+
# 'abc' =~ /ab/
|
16
|
+
# /ab/ =~ 'abc'
|
17
|
+
# 'abc'.match(/ab/)
|
18
|
+
# /ab/.match('abc')
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# 'abc'.include?('ab')
|
22
|
+
class StringInclude < Base
|
23
|
+
extend AutoCorrector
|
24
|
+
|
25
|
+
MSG = 'Use `String#include?` instead of a regex match with literal-only pattern.'
|
26
|
+
RESTRICT_ON_SEND = %i[match =~ match?].freeze
|
27
|
+
|
28
|
+
def_node_matcher :redundant_regex?, <<~PATTERN
|
29
|
+
{(send $!nil? {:match :=~ :match?} (regexp (str $#literal?) (regopt)))
|
30
|
+
(send (regexp (str $#literal?) (regopt)) {:match :match?} $str)
|
31
|
+
(match-with-lvasgn (regexp (str $#literal?) (regopt)) $_)}
|
32
|
+
PATTERN
|
33
|
+
|
34
|
+
def on_send(node)
|
35
|
+
return unless (receiver, regex_str = redundant_regex?(node))
|
36
|
+
|
37
|
+
add_offense(node) do |corrector|
|
38
|
+
receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
|
39
|
+
regex_str = interpret_string_escapes(regex_str)
|
40
|
+
|
41
|
+
new_source = "#{receiver.source}.include?(#{to_string_literal(regex_str)})"
|
42
|
+
|
43
|
+
corrector.replace(node.source_range, new_source)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
alias on_match_with_lvasgn on_send
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def literal?(regex_str)
|
51
|
+
regex_str.match?(/\A#{Util::LITERAL_REGEX}+\z/o)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -18,10 +18,12 @@ module RuboCop
|
|
18
18
|
# 'abc'.gsub(/a+/, 'd')
|
19
19
|
# 'abc'.tr('b', 'd')
|
20
20
|
# 'a b c'.delete(' ')
|
21
|
-
class StringReplacement <
|
21
|
+
class StringReplacement < Base
|
22
22
|
include RangeHelp
|
23
|
+
extend AutoCorrector
|
23
24
|
|
24
25
|
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
|
26
|
+
RESTRICT_ON_SEND = %i[gsub gsub!].freeze
|
25
27
|
DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/.freeze
|
26
28
|
DELETE = 'delete'
|
27
29
|
TR = 'tr'
|
@@ -42,33 +44,37 @@ module RuboCop
|
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
45
|
-
|
47
|
+
private
|
48
|
+
|
49
|
+
def offense(node, first_param, second_param)
|
50
|
+
first_source, = first_source(first_param)
|
51
|
+
first_source = interpret_string_escapes(first_source) unless first_param.str_type?
|
52
|
+
second_source, = *second_param
|
53
|
+
message = message(node, first_source, second_source)
|
54
|
+
|
55
|
+
add_offense(range(node), message: message) do |corrector|
|
56
|
+
autocorrect(corrector, node)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def autocorrect(corrector, node)
|
46
61
|
_string, _method, first_param, second_param = *node
|
47
62
|
first_source, = first_source(first_param)
|
48
63
|
second_source, = *second_param
|
49
64
|
|
50
65
|
first_source = interpret_string_escapes(first_source) unless first_param.str_type?
|
51
66
|
|
52
|
-
|
53
|
-
replacement_method(node, first_source, second_source)
|
54
|
-
|
55
|
-
replace_method(node, first_source, second_source, first_param,
|
56
|
-
replacement_method)
|
67
|
+
replace_method(corrector, node, first_source, second_source, first_param)
|
57
68
|
end
|
58
69
|
|
59
|
-
def replace_method(node,
|
60
|
-
|
61
|
-
corrector.replace(node.loc.selector, replacement)
|
62
|
-
unless first_param.str_type?
|
63
|
-
corrector.replace(first_param.source_range,
|
64
|
-
to_string_literal(first))
|
65
|
-
end
|
70
|
+
def replace_method(corrector, node, first_source, second_source, first_param)
|
71
|
+
replacement_method = replacement_method(node, first_source, second_source)
|
66
72
|
|
67
|
-
|
68
|
-
|
69
|
-
end
|
73
|
+
corrector.replace(node.loc.selector, replacement_method)
|
74
|
+
corrector.replace(first_param.source_range, to_string_literal(first_source)) unless first_param.str_type?
|
70
75
|
|
71
|
-
|
76
|
+
remove_second_param(corrector, node, first_param) if second_source.empty? && first_source.length == 1
|
77
|
+
end
|
72
78
|
|
73
79
|
def accept_second_param?(second_param)
|
74
80
|
second_source, = *second_param
|
@@ -92,15 +98,6 @@ module RuboCop
|
|
92
98
|
first_source.length != 1
|
93
99
|
end
|
94
100
|
|
95
|
-
def offense(node, first_param, second_param)
|
96
|
-
first_source, = first_source(first_param)
|
97
|
-
first_source = interpret_string_escapes(first_source) unless first_param.str_type?
|
98
|
-
second_source, = *second_param
|
99
|
-
message = message(node, first_source, second_source)
|
100
|
-
|
101
|
-
add_offense(node, location: range(node), message: message)
|
102
|
-
end
|
103
|
-
|
104
101
|
def first_source(first_param)
|
105
102
|
case first_param.type
|
106
103
|
when :regexp
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop identifies places where custom code finding the sum of elements
|
7
|
+
# in some Enumerable object can be replaced by `Enumerable#sum` method.
|
8
|
+
#
|
9
|
+
# This cop can change auto-correction scope depending on the value of
|
10
|
+
# `SafeAutoCorrect`.
|
11
|
+
# Its auto-correction is marked as safe by default (`SafeAutoCorrect: true`)
|
12
|
+
# to prevent `TypeError` in auto-correced code when initial value is not
|
13
|
+
# specified as shown below:
|
14
|
+
#
|
15
|
+
# [source,ruby]
|
16
|
+
# ----
|
17
|
+
# ['a', 'b'].sum # => (String can't be coerced into Integer)
|
18
|
+
# ----
|
19
|
+
#
|
20
|
+
# Therefore if initial value is not specified, unsafe auto-corrected will not occur.
|
21
|
+
#
|
22
|
+
# If you always want to enable auto-correction, you can set `SafeAutoCorrect: false`.
|
23
|
+
#
|
24
|
+
# [source,yaml]
|
25
|
+
# ----
|
26
|
+
# Performance/Sum:
|
27
|
+
# SafeAutoCorrect: false
|
28
|
+
# ----
|
29
|
+
#
|
30
|
+
# Please note that the auto-correction command line option will be changed from
|
31
|
+
# `rubocop -a` to `rubocop -A`, which includes unsafe auto-correction.
|
32
|
+
#
|
33
|
+
# @example
|
34
|
+
# # bad
|
35
|
+
# [1, 2, 3].inject(:+) # These bad cases with no initial value are unsafe and
|
36
|
+
# [1, 2, 3].inject(&:+) # will not be auto-correced by default. If you want to
|
37
|
+
# [1, 2, 3].reduce { |acc, elem| acc + elem } # auto-corrected, you can set `SafeAutoCorrect: false`.
|
38
|
+
# [1, 2, 3].reduce(10, :+)
|
39
|
+
# [1, 2, 3].map { |elem| elem ** 2 }.sum
|
40
|
+
# [1, 2, 3].collect(&:count).sum(10)
|
41
|
+
#
|
42
|
+
# # good
|
43
|
+
# [1, 2, 3].sum
|
44
|
+
# [1, 2, 3].sum(10)
|
45
|
+
# [1, 2, 3].sum { |elem| elem ** 2 }
|
46
|
+
# [1, 2, 3].sum(10, &:count)
|
47
|
+
#
|
48
|
+
class Sum < Base
|
49
|
+
include RangeHelp
|
50
|
+
extend AutoCorrector
|
51
|
+
|
52
|
+
MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
|
53
|
+
MSG_IF_NO_INIT_VALUE =
|
54
|
+
'Use `%<good_method>s` instead of `%<bad_method>s`, unless calling `%<bad_method>s` on an empty array.'
|
55
|
+
RESTRICT_ON_SEND = %i[inject reduce sum].freeze
|
56
|
+
|
57
|
+
def_node_matcher :sum_candidate?, <<~PATTERN
|
58
|
+
(send _ ${:inject :reduce} $_init ? ${(sym :+) (block_pass (sym :+))})
|
59
|
+
PATTERN
|
60
|
+
|
61
|
+
def_node_matcher :sum_map_candidate?, <<~PATTERN
|
62
|
+
(send
|
63
|
+
{
|
64
|
+
(block $(send _ {:map :collect}) ...)
|
65
|
+
$(send _ {:map :collect} (block_pass _))
|
66
|
+
}
|
67
|
+
:sum $_init ?)
|
68
|
+
PATTERN
|
69
|
+
|
70
|
+
def_node_matcher :sum_with_block_candidate?, <<~PATTERN
|
71
|
+
(block
|
72
|
+
$(send _ {:inject :reduce} $_init ?)
|
73
|
+
(args (arg $_acc) (arg $_elem))
|
74
|
+
$send)
|
75
|
+
PATTERN
|
76
|
+
|
77
|
+
def_node_matcher :acc_plus_elem?, <<~PATTERN
|
78
|
+
(send (lvar %1) :+ (lvar %2))
|
79
|
+
PATTERN
|
80
|
+
alias elem_plus_acc? acc_plus_elem?
|
81
|
+
|
82
|
+
def on_send(node)
|
83
|
+
return if empty_array_literal?(node)
|
84
|
+
|
85
|
+
handle_sum_candidate(node)
|
86
|
+
handle_sum_map_candidate(node)
|
87
|
+
end
|
88
|
+
|
89
|
+
def on_block(node)
|
90
|
+
sum_with_block_candidate?(node) do |send, init, var_acc, var_elem, body|
|
91
|
+
if acc_plus_elem?(body, var_acc, var_elem) || elem_plus_acc?(body, var_elem, var_acc)
|
92
|
+
range = sum_block_range(send, node)
|
93
|
+
message = build_block_message(send, init, var_acc, var_elem, body)
|
94
|
+
|
95
|
+
add_offense(range, message: message) do |corrector|
|
96
|
+
autocorrect(corrector, init, range)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
def handle_sum_candidate(node)
|
105
|
+
sum_candidate?(node) do |method, init, operation|
|
106
|
+
range = sum_method_range(node)
|
107
|
+
message = build_method_message(node, method, init, operation)
|
108
|
+
|
109
|
+
add_offense(range, message: message) do |corrector|
|
110
|
+
autocorrect(corrector, init, range)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def handle_sum_map_candidate(node)
|
116
|
+
sum_map_candidate?(node) do |map, init|
|
117
|
+
next if node.block_literal? || node.block_argument?
|
118
|
+
|
119
|
+
message = build_sum_map_message(map.method_name, init)
|
120
|
+
|
121
|
+
add_offense(sum_map_range(map, node), message: message) do |corrector|
|
122
|
+
autocorrect_sum_map(corrector, node, map, init)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def empty_array_literal?(node)
|
128
|
+
receiver = node.children.first
|
129
|
+
array_literal?(node) && receiver && receiver.children.empty?
|
130
|
+
end
|
131
|
+
|
132
|
+
def array_literal?(node)
|
133
|
+
receiver = node.children.first
|
134
|
+
receiver&.literal? && receiver&.array_type?
|
135
|
+
end
|
136
|
+
|
137
|
+
def autocorrect(corrector, init, range)
|
138
|
+
return if init.empty? && safe_autocorrect?
|
139
|
+
|
140
|
+
replacement = build_good_method(init)
|
141
|
+
|
142
|
+
corrector.replace(range, replacement)
|
143
|
+
end
|
144
|
+
|
145
|
+
def autocorrect_sum_map(corrector, sum, map, init)
|
146
|
+
sum_range = method_call_with_args_range(sum)
|
147
|
+
map_range = method_call_with_args_range(map)
|
148
|
+
|
149
|
+
block_pass = map.last_argument if map.last_argument&.block_pass_type?
|
150
|
+
replacement = build_good_method(init, block_pass)
|
151
|
+
|
152
|
+
corrector.remove(sum_range)
|
153
|
+
corrector.replace(map_range, ".#{replacement}")
|
154
|
+
end
|
155
|
+
|
156
|
+
def sum_method_range(node)
|
157
|
+
range_between(node.loc.selector.begin_pos, node.loc.end.end_pos)
|
158
|
+
end
|
159
|
+
|
160
|
+
def sum_map_range(map, sum)
|
161
|
+
range_between(map.loc.selector.begin_pos, sum.source_range.end.end_pos)
|
162
|
+
end
|
163
|
+
|
164
|
+
def sum_block_range(send, node)
|
165
|
+
range_between(send.loc.selector.begin_pos, node.loc.end.end_pos)
|
166
|
+
end
|
167
|
+
|
168
|
+
def build_method_message(node, method, init, operation)
|
169
|
+
good_method = build_good_method(init)
|
170
|
+
bad_method = build_method_bad_method(init, method, operation)
|
171
|
+
msg = if init.empty? && !array_literal?(node)
|
172
|
+
MSG_IF_NO_INIT_VALUE
|
173
|
+
else
|
174
|
+
MSG
|
175
|
+
end
|
176
|
+
format(msg, good_method: good_method, bad_method: bad_method)
|
177
|
+
end
|
178
|
+
|
179
|
+
def build_sum_map_message(method, init)
|
180
|
+
sum_method = build_good_method(init)
|
181
|
+
good_method = "#{sum_method} { ... }"
|
182
|
+
bad_method = "#{method} { ... }.#{sum_method}"
|
183
|
+
format(MSG, good_method: good_method, bad_method: bad_method)
|
184
|
+
end
|
185
|
+
|
186
|
+
def build_block_message(send, init, var_acc, var_elem, body)
|
187
|
+
good_method = build_good_method(init)
|
188
|
+
bad_method = build_block_bad_method(send.method_name, init, var_acc, var_elem, body)
|
189
|
+
format(MSG, good_method: good_method, bad_method: bad_method)
|
190
|
+
end
|
191
|
+
|
192
|
+
def build_good_method(init, block_pass = nil)
|
193
|
+
good_method = 'sum'
|
194
|
+
|
195
|
+
args = []
|
196
|
+
unless init.empty?
|
197
|
+
init = init.first
|
198
|
+
args << init.source unless init.int_type? && init.value.zero?
|
199
|
+
end
|
200
|
+
args << block_pass.source if block_pass
|
201
|
+
good_method += "(#{args.join(', ')})" unless args.empty?
|
202
|
+
good_method
|
203
|
+
end
|
204
|
+
|
205
|
+
def build_method_bad_method(init, method, operation)
|
206
|
+
bad_method = "#{method}("
|
207
|
+
unless init.empty?
|
208
|
+
init = init.first
|
209
|
+
bad_method += "#{init.source}, "
|
210
|
+
end
|
211
|
+
bad_method += if operation.block_pass_type?
|
212
|
+
'&:+)'
|
213
|
+
else
|
214
|
+
':+)'
|
215
|
+
end
|
216
|
+
bad_method
|
217
|
+
end
|
218
|
+
|
219
|
+
def build_block_bad_method(method, init, var_acc, var_elem, body)
|
220
|
+
bad_method = method.to_s
|
221
|
+
|
222
|
+
unless init.empty?
|
223
|
+
init = init.first
|
224
|
+
bad_method += "(#{init.source})"
|
225
|
+
end
|
226
|
+
bad_method += " { |#{var_acc}, #{var_elem}| #{body.source} }"
|
227
|
+
bad_method
|
228
|
+
end
|
229
|
+
|
230
|
+
def method_call_with_args_range(node)
|
231
|
+
node.receiver.source_range.end.join(node.source_range.end)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|