rubocop-performance 1.5.2 → 1.8.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +5 -1
  4. data/config/default.yml +96 -13
  5. data/lib/rubocop/cop/mixin/regexp_metacharacter.rb +76 -0
  6. data/lib/rubocop/cop/mixin/sort_block.rb +28 -0
  7. data/lib/rubocop/cop/performance/ancestors_include.rb +48 -0
  8. data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +45 -0
  9. data/lib/rubocop/cop/performance/bind_call.rb +77 -0
  10. data/lib/rubocop/cop/performance/caller.rb +5 -4
  11. data/lib/rubocop/cop/performance/case_when_splat.rb +18 -11
  12. data/lib/rubocop/cop/performance/casecmp.rb +17 -23
  13. data/lib/rubocop/cop/performance/chain_array_allocation.rb +5 -11
  14. data/lib/rubocop/cop/performance/collection_literal_in_loop.rb +140 -0
  15. data/lib/rubocop/cop/performance/compare_with_block.rb +12 -23
  16. data/lib/rubocop/cop/performance/count.rb +14 -17
  17. data/lib/rubocop/cop/performance/delete_prefix.rb +87 -0
  18. data/lib/rubocop/cop/performance/delete_suffix.rb +87 -0
  19. data/lib/rubocop/cop/performance/detect.rb +64 -32
  20. data/lib/rubocop/cop/performance/double_start_end_with.rb +18 -26
  21. data/lib/rubocop/cop/performance/end_with.rb +38 -25
  22. data/lib/rubocop/cop/performance/fixed_size.rb +2 -2
  23. data/lib/rubocop/cop/performance/flat_map.rb +21 -23
  24. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +14 -15
  25. data/lib/rubocop/cop/performance/io_readlines.rb +116 -0
  26. data/lib/rubocop/cop/performance/open_struct.rb +3 -3
  27. data/lib/rubocop/cop/performance/range_include.rb +15 -12
  28. data/lib/rubocop/cop/performance/redundant_block_call.rb +14 -9
  29. data/lib/rubocop/cop/performance/redundant_match.rb +13 -8
  30. data/lib/rubocop/cop/performance/redundant_merge.rb +36 -23
  31. data/lib/rubocop/cop/performance/redundant_sort_block.rb +43 -0
  32. data/lib/rubocop/cop/performance/redundant_string_chars.rb +133 -0
  33. data/lib/rubocop/cop/performance/regexp_match.rb +32 -32
  34. data/lib/rubocop/cop/performance/reverse_each.rb +10 -5
  35. data/lib/rubocop/cop/performance/reverse_first.rb +72 -0
  36. data/lib/rubocop/cop/performance/size.rb +41 -43
  37. data/lib/rubocop/cop/performance/sort_reverse.rb +45 -0
  38. data/lib/rubocop/cop/performance/squeeze.rb +66 -0
  39. data/lib/rubocop/cop/performance/start_with.rb +38 -28
  40. data/lib/rubocop/cop/performance/string_include.rb +55 -0
  41. data/lib/rubocop/cop/performance/string_replacement.rb +25 -36
  42. data/lib/rubocop/cop/performance/sum.rb +134 -0
  43. data/lib/rubocop/cop/performance/times_map.rb +12 -19
  44. data/lib/rubocop/cop/performance/unfreeze_string.rb +4 -8
  45. data/lib/rubocop/cop/performance/uri_default_parser.rb +7 -13
  46. data/lib/rubocop/cop/performance_cops.rb +17 -0
  47. data/lib/rubocop/performance/inject.rb +1 -1
  48. data/lib/rubocop/performance/version.rb +1 -1
  49. metadata +41 -11
@@ -12,13 +12,14 @@ module RuboCop
12
12
  #
13
13
  # # good
14
14
  # [].reverse_each
15
- class ReverseEach < Cop
15
+ class ReverseEach < Base
16
16
  include RangeHelp
17
+ extend AutoCorrector
17
18
 
18
19
  MSG = 'Use `reverse_each` instead of `reverse.each`.'
19
20
  UNDERSCORE = '_'
20
21
 
21
- def_node_matcher :reverse_each?, <<-MATCHER
22
+ def_node_matcher :reverse_each?, <<~MATCHER
22
23
  (send $(send _ :reverse) :each)
23
24
  MATCHER
24
25
 
@@ -29,12 +30,16 @@ module RuboCop
29
30
 
30
31
  range = range_between(location_of_reverse, end_location)
31
32
 
32
- add_offense(node, location: range)
33
+ add_offense(range) do |corrector|
34
+ corrector.replace(replacement_range(node), UNDERSCORE)
35
+ end
33
36
  end
34
37
  end
35
38
 
36
- def autocorrect(node)
37
- ->(corrector) { corrector.replace(node.loc.dot, UNDERSCORE) }
39
+ private
40
+
41
+ def replacement_range(node)
42
+ range_between(node.loc.dot.begin_pos, node.loc.selector.begin_pos)
38
43
  end
39
44
  end
40
45
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop identifies places where `reverse.first(n)` and `reverse.first`
7
+ # can be replaced by `last(n).reverse` and `last`.
8
+ #
9
+ # @example
10
+ #
11
+ # # bad
12
+ # array.reverse.first(5)
13
+ # array.reverse.first
14
+ #
15
+ # # good
16
+ # array.last(5).reverse
17
+ # array.last
18
+ #
19
+ class ReverseFirst < Base
20
+ include RangeHelp
21
+ extend AutoCorrector
22
+
23
+ MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
24
+
25
+ def_node_matcher :reverse_first_candidate?, <<~PATTERN
26
+ (send $(send _ :reverse) :first (int _)?)
27
+ PATTERN
28
+
29
+ def on_send(node)
30
+ reverse_first_candidate?(node) do |receiver|
31
+ range = correction_range(receiver, node)
32
+ message = build_message(node)
33
+
34
+ add_offense(range, message: message) do |corrector|
35
+ replacement = build_good_method(node)
36
+
37
+ corrector.replace(range, replacement)
38
+ end
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def correction_range(receiver, node)
45
+ range_between(receiver.loc.selector.begin_pos, node.loc.expression.end_pos)
46
+ end
47
+
48
+ def build_message(node)
49
+ good_method = build_good_method(node)
50
+ bad_method = build_bad_method(node)
51
+ format(MSG, good_method: good_method, bad_method: bad_method)
52
+ end
53
+
54
+ def build_good_method(node)
55
+ if node.arguments?
56
+ "last(#{node.arguments.first.source}).reverse"
57
+ else
58
+ 'last'
59
+ end
60
+ end
61
+
62
+ def build_bad_method(node)
63
+ if node.arguments?
64
+ "reverse.first(#{node.arguments.first.source})"
65
+ else
66
+ 'reverse.first'
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -9,67 +9,65 @@ 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 < Cop
27
- MSG = 'Use `size` instead of `count`.'
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
38
+ class Size < Base
39
+ extend AutoCorrector
46
40
 
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
41
+ MSG = 'Use `size` instead of `count`.'
65
42
 
66
- def hash?(node)
67
- return true if node.hash_type?
68
- return false unless node.send_type?
43
+ def_node_matcher :array?, <<~PATTERN
44
+ {
45
+ [!nil? array_type?]
46
+ (send _ :to_a)
47
+ (send (const nil? :Array) :[] _)
48
+ (send nil? :Array _)
49
+ }
50
+ PATTERN
51
+
52
+ def_node_matcher :hash?, <<~PATTERN
53
+ {
54
+ [!nil? hash_type?]
55
+ (send _ :to_h)
56
+ (send (const nil? :Hash) :[] _)
57
+ (send nil? :Hash _)
58
+ }
59
+ PATTERN
60
+
61
+ def_node_matcher :count?, <<~PATTERN
62
+ (send {#array? #hash?} :count)
63
+ PATTERN
69
64
 
70
- _, constant = *node.receiver
65
+ def on_send(node)
66
+ return if node.parent&.block_type? || !count?(node)
71
67
 
72
- constant == :Hash || node.method?(:to_h)
68
+ add_offense(node.loc.selector) do |corrector|
69
+ corrector.replace(node.loc.selector, 'size')
70
+ end
73
71
  end
74
72
  end
75
73
  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,66 @@
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
+
26
+ PREFERRED_METHODS = {
27
+ gsub: :squeeze,
28
+ gsub!: :squeeze!
29
+ }.freeze
30
+
31
+ def_node_matcher :squeeze_candidate?, <<~PATTERN
32
+ (send
33
+ $!nil? ${:gsub :gsub!}
34
+ (regexp
35
+ (str $#repeating_literal?)
36
+ (regopt))
37
+ (str $_))
38
+ PATTERN
39
+
40
+ def on_send(node)
41
+ squeeze_candidate?(node) do |receiver, bad_method, regexp_str, replace_str|
42
+ regexp_str = regexp_str[0..-2] # delete '+' from the end
43
+ regexp_str = interpret_string_escapes(regexp_str)
44
+ return unless replace_str == regexp_str
45
+
46
+ good_method = PREFERRED_METHODS[bad_method]
47
+ message = format(MSG, current: bad_method, prefer: good_method)
48
+
49
+ add_offense(node.loc.selector, message: message) do |corrector|
50
+ string_literal = to_string_literal(replace_str)
51
+ new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
52
+
53
+ corrector.replace(node.source_range, new_code)
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def repeating_literal?(regex_str)
61
+ regex_str.match?(/\A(?:#{Util::LITERAL_REGEX})\+\z/)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -3,8 +3,11 @@
3
3
  module RuboCop
4
4
  module Cop
5
5
  module Performance
6
- # This cop identifies unnecessary use of a regex where
7
- # `String#start_with?` would suffice.
6
+ # This cop identifies unnecessary use of a regex where `String#start_with?` would suffice.
7
+ #
8
+ # This cop has `SafeMultiline` configuration option that `true` by default because
9
+ # `^start` is unsafe as it will behave incompatible with `start_with?`
10
+ # for receiver is multiline string.
8
11
  #
9
12
  # @example
10
13
  # # bad
@@ -17,47 +20,54 @@ module RuboCop
17
20
  #
18
21
  # # good
19
22
  # 'abc'.start_with?('ab')
20
- class StartWith < Cop
23
+ #
24
+ # @example SafeMultiline: true (default)
25
+ #
26
+ # # good
27
+ # 'abc'.match?(/^ab/)
28
+ # /^ab/.match?('abc')
29
+ # 'abc' =~ /^ab/
30
+ # /^ab/ =~ 'abc'
31
+ # 'abc'.match(/^ab/)
32
+ # /^ab/.match('abc')
33
+ #
34
+ # @example SafeMultiline: false
35
+ #
36
+ # # bad
37
+ # 'abc'.match?(/^ab/)
38
+ # /^ab/.match?('abc')
39
+ # 'abc' =~ /^ab/
40
+ # /^ab/ =~ 'abc'
41
+ # 'abc'.match(/^ab/)
42
+ # /^ab/.match('abc')
43
+ #
44
+ class StartWith < Base
45
+ include RegexpMetacharacter
46
+ extend AutoCorrector
47
+
21
48
  MSG = 'Use `String#start_with?` instead of a regex match anchored to ' \
22
49
  'the beginning of the string.'
23
- SINGLE_QUOTE = "'"
24
50
 
25
- def_node_matcher :redundant_regex?, <<-PATTERN
51
+ def_node_matcher :redundant_regex?, <<~PATTERN
26
52
  {(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_start?) (regopt)))
27
53
  (send (regexp (str $#literal_at_start?) (regopt)) {:match :match?} $_)
28
54
  (match-with-lvasgn (regexp (str $#literal_at_start?) (regopt)) $_)}
29
55
  PATTERN
30
56
 
31
- def literal_at_start?(regex_str)
32
- # is this regexp 'literal' in the sense of only matching literal
33
- # chars, rather than using metachars like `.` and `*` and so on?
34
- # also, is it anchored at the start of the string?
35
- # (tricky: \s, \d, and so on are metacharacters, but other characters
36
- # escaped with a slash are just literals. LITERAL_REGEX takes all
37
- # that into account.)
38
- regex_str =~ /\A\\A(?:#{LITERAL_REGEX})+\z/
39
- end
40
-
41
57
  def on_send(node)
42
- return unless redundant_regex?(node)
43
-
44
- add_offense(node)
45
- end
46
- alias on_match_with_lvasgn on_send
58
+ return unless (receiver, regex_str = redundant_regex?(node))
47
59
 
48
- def autocorrect(node)
49
- redundant_regex?(node) do |receiver, regex_str|
60
+ add_offense(node) do |corrector|
50
61
  receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
51
- regex_str = regex_str[2..-1] # drop \A anchor
62
+ regex_str = drop_start_metacharacter(regex_str)
52
63
  regex_str = interpret_string_escapes(regex_str)
53
64
 
54
- lambda do |corrector|
55
- new_source = receiver.source + '.start_with?(' +
56
- to_string_literal(regex_str) + ')'
57
- corrector.replace(node.source_range, new_source)
58
- end
65
+ new_source = "#{receiver.source}.start_with?(#{to_string_literal(regex_str)})"
66
+
67
+ corrector.replace(node.source_range, new_source)
59
68
  end
60
69
  end
70
+ alias on_match_with_lvasgn on_send
61
71
  end
62
72
  end
63
73
  end
@@ -0,0 +1,55 @@
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
+
27
+ def_node_matcher :redundant_regex?, <<~PATTERN
28
+ {(send $!nil? {:match :=~ :match?} (regexp (str $#literal?) (regopt)))
29
+ (send (regexp (str $#literal?) (regopt)) {:match :match?} $str)
30
+ (match-with-lvasgn (regexp (str $#literal?) (regopt)) $_)}
31
+ PATTERN
32
+
33
+ def on_send(node)
34
+ return unless (receiver, regex_str = redundant_regex?(node))
35
+
36
+ add_offense(node) do |corrector|
37
+ receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
38
+ regex_str = interpret_string_escapes(regex_str)
39
+
40
+ new_source = "#{receiver.source}.include?(#{to_string_literal(regex_str)})"
41
+
42
+ corrector.replace(node.source_range, new_source)
43
+ end
44
+ end
45
+ alias on_match_with_lvasgn on_send
46
+
47
+ private
48
+
49
+ def literal?(regex_str)
50
+ regex_str.match?(/\A#{Util::LITERAL_REGEX}+\z/)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end