rubocop-performance 1.8.1 → 1.10.1
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/LICENSE.txt +1 -1
- data/README.md +10 -2
- data/config/default.yml +47 -6
- data/lib/rubocop/cop/mixin/regexp_metacharacter.rb +4 -4
- data/lib/rubocop/cop/performance/ancestors_include.rb +1 -0
- data/lib/rubocop/cop/performance/array_semi_infinite_range_slice.rb +77 -0
- data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +1 -0
- data/lib/rubocop/cop/performance/bind_call.rb +3 -2
- data/lib/rubocop/cop/performance/block_given_with_explicit_block.rb +52 -0
- data/lib/rubocop/cop/performance/caller.rb +13 -15
- data/lib/rubocop/cop/performance/casecmp.rb +1 -0
- data/lib/rubocop/cop/performance/chain_array_allocation.rb +20 -18
- data/lib/rubocop/cop/performance/constant_regexp.rb +73 -0
- data/lib/rubocop/cop/performance/count.rb +1 -0
- data/lib/rubocop/cop/performance/delete_prefix.rb +1 -0
- data/lib/rubocop/cop/performance/delete_suffix.rb +1 -0
- data/lib/rubocop/cop/performance/detect.rb +3 -2
- data/lib/rubocop/cop/performance/end_with.rb +1 -0
- data/lib/rubocop/cop/performance/fixed_size.rb +1 -0
- data/lib/rubocop/cop/performance/flat_map.rb +1 -0
- data/lib/rubocop/cop/performance/inefficient_hash_search.rb +2 -0
- data/lib/rubocop/cop/performance/io_readlines.rb +3 -7
- data/lib/rubocop/cop/performance/method_object_as_block.rb +32 -0
- data/lib/rubocop/cop/performance/open_struct.rb +1 -0
- data/lib/rubocop/cop/performance/range_include.rb +1 -0
- data/lib/rubocop/cop/performance/redundant_block_call.rb +4 -4
- data/lib/rubocop/cop/performance/redundant_equality_comparison_block.rb +80 -0
- data/lib/rubocop/cop/performance/redundant_match.rb +1 -0
- data/lib/rubocop/cop/performance/redundant_merge.rb +1 -0
- data/lib/rubocop/cop/performance/redundant_split_regexp_argument.rb +64 -0
- data/lib/rubocop/cop/performance/redundant_string_chars.rb +2 -6
- data/lib/rubocop/cop/performance/reverse_each.rb +7 -10
- data/lib/rubocop/cop/performance/reverse_first.rb +1 -0
- data/lib/rubocop/cop/performance/size.rb +1 -0
- data/lib/rubocop/cop/performance/squeeze.rb +2 -1
- data/lib/rubocop/cop/performance/start_with.rb +1 -0
- data/lib/rubocop/cop/performance/string_include.rb +2 -1
- data/lib/rubocop/cop/performance/string_replacement.rb +1 -0
- data/lib/rubocop/cop/performance/sum.rb +123 -15
- data/lib/rubocop/cop/performance/times_map.rb +1 -0
- data/lib/rubocop/cop/performance/unfreeze_string.rb +19 -1
- data/lib/rubocop/cop/performance/uri_default_parser.rb +1 -0
- data/lib/rubocop/cop/performance_cops.rb +6 -0
- data/lib/rubocop/performance/version.rb +6 -1
- metadata +25 -27
@@ -29,38 +29,40 @@ module RuboCop
|
|
29
29
|
# [1,2].first # => 1
|
30
30
|
# [1,2].first(1) # => [1]
|
31
31
|
#
|
32
|
-
RETURN_NEW_ARRAY_WHEN_ARGS =
|
32
|
+
RETURN_NEW_ARRAY_WHEN_ARGS = %i[first last pop sample shift].to_set.freeze
|
33
33
|
|
34
34
|
# These methods return a new array only when called without a block.
|
35
|
-
RETURNS_NEW_ARRAY_WHEN_NO_BLOCK =
|
35
|
+
RETURNS_NEW_ARRAY_WHEN_NO_BLOCK = %i[zip product].to_set.freeze
|
36
36
|
|
37
37
|
# These methods ALWAYS return a new array
|
38
38
|
# after they're called it's safe to mutate the the resulting array
|
39
|
-
ALWAYS_RETURNS_NEW_ARRAY =
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
39
|
+
ALWAYS_RETURNS_NEW_ARRAY = %i[* + - collect compact drop
|
40
|
+
drop_while flatten map reject
|
41
|
+
reverse rotate select shuffle sort
|
42
|
+
take take_while transpose uniq
|
43
|
+
values_at |].to_set.freeze
|
44
44
|
|
45
45
|
# These methods have a mutation alternative. For example :collect
|
46
46
|
# can be called as :collect!
|
47
|
-
HAS_MUTATION_ALTERNATIVE =
|
48
|
-
|
49
|
-
|
50
|
-
|
47
|
+
HAS_MUTATION_ALTERNATIVE = %i[collect compact flatten map reject
|
48
|
+
reverse rotate select shuffle sort uniq].to_set.freeze
|
49
|
+
|
50
|
+
RETURNS_NEW_ARRAY = (ALWAYS_RETURNS_NEW_ARRAY + RETURNS_NEW_ARRAY_WHEN_NO_BLOCK).freeze
|
51
|
+
|
52
|
+
MSG = 'Use unchained `%<method>s` and `%<second_method>s!` '\
|
51
53
|
'(followed by `return array` if required) instead of chaining '\
|
52
54
|
'`%<method>s...%<second_method>s`.'
|
53
55
|
|
54
|
-
def_node_matcher :
|
55
|
-
{
|
56
|
-
(send
|
57
|
-
(
|
58
|
-
(send
|
59
|
-
}
|
56
|
+
def_node_matcher :chain_array_allocation?, <<~PATTERN
|
57
|
+
(send {
|
58
|
+
(send _ $%RETURN_NEW_ARRAY_WHEN_ARGS {int lvar ivar cvar gvar})
|
59
|
+
(block (send _ $%ALWAYS_RETURNS_NEW_ARRAY) ...)
|
60
|
+
(send _ $%RETURNS_NEW_ARRAY ...)
|
61
|
+
} $%HAS_MUTATION_ALTERNATIVE ...)
|
60
62
|
PATTERN
|
61
63
|
|
62
64
|
def on_send(node)
|
63
|
-
|
65
|
+
chain_array_allocation?(node) do |fm, sm|
|
64
66
|
range = range_between(node.loc.dot.begin_pos, node.source_range.end_pos)
|
65
67
|
|
66
68
|
add_offense(range, message: format(MSG, method: fm, second_method: sm))
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop finds regular expressions with dynamic components that are all constants.
|
7
|
+
#
|
8
|
+
# Ruby allocates a new Regexp object every time it executes a code containing such
|
9
|
+
# a regular expression. It is more efficient to extract it into a constant,
|
10
|
+
# memoize it, or add an `/o` option to perform `#{}` interpolation only once and
|
11
|
+
# reuse that Regexp object.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
#
|
15
|
+
# # bad
|
16
|
+
# def tokens(pattern)
|
17
|
+
# pattern.scan(TOKEN).reject { |token| token.match?(/\A#{SEPARATORS}\Z/) }
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # good
|
21
|
+
# ALL_SEPARATORS = /\A#{SEPARATORS}\Z/
|
22
|
+
# def tokens(pattern)
|
23
|
+
# pattern.scan(TOKEN).reject { |token| token.match?(ALL_SEPARATORS) }
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# # good
|
27
|
+
# def tokens(pattern)
|
28
|
+
# pattern.scan(TOKEN).reject { |token| token.match?(/\A#{SEPARATORS}\Z/o) }
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# # good
|
32
|
+
# def separators
|
33
|
+
# @separators ||= /\A#{SEPARATORS}\Z/
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
class ConstantRegexp < Base
|
37
|
+
extend AutoCorrector
|
38
|
+
|
39
|
+
MSG = 'Extract this regexp into a constant, memoize it, or append an `/o` option to its options.'
|
40
|
+
|
41
|
+
def on_regexp(node)
|
42
|
+
return if within_allowed_assignment?(node) ||
|
43
|
+
!include_interpolated_const?(node) ||
|
44
|
+
node.single_interpolation?
|
45
|
+
|
46
|
+
add_offense(node) do |corrector|
|
47
|
+
corrector.insert_after(node, 'o')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def within_allowed_assignment?(node)
|
54
|
+
node.each_ancestor(:casgn, :or_asgn).any?
|
55
|
+
end
|
56
|
+
|
57
|
+
def_node_matcher :regexp_escape?, <<~PATTERN
|
58
|
+
(send
|
59
|
+
(const nil? :Regexp) :escape const_type?)
|
60
|
+
PATTERN
|
61
|
+
|
62
|
+
def include_interpolated_const?(node)
|
63
|
+
return false unless node.interpolation?
|
64
|
+
|
65
|
+
node.each_child_node(:begin).all? do |begin_node|
|
66
|
+
inner_node = begin_node.children.first
|
67
|
+
inner_node && (inner_node.const_type? || regexp_escape?(inner_node))
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -4,8 +4,8 @@ module RuboCop
|
|
4
4
|
module Cop
|
5
5
|
module Performance
|
6
6
|
# This cop is used to identify usages of `first`, `last`, `[0]` or `[-1]`
|
7
|
-
# chained to `select`, `find_all
|
8
|
-
#
|
7
|
+
# chained to `select`, `find_all` or `filter` and change them to use
|
8
|
+
# `detect` instead.
|
9
9
|
#
|
10
10
|
# @example
|
11
11
|
# # bad
|
@@ -39,6 +39,7 @@ module RuboCop
|
|
39
39
|
'`%<first_method>s[%<index>i]`.'
|
40
40
|
INDEX_REVERSE_MSG = 'Use `reverse.%<prefer>s` instead of ' \
|
41
41
|
'`%<first_method>s[%<index>i]`.'
|
42
|
+
RESTRICT_ON_SEND = %i[first last []].freeze
|
42
43
|
|
43
44
|
def_node_matcher :detect_candidate?, <<~PATTERN
|
44
45
|
{
|
@@ -47,6 +47,7 @@ module RuboCop
|
|
47
47
|
|
48
48
|
MSG = 'Use `String#end_with?` instead of a regex match anchored to ' \
|
49
49
|
'the end of the string.'
|
50
|
+
RESTRICT_ON_SEND = %i[match =~ match?].freeze
|
50
51
|
|
51
52
|
def_node_matcher :redundant_regex?, <<~PATTERN
|
52
53
|
{(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_end?) (regopt)))
|
@@ -47,6 +47,7 @@ module RuboCop
|
|
47
47
|
#
|
48
48
|
class FixedSize < Base
|
49
49
|
MSG = 'Do not compute the size of statically sized objects.'
|
50
|
+
RESTRICT_ON_SEND = %i[count length size].freeze
|
50
51
|
|
51
52
|
def_node_matcher :counter, <<~MATCHER
|
52
53
|
(send ${array hash str sym} {:count :length :size} $...)
|
@@ -19,6 +19,7 @@ module RuboCop
|
|
19
19
|
extend AutoCorrector
|
20
20
|
|
21
21
|
MSG = 'Use `flat_map` instead of `%<method>s...%<flatten>s`.'
|
22
|
+
RESTRICT_ON_SEND = %i[flatten flatten!].freeze
|
22
23
|
FLATTEN_MULTIPLE_LEVELS = ' Beware, `flat_map` only flattens 1 level ' \
|
23
24
|
'and `flatten` can be used to flatten ' \
|
24
25
|
'multiple levels.'
|
@@ -29,14 +29,14 @@ module RuboCop
|
|
29
29
|
extend AutoCorrector
|
30
30
|
|
31
31
|
MSG = 'Use `%<good>s` instead of `%<bad>s`.'
|
32
|
-
|
32
|
+
RESTRICT_ON_SEND = (Enumerable.instance_methods + [:each]).freeze
|
33
33
|
|
34
34
|
def_node_matcher :readlines_on_class?, <<~PATTERN
|
35
|
-
$(send $(send (const nil? {:IO :File}) :readlines ...)
|
35
|
+
$(send $(send (const nil? {:IO :File}) :readlines ...) _)
|
36
36
|
PATTERN
|
37
37
|
|
38
38
|
def_node_matcher :readlines_on_instance?, <<~PATTERN
|
39
|
-
$(send $(send ${nil? !const_type?} :readlines ...)
|
39
|
+
$(send $(send ${nil? !const_type?} :readlines ...) _ ...)
|
40
40
|
PATTERN
|
41
41
|
|
42
42
|
def on_send(node)
|
@@ -55,10 +55,6 @@ module RuboCop
|
|
55
55
|
|
56
56
|
private
|
57
57
|
|
58
|
-
def enumerable_method?(node)
|
59
|
-
ENUMERABLE_METHODS.include?(node.to_sym)
|
60
|
-
end
|
61
|
-
|
62
58
|
def autocorrect(corrector, enumerable_call, readlines_call, receiver)
|
63
59
|
# We cannot safely correct `.readlines` method called on IO/File classes
|
64
60
|
# due to its signature and we are not sure with implicit receiver
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop identifies places where methods are converted to blocks, with the
|
7
|
+
# use of `&method`, and passed as arguments to method calls.
|
8
|
+
# It is faster to replace those with explicit blocks, calling those methods inside.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# # bad
|
12
|
+
# array.map(&method(:do_something))
|
13
|
+
# [1, 2, 3].each(&out.method(:puts))
|
14
|
+
#
|
15
|
+
# # good
|
16
|
+
# array.map { |x| do_something(x) }
|
17
|
+
# [1, 2, 3].each { |x| out.puts(x) }
|
18
|
+
#
|
19
|
+
class MethodObjectAsBlock < Base
|
20
|
+
MSG = 'Use block explicitly instead of block-passing a method object.'
|
21
|
+
|
22
|
+
def_node_matcher :method_object_as_argument?, <<~PATTERN
|
23
|
+
(^send (send _ :method sym))
|
24
|
+
PATTERN
|
25
|
+
|
26
|
+
def on_block_pass(node)
|
27
|
+
add_offense(node) if method_object_as_argument?(node)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -30,6 +30,7 @@ module RuboCop
|
|
30
30
|
class OpenStruct < Base
|
31
31
|
MSG = 'Consider using `Struct` over `OpenStruct` ' \
|
32
32
|
'to optimize the performance.'
|
33
|
+
RESTRICT_ON_SEND = %i[new].freeze
|
33
34
|
|
34
35
|
def_node_matcher :open_struct, <<~PATTERN
|
35
36
|
(send (const {nil? cbase} :OpenStruct) :new ...)
|
@@ -28,6 +28,7 @@ module RuboCop
|
|
28
28
|
extend AutoCorrector
|
29
29
|
|
30
30
|
MSG = 'Use `Range#cover?` instead of `Range#%<bad_method>s`.'
|
31
|
+
RESTRICT_ON_SEND = %i[include? member?].freeze
|
31
32
|
|
32
33
|
# TODO: If we traced out assignments of variables to their uses, we
|
33
34
|
# might pick up on a few more instances of this issue
|
@@ -80,11 +80,11 @@ module RuboCop
|
|
80
80
|
def calls_to_report(argname, body)
|
81
81
|
return [] if blockarg_assigned?(body, argname)
|
82
82
|
|
83
|
-
|
83
|
+
blockarg_calls(body, argname).map do |call|
|
84
|
+
return [] if args_include_block_pass?(call)
|
84
85
|
|
85
|
-
|
86
|
-
|
87
|
-
calls
|
86
|
+
call
|
87
|
+
end
|
88
88
|
end
|
89
89
|
|
90
90
|
def args_include_block_pass?(blockcall)
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop checks for uses `Enumerable#all?`, `Enumerable#any?`, `Enumerable#one?`,
|
7
|
+
# and `Enumerable#none?` are compared with `===` or similar methods in block.
|
8
|
+
#
|
9
|
+
# By default, `Object#===` behaves the same as `Object#==`, but this
|
10
|
+
# behavior is appropriately overridden in subclass. For example,
|
11
|
+
# `Range#===` returns `true` when argument is within the range.
|
12
|
+
# Therefore, It is marked as unsafe by default because `===` and `==`
|
13
|
+
# do not always behave the same.
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # bad
|
17
|
+
# items.all? { |item| pattern === item }
|
18
|
+
# items.all? { |item| item == other }
|
19
|
+
# items.all? { |item| item.is_a?(Klass) }
|
20
|
+
# items.all? { |item| item.kind_of?(Klass) }
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# items.all?(pattern)
|
24
|
+
#
|
25
|
+
class RedundantEqualityComparisonBlock < Base
|
26
|
+
extend AutoCorrector
|
27
|
+
extend TargetRubyVersion
|
28
|
+
|
29
|
+
minimum_target_ruby_version 2.5
|
30
|
+
|
31
|
+
MSG = 'Use `%<prefer>s` instead of block.'
|
32
|
+
|
33
|
+
TARGET_METHODS = %i[all? any? one? none?].freeze
|
34
|
+
COMPARISON_METHODS = %i[== === is_a? kind_of?].freeze
|
35
|
+
IS_A_METHODS = %i[is_a? kind_of?].freeze
|
36
|
+
|
37
|
+
def on_block(node)
|
38
|
+
return unless TARGET_METHODS.include?(node.method_name) && node.arguments.one?
|
39
|
+
|
40
|
+
block_argument = node.arguments.first
|
41
|
+
block_body = node.body
|
42
|
+
return unless use_equality_comparison_block?(block_body)
|
43
|
+
return if same_block_argument_and_is_a_argument?(block_body, block_argument)
|
44
|
+
return unless (new_argument = new_argument(block_argument, block_body))
|
45
|
+
|
46
|
+
range = offense_range(node)
|
47
|
+
prefer = "#{node.method_name}(#{new_argument})"
|
48
|
+
|
49
|
+
add_offense(range, message: format(MSG, prefer: prefer)) do |corrector|
|
50
|
+
corrector.replace(range, prefer)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def use_equality_comparison_block?(block_body)
|
57
|
+
block_body.send_type? && COMPARISON_METHODS.include?(block_body.method_name)
|
58
|
+
end
|
59
|
+
|
60
|
+
def same_block_argument_and_is_a_argument?(block_body, block_argument)
|
61
|
+
return false unless IS_A_METHODS.include?(block_body.method_name)
|
62
|
+
|
63
|
+
block_argument.source == block_body.first_argument.source
|
64
|
+
end
|
65
|
+
|
66
|
+
def new_argument(block_argument, block_body)
|
67
|
+
if block_argument.source == block_body.receiver.source
|
68
|
+
block_body.first_argument.source
|
69
|
+
elsif block_argument.source == block_body.first_argument.source
|
70
|
+
block_body.receiver.source
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def offense_range(node)
|
75
|
+
node.send_node.loc.selector.join(node.source_range.end)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -22,6 +22,7 @@ module RuboCop
|
|
22
22
|
|
23
23
|
MSG = 'Use `=~` in places where the `MatchData` returned by ' \
|
24
24
|
'`#match` will not be used.'
|
25
|
+
RESTRICT_ON_SEND = %i[match].freeze
|
25
26
|
|
26
27
|
# 'match' is a fairly generic name, so we don't flag it unless we see
|
27
28
|
# a string or regexp literal on one side or the other
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Performance
|
6
|
+
# This cop identifies places where `split` argument can be replaced from
|
7
|
+
# a deterministic regexp to a string.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# 'a,b,c'.split(/,/)
|
12
|
+
#
|
13
|
+
# # good
|
14
|
+
# 'a,b,c'.split(',')
|
15
|
+
class RedundantSplitRegexpArgument < Base
|
16
|
+
extend AutoCorrector
|
17
|
+
|
18
|
+
MSG = 'Use string as argument instead of regexp.'
|
19
|
+
RESTRICT_ON_SEND = %i[split].freeze
|
20
|
+
DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/.freeze
|
21
|
+
STR_SPECIAL_CHARS = %w[\n \" \' \\\\ \t \b \f \r].freeze
|
22
|
+
|
23
|
+
def_node_matcher :split_call_with_regexp?, <<~PATTERN
|
24
|
+
{(send !nil? :split $regexp)}
|
25
|
+
PATTERN
|
26
|
+
|
27
|
+
def on_send(node)
|
28
|
+
return unless (regexp_node = split_call_with_regexp?(node))
|
29
|
+
return if regexp_node.ignore_case?
|
30
|
+
return unless determinist_regexp?(regexp_node)
|
31
|
+
|
32
|
+
add_offense(regexp_node) do |corrector|
|
33
|
+
new_argument = replacement(regexp_node)
|
34
|
+
|
35
|
+
corrector.replace(regexp_node, "\"#{new_argument}\"")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def determinist_regexp?(regexp_node)
|
42
|
+
DETERMINISTIC_REGEX.match?(regexp_node.source)
|
43
|
+
end
|
44
|
+
|
45
|
+
def replacement(regexp_node)
|
46
|
+
regexp_content = regexp_node.content
|
47
|
+
stack = []
|
48
|
+
chars = regexp_content.chars.each_with_object([]) do |char, strings|
|
49
|
+
if stack.empty? && char == '\\'
|
50
|
+
stack.push(char)
|
51
|
+
else
|
52
|
+
strings << "#{stack.pop}#{char}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
chars.map do |char|
|
56
|
+
char = char.dup
|
57
|
+
char.delete!('\\') unless STR_SPECIAL_CHARS.include?(char)
|
58
|
+
char
|
59
|
+
end.join
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|