rubocop-performance 1.5.1 → 1.7.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +5 -1
  4. data/config/default.yml +78 -6
  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 +47 -0
  8. data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +50 -0
  9. data/lib/rubocop/cop/performance/bind_call.rb +87 -0
  10. data/lib/rubocop/cop/performance/caller.rb +2 -2
  11. data/lib/rubocop/cop/performance/casecmp.rb +5 -3
  12. data/lib/rubocop/cop/performance/chain_array_allocation.rb +1 -1
  13. data/lib/rubocop/cop/performance/compare_with_block.rb +2 -2
  14. data/lib/rubocop/cop/performance/count.rb +3 -3
  15. data/lib/rubocop/cop/performance/delete_prefix.rb +96 -0
  16. data/lib/rubocop/cop/performance/delete_suffix.rb +96 -0
  17. data/lib/rubocop/cop/performance/detect.rb +1 -1
  18. data/lib/rubocop/cop/performance/double_start_end_with.rb +2 -2
  19. data/lib/rubocop/cop/performance/end_with.rb +30 -12
  20. data/lib/rubocop/cop/performance/fixed_size.rb +1 -1
  21. data/lib/rubocop/cop/performance/flat_map.rb +1 -1
  22. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +1 -1
  23. data/lib/rubocop/cop/performance/io_readlines.rb +127 -0
  24. data/lib/rubocop/cop/performance/open_struct.rb +1 -1
  25. data/lib/rubocop/cop/performance/range_include.rb +10 -8
  26. data/lib/rubocop/cop/performance/redundant_block_call.rb +3 -3
  27. data/lib/rubocop/cop/performance/redundant_match.rb +2 -2
  28. data/lib/rubocop/cop/performance/redundant_merge.rb +20 -7
  29. data/lib/rubocop/cop/performance/redundant_sort_block.rb +53 -0
  30. data/lib/rubocop/cop/performance/redundant_string_chars.rb +137 -0
  31. data/lib/rubocop/cop/performance/regexp_match.rb +13 -13
  32. data/lib/rubocop/cop/performance/reverse_each.rb +3 -2
  33. data/lib/rubocop/cop/performance/reverse_first.rb +78 -0
  34. data/lib/rubocop/cop/performance/size.rb +35 -37
  35. data/lib/rubocop/cop/performance/sort_reverse.rb +54 -0
  36. data/lib/rubocop/cop/performance/squeeze.rb +70 -0
  37. data/lib/rubocop/cop/performance/start_with.rb +30 -15
  38. data/lib/rubocop/cop/performance/string_include.rb +59 -0
  39. data/lib/rubocop/cop/performance/string_replacement.rb +4 -11
  40. data/lib/rubocop/cop/performance/times_map.rb +1 -1
  41. data/lib/rubocop/cop/performance/unfreeze_string.rb +3 -7
  42. data/lib/rubocop/cop/performance/uri_default_parser.rb +1 -1
  43. data/lib/rubocop/cop/performance_cops.rb +15 -0
  44. data/lib/rubocop/performance/inject.rb +1 -1
  45. data/lib/rubocop/performance/version.rb +1 -1
  46. metadata +25 -11
@@ -0,0 +1,78 @@
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 < Cop
20
+ include RangeHelp
21
+
22
+ MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
23
+
24
+ def_node_matcher :reverse_first_candidate?, <<~PATTERN
25
+ (send $(send _ :reverse) :first (int _)?)
26
+ PATTERN
27
+
28
+ def on_send(node)
29
+ reverse_first_candidate?(node) do |receiver|
30
+ range = correction_range(receiver, node)
31
+ message = build_message(node)
32
+
33
+ add_offense(node, location: range, message: message)
34
+ end
35
+ end
36
+
37
+ def autocorrect(node)
38
+ reverse_first_candidate?(node) do |receiver|
39
+ range = correction_range(receiver, node)
40
+ replacement = build_good_method(node)
41
+
42
+ lambda do |corrector|
43
+ corrector.replace(range, replacement)
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def correction_range(receiver, node)
51
+ range_between(receiver.loc.selector.begin_pos, node.loc.expression.end_pos)
52
+ end
53
+
54
+ def build_message(node)
55
+ good_method = build_good_method(node)
56
+ bad_method = build_bad_method(node)
57
+ format(MSG, good_method: good_method, bad_method: bad_method)
58
+ end
59
+
60
+ def build_good_method(node)
61
+ if node.arguments?
62
+ "last(#{node.arguments.first.source}).reverse"
63
+ else
64
+ 'last'
65
+ end
66
+ end
67
+
68
+ def build_bad_method(node)
69
+ if node.arguments?
70
+ "reverse.first(#{node.arguments.first.source})"
71
+ else
72
+ 'reverse.first'
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -9,15 +9,27 @@ 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 }
@@ -26,8 +38,30 @@ module RuboCop
26
38
  class Size < Cop
27
39
  MSG = 'Use `size` instead of `count`.'
28
40
 
41
+ def_node_matcher :array?, <<~PATTERN
42
+ {
43
+ [!nil? array_type?]
44
+ (send _ :to_a)
45
+ (send (const nil? :Array) :[] _)
46
+ (send nil? :Array _)
47
+ }
48
+ PATTERN
49
+
50
+ def_node_matcher :hash?, <<~PATTERN
51
+ {
52
+ [!nil? hash_type?]
53
+ (send _ :to_h)
54
+ (send (const nil? :Hash) :[] _)
55
+ (send nil? :Hash _)
56
+ }
57
+ PATTERN
58
+
59
+ def_node_matcher :count?, <<~PATTERN
60
+ (send {#array? #hash?} :count)
61
+ PATTERN
62
+
29
63
  def on_send(node)
30
- return unless eligible_node?(node)
64
+ return if node.parent&.block_type? || !count?(node)
31
65
 
32
66
  add_offense(node, location: :selector)
33
67
  end
@@ -35,42 +69,6 @@ module RuboCop
35
69
  def autocorrect(node)
36
70
  ->(corrector) { corrector.replace(node.loc.selector, 'size') }
37
71
  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
71
-
72
- constant == :Hash || node.method?(:to_h)
73
- end
74
72
  end
75
73
  end
76
74
  end
@@ -0,0 +1,54 @@
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 < Cop
17
+ include SortBlock
18
+
19
+ MSG = 'Use `sort.reverse` instead of `%<bad_method>s`.'
20
+
21
+ def on_block(node)
22
+ sort_with_block?(node) do |send, var_a, var_b, body|
23
+ replaceable_body?(body, var_b, var_a) do
24
+ range = sort_range(send, node)
25
+
26
+ add_offense(
27
+ node,
28
+ location: range,
29
+ message: message(var_a, var_b)
30
+ )
31
+ end
32
+ end
33
+ end
34
+
35
+ def autocorrect(node)
36
+ sort_with_block?(node) do |send, _var_a, _var_b, _body|
37
+ lambda do |corrector|
38
+ range = sort_range(send, node)
39
+ replacement = 'sort.reverse'
40
+ corrector.replace(range, replacement)
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def message(var_a, var_b)
48
+ bad_method = "sort { |#{var_a}, #{var_b}| #{var_b} <=> #{var_a} }"
49
+ format(MSG, bad_method: bad_method)
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,70 @@
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 < Cop
22
+ MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
23
+
24
+ PREFERRED_METHODS = {
25
+ gsub: :squeeze,
26
+ gsub!: :squeeze!
27
+ }.freeze
28
+
29
+ def_node_matcher :squeeze_candidate?, <<~PATTERN
30
+ (send
31
+ $!nil? ${:gsub :gsub!}
32
+ (regexp
33
+ (str $#repeating_literal?)
34
+ (regopt))
35
+ (str $_))
36
+ PATTERN
37
+
38
+ def on_send(node)
39
+ squeeze_candidate?(node) do |_, bad_method, regexp_str, replace_str|
40
+ regexp_str = regexp_str[0..-2] # delete '+' from the end
41
+ regexp_str = interpret_string_escapes(regexp_str)
42
+ return unless replace_str == regexp_str
43
+
44
+ good_method = PREFERRED_METHODS[bad_method]
45
+ message = format(MSG, current: bad_method, prefer: good_method)
46
+ add_offense(node, location: :selector, message: message)
47
+ end
48
+ end
49
+
50
+ def autocorrect(node)
51
+ squeeze_candidate?(node) do |receiver, bad_method, _regexp_str, replace_str|
52
+ lambda do |corrector|
53
+ good_method = PREFERRED_METHODS[bad_method]
54
+ string_literal = to_string_literal(replace_str)
55
+
56
+ new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
57
+ corrector.replace(node.source_range, new_code)
58
+ end
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def repeating_literal?(regex_str)
65
+ regex_str.match?(/\A(?:#{Util::LITERAL_REGEX})\+\z/)
66
+ end
67
+ end
68
+ end
69
+ end
70
+ 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,27 +20,39 @@ module RuboCop
17
20
  #
18
21
  # # good
19
22
  # 'abc'.start_with?('ab')
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
+ #
20
44
  class StartWith < Cop
45
+ include RegexpMetacharacter
46
+
21
47
  MSG = 'Use `String#start_with?` instead of a regex match anchored to ' \
22
48
  'the beginning of the string.'
23
- SINGLE_QUOTE = "'"
24
49
 
25
- def_node_matcher :redundant_regex?, <<-PATTERN
50
+ def_node_matcher :redundant_regex?, <<~PATTERN
26
51
  {(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_start?) (regopt)))
27
52
  (send (regexp (str $#literal_at_start?) (regopt)) {:match :match?} $_)
28
53
  (match-with-lvasgn (regexp (str $#literal_at_start?) (regopt)) $_)}
29
54
  PATTERN
30
55
 
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
56
  def on_send(node)
42
57
  return unless redundant_regex?(node)
43
58
 
@@ -48,7 +63,7 @@ module RuboCop
48
63
  def autocorrect(node)
49
64
  redundant_regex?(node) do |receiver, regex_str|
50
65
  receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
51
- regex_str = regex_str[2..-1] # drop \A anchor
66
+ regex_str = drop_start_metacharacter(regex_str)
52
67
  regex_str = interpret_string_escapes(regex_str)
53
68
 
54
69
  lambda do |corrector|
@@ -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#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 < Cop
23
+ MSG = 'Use `String#include?` instead of a regex match with literal-only pattern.'
24
+
25
+ def_node_matcher :redundant_regex?, <<~PATTERN
26
+ {(send $!nil? {:match :=~ :match?} (regexp (str $#literal?) (regopt)))
27
+ (send (regexp (str $#literal?) (regopt)) {:match :match?} $str)
28
+ (match-with-lvasgn (regexp (str $#literal?) (regopt)) $_)}
29
+ PATTERN
30
+
31
+ def on_send(node)
32
+ return unless redundant_regex?(node)
33
+
34
+ add_offense(node)
35
+ end
36
+ alias on_match_with_lvasgn on_send
37
+
38
+ def autocorrect(node)
39
+ redundant_regex?(node) do |receiver, regex_str|
40
+ receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
41
+ regex_str = interpret_string_escapes(regex_str)
42
+
43
+ lambda do |corrector|
44
+ new_source = receiver.source + '.include?(' +
45
+ to_string_literal(regex_str) + ')'
46
+ corrector.replace(node.source_range, new_source)
47
+ end
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def literal?(regex_str)
54
+ regex_str.match?(/\A#{Util::LITERAL_REGEX}+\z/)
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -26,9 +26,8 @@ module RuboCop
26
26
  DELETE = 'delete'
27
27
  TR = 'tr'
28
28
  BANG = '!'
29
- SINGLE_QUOTE = "'"
30
29
 
31
- def_node_matcher :string_replacement?, <<-PATTERN
30
+ def_node_matcher :string_replacement?, <<~PATTERN
32
31
  (send _ {:gsub :gsub!}
33
32
  ${regexp str (send (const nil? :Regexp) {:new :compile} _)}
34
33
  $str)
@@ -48,9 +47,7 @@ module RuboCop
48
47
  first_source, = first_source(first_param)
49
48
  second_source, = *second_param
50
49
 
51
- unless first_param.str_type?
52
- first_source = interpret_string_escapes(first_source)
53
- end
50
+ first_source = interpret_string_escapes(first_source) unless first_param.str_type?
54
51
 
55
52
  replacement_method =
56
53
  replacement_method(node, first_source, second_source)
@@ -67,9 +64,7 @@ module RuboCop
67
64
  to_string_literal(first))
68
65
  end
69
66
 
70
- if second.empty? && first.length == 1
71
- remove_second_param(corrector, node, first_param)
72
- end
67
+ remove_second_param(corrector, node, first_param) if second.empty? && first.length == 1
73
68
  end
74
69
  end
75
70
 
@@ -99,9 +94,7 @@ module RuboCop
99
94
 
100
95
  def offense(node, first_param, second_param)
101
96
  first_source, = first_source(first_param)
102
- unless first_param.str_type?
103
- first_source = interpret_string_escapes(first_source)
104
- end
97
+ first_source = interpret_string_escapes(first_source) unless first_param.str_type?
105
98
  second_source, = *second_param
106
99
  message = message(node, first_source, second_source)
107
100