rubocop-performance 1.6.0 → 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 (47) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +1 -1
  3. data/config/default.yml +77 -10
  4. data/lib/rubocop/cop/mixin/regexp_metacharacter.rb +39 -4
  5. data/lib/rubocop/cop/mixin/sort_block.rb +28 -0
  6. data/lib/rubocop/cop/performance/ancestors_include.rb +48 -0
  7. data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +45 -0
  8. data/lib/rubocop/cop/performance/bind_call.rb +8 -18
  9. data/lib/rubocop/cop/performance/caller.rb +3 -2
  10. data/lib/rubocop/cop/performance/case_when_splat.rb +18 -11
  11. data/lib/rubocop/cop/performance/casecmp.rb +12 -20
  12. data/lib/rubocop/cop/performance/chain_array_allocation.rb +4 -10
  13. data/lib/rubocop/cop/performance/collection_literal_in_loop.rb +140 -0
  14. data/lib/rubocop/cop/performance/compare_with_block.rb +10 -21
  15. data/lib/rubocop/cop/performance/count.rb +13 -16
  16. data/lib/rubocop/cop/performance/delete_prefix.rb +43 -28
  17. data/lib/rubocop/cop/performance/delete_suffix.rb +43 -28
  18. data/lib/rubocop/cop/performance/detect.rb +63 -31
  19. data/lib/rubocop/cop/performance/double_start_end_with.rb +16 -24
  20. data/lib/rubocop/cop/performance/end_with.rb +29 -17
  21. data/lib/rubocop/cop/performance/fixed_size.rb +1 -1
  22. data/lib/rubocop/cop/performance/flat_map.rb +20 -22
  23. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +13 -14
  24. data/lib/rubocop/cop/performance/io_readlines.rb +116 -0
  25. data/lib/rubocop/cop/performance/open_struct.rb +2 -2
  26. data/lib/rubocop/cop/performance/range_include.rb +14 -11
  27. data/lib/rubocop/cop/performance/redundant_block_call.rb +11 -6
  28. data/lib/rubocop/cop/performance/redundant_match.rb +11 -6
  29. data/lib/rubocop/cop/performance/redundant_merge.rb +18 -17
  30. data/lib/rubocop/cop/performance/redundant_sort_block.rb +43 -0
  31. data/lib/rubocop/cop/performance/redundant_string_chars.rb +133 -0
  32. data/lib/rubocop/cop/performance/regexp_match.rb +20 -20
  33. data/lib/rubocop/cop/performance/reverse_each.rb +9 -5
  34. data/lib/rubocop/cop/performance/reverse_first.rb +72 -0
  35. data/lib/rubocop/cop/performance/size.rb +41 -43
  36. data/lib/rubocop/cop/performance/sort_reverse.rb +45 -0
  37. data/lib/rubocop/cop/performance/squeeze.rb +66 -0
  38. data/lib/rubocop/cop/performance/start_with.rb +29 -17
  39. data/lib/rubocop/cop/performance/string_include.rb +55 -0
  40. data/lib/rubocop/cop/performance/string_replacement.rb +23 -27
  41. data/lib/rubocop/cop/performance/sum.rb +134 -0
  42. data/lib/rubocop/cop/performance/times_map.rb +11 -18
  43. data/lib/rubocop/cop/performance/unfreeze_string.rb +2 -2
  44. data/lib/rubocop/cop/performance/uri_default_parser.rb +6 -12
  45. data/lib/rubocop/cop/performance_cops.rb +12 -0
  46. data/lib/rubocop/performance/version.rb +1 -1
  47. metadata +33 -8
@@ -18,7 +18,7 @@ module RuboCop
18
18
  # caller(1..1).first
19
19
  # caller_locations(2..2).first
20
20
  # caller_locations(1..1).first
21
- class Caller < Cop
21
+ class Caller < Base
22
22
  MSG_BRACE = 'Use `%<method>s(%<n>d..%<n>d).first`' \
23
23
  ' instead of `%<method>s[%<m>d]`.'
24
24
  MSG_FIRST = 'Use `%<method>s(%<n>d..%<n>d).first`' \
@@ -41,7 +41,8 @@ module RuboCop
41
41
  def on_send(node)
42
42
  return unless caller_with_scope_method?(node)
43
43
 
44
- add_offense(node)
44
+ message = message(node)
45
+ add_offense(node, message: message)
45
46
  end
46
47
 
47
48
  private
@@ -53,9 +53,10 @@ module RuboCop
53
53
  # when 5
54
54
  # baz
55
55
  # end
56
- class CaseWhenSplat < Cop
56
+ class CaseWhenSplat < Base
57
57
  include Alignment
58
58
  include RangeHelp
59
+ extend AutoCorrector
59
60
 
60
61
  MSG = 'Reordering `when` conditions with a splat to the end ' \
61
62
  'of the `when` branches can improve performance.'
@@ -66,24 +67,30 @@ module RuboCop
66
67
  when_conditions = case_node.when_branches.flat_map(&:conditions)
67
68
 
68
69
  splat_offenses(when_conditions).reverse_each do |condition|
69
- range = condition.parent.loc.keyword.join(condition.source_range)
70
+ next if ignored_node?(condition.parent)
71
+
72
+ ignore_node(condition.parent)
70
73
  variable, = *condition
71
74
  message = variable.array_type? ? ARRAY_MSG : MSG
72
- add_offense(condition.parent, location: range, message: message)
75
+ add_offense(range(condition), message: message) do |corrector|
76
+ autocorrect(corrector, condition.parent)
77
+ end
73
78
  end
74
79
  end
75
80
 
76
- def autocorrect(when_node)
77
- lambda do |corrector|
78
- if needs_reorder?(when_node)
79
- reorder_condition(corrector, when_node)
80
- else
81
- inline_fix_branch(corrector, when_node)
82
- end
81
+ private
82
+
83
+ def autocorrect(corrector, when_node)
84
+ if needs_reorder?(when_node)
85
+ reorder_condition(corrector, when_node)
86
+ else
87
+ inline_fix_branch(corrector, when_node)
83
88
  end
84
89
  end
85
90
 
86
- private
91
+ def range(node)
92
+ node.parent.loc.keyword.join(node.source_range)
93
+ end
87
94
 
88
95
  def replacement(conditions)
89
96
  reordered = conditions.partition(&:splat_type?).reverse
@@ -19,7 +19,9 @@ module RuboCop
19
19
  # # good
20
20
  # str.casecmp('ABC').zero?
21
21
  # 'abc'.casecmp(str).zero?
22
- class Casecmp < Cop
22
+ class Casecmp < Base
23
+ extend AutoCorrector
24
+
23
25
  MSG = 'Use `%<good>s` instead of `%<bad>s`.'
24
26
  CASE_METHODS = %i[downcase upcase].freeze
25
27
 
@@ -48,21 +50,13 @@ module RuboCop
48
50
  return unless downcase_eq(node) || eq_downcase(node)
49
51
  return unless (parts = take_method_apart(node))
50
52
 
51
- _, _, arg, variable = parts
53
+ _receiver, method, arg, variable = parts
52
54
  good_method = build_good_method(arg, variable)
53
55
 
54
- add_offense(
55
- node,
56
- message: format(MSG, good: good_method, bad: node.source)
57
- )
58
- end
59
-
60
- def autocorrect(node)
61
- return unless (parts = take_method_apart(node))
62
-
63
- receiver, method, arg, variable = parts
64
-
65
- correction(node, receiver, method, arg, variable)
56
+ message = format(MSG, good: good_method, bad: node.source)
57
+ add_offense(node, message: message) do |corrector|
58
+ correction(corrector, node, method, arg, variable)
59
+ end
66
60
  end
67
61
 
68
62
  private
@@ -84,14 +78,12 @@ module RuboCop
84
78
  [receiver, method, arg, variable]
85
79
  end
86
80
 
87
- def correction(node, _receiver, method, arg, variable)
88
- lambda do |corrector|
89
- corrector.insert_before(node.loc.expression, '!') if method == :!=
81
+ def correction(corrector, node, method, arg, variable)
82
+ corrector.insert_before(node.loc.expression, '!') if method == :!=
90
83
 
91
- replacement = build_good_method(arg, variable)
84
+ replacement = build_good_method(arg, variable)
92
85
 
93
- corrector.replace(node.loc.expression, replacement)
94
- end
86
+ corrector.replace(node.loc.expression, replacement)
95
87
  end
96
88
 
97
89
  def build_good_method(arg, variable)
@@ -20,7 +20,7 @@ module RuboCop
20
20
  # array.flatten!
21
21
  # array.map! { |x| x.downcase }
22
22
  # array
23
- class ChainArrayAllocation < Cop
23
+ class ChainArrayAllocation < Base
24
24
  include RangeHelp
25
25
 
26
26
  # These methods return a new array but only sometimes. They must be
@@ -61,15 +61,9 @@ module RuboCop
61
61
 
62
62
  def on_send(node)
63
63
  flat_map_candidate?(node) do |fm, sm, _|
64
- range = range_between(
65
- node.loc.dot.begin_pos,
66
- node.source_range.end_pos
67
- )
68
- add_offense(
69
- node,
70
- location: range,
71
- message: format(MSG, method: fm, second_method: sm)
72
- )
64
+ range = range_between(node.loc.dot.begin_pos, node.source_range.end_pos)
65
+
66
+ add_offense(range, message: format(MSG, method: fm, second_method: sm))
73
67
  end
74
68
  end
75
69
  end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Performance
8
+ # This cop identifies places where Array and Hash literals are used
9
+ # within loops. It is better to extract them into a local variable or constant
10
+ # to avoid unnecessary allocations on each iteration.
11
+ #
12
+ # You can set the minimum number of elements to consider
13
+ # an offense with `MinSize`.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # users.select do |user|
18
+ # %i[superadmin admin].include?(user.role)
19
+ # end
20
+ #
21
+ # # good
22
+ # admin_roles = %i[superadmin admin]
23
+ # users.select do |user|
24
+ # admin_roles.include?(user.role)
25
+ # end
26
+ #
27
+ # # good
28
+ # ADMIN_ROLES = %i[superadmin admin]
29
+ # ...
30
+ # users.select do |user|
31
+ # ADMIN_ROLES.include?(user.role)
32
+ # end
33
+ #
34
+ class CollectionLiteralInLoop < Base
35
+ MSG = 'Avoid immutable %<literal_class>s literals in loops. '\
36
+ 'It is better to extract it into a local variable or a constant.'
37
+
38
+ POST_CONDITION_LOOP_TYPES = %i[while_post until_post].freeze
39
+ LOOP_TYPES = (POST_CONDITION_LOOP_TYPES + %i[while until for]).freeze
40
+
41
+ ENUMERABLE_METHOD_NAMES = (Enumerable.instance_methods + [:each]).to_set.freeze
42
+ NONMUTATING_ARRAY_METHODS = %i[& * + - <=> == [] all? any? assoc at
43
+ bsearch bsearch_index collect combination
44
+ compact count cycle deconstruct difference dig
45
+ drop drop_while each each_index empty? eql?
46
+ fetch filter find_index first flatten hash
47
+ include? index inspect intersection join
48
+ last length map max min minmax none? one? pack
49
+ permutation product rassoc reject
50
+ repeated_combination repeated_permutation reverse
51
+ reverse_each rindex rotate sample select shuffle
52
+ size slice sort sum take take_while
53
+ to_a to_ary to_h to_s transpose union uniq
54
+ values_at zip |].freeze
55
+
56
+ ARRAY_METHODS = (ENUMERABLE_METHOD_NAMES | NONMUTATING_ARRAY_METHODS).to_set.freeze
57
+
58
+ NONMUTATING_HASH_METHODS = %i[< <= == > >= [] any? assoc compact dig
59
+ each each_key each_pair each_value empty?
60
+ eql? fetch fetch_values filter flatten has_key?
61
+ has_value? hash include? inspect invert key key?
62
+ keys? length member? merge rassoc rehash reject
63
+ select size slice to_a to_h to_hash to_proc to_s
64
+ transform_keys transform_values value? values values_at].freeze
65
+
66
+ HASH_METHODS = (ENUMERABLE_METHOD_NAMES | NONMUTATING_HASH_METHODS).to_set.freeze
67
+
68
+ def_node_matcher :kernel_loop?, <<~PATTERN
69
+ (block
70
+ (send {nil? (const nil? :Kernel)} :loop)
71
+ ...)
72
+ PATTERN
73
+
74
+ def_node_matcher :enumerable_loop?, <<~PATTERN
75
+ (block
76
+ (send $_ #enumerable_method? ...)
77
+ ...)
78
+ PATTERN
79
+
80
+ def on_send(node)
81
+ receiver, method, = *node.children
82
+ return unless check_literal?(receiver, method) && parent_is_loop?(receiver)
83
+
84
+ message = format(MSG, literal_class: literal_class(receiver))
85
+ add_offense(receiver, message: message)
86
+ end
87
+
88
+ private
89
+
90
+ def check_literal?(node, method)
91
+ !node.nil? &&
92
+ nonmutable_method_of_array_or_hash?(node, method) &&
93
+ node.children.size >= min_size &&
94
+ node.recursive_basic_literal?
95
+ end
96
+
97
+ def nonmutable_method_of_array_or_hash?(node, method)
98
+ (node.array_type? && ARRAY_METHODS.include?(method)) ||
99
+ (node.hash_type? && HASH_METHODS.include?(method))
100
+ end
101
+
102
+ def parent_is_loop?(node)
103
+ node.each_ancestor.any? { |ancestor| loop?(ancestor, node) }
104
+ end
105
+
106
+ def loop?(ancestor, node)
107
+ keyword_loop?(ancestor.type) ||
108
+ kernel_loop?(ancestor) ||
109
+ node_within_enumerable_loop?(node, ancestor)
110
+ end
111
+
112
+ def keyword_loop?(type)
113
+ LOOP_TYPES.include?(type)
114
+ end
115
+
116
+ def node_within_enumerable_loop?(node, ancestor)
117
+ enumerable_loop?(ancestor) do |receiver|
118
+ receiver != node && !receiver&.descendants&.include?(node)
119
+ end
120
+ end
121
+
122
+ def literal_class(node)
123
+ if node.array_type?
124
+ 'Array'
125
+ elsif node.hash_type?
126
+ 'Hash'
127
+ end
128
+ end
129
+
130
+ def enumerable_method?(method_name)
131
+ ENUMERABLE_METHOD_NAMES.include?(method_name)
132
+ end
133
+
134
+ def min_size
135
+ Integer(cop_config['MinSize'] || 1)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -23,8 +23,9 @@ module RuboCop
23
23
  # array.max_by(&:foo)
24
24
  # array.min_by(&:foo)
25
25
  # array.sort_by { |a| a[:foo] }
26
- class CompareWithBlock < Cop
26
+ class CompareWithBlock < Base
27
27
  include RangeHelp
28
+ extend AutoCorrector
28
29
 
29
30
  MSG = 'Use `%<compare_method>s_by%<instead>s` instead of ' \
30
31
  '`%<compare_method>s { |%<var_a>s, %<var_b>s| %<str_a>s ' \
@@ -51,27 +52,15 @@ module RuboCop
51
52
 
52
53
  range = compare_range(send, node)
53
54
 
54
- add_offense(
55
- node,
56
- location: range,
57
- message: message(send, method, var_a, var_b, args_a)
58
- )
59
- end
60
- end
61
- end
62
-
63
- def autocorrect(node)
64
- lambda do |corrector|
65
- send, var_a, var_b, body = compare?(node)
66
- method, arg, = replaceable_body?(body, var_a, var_b)
67
- replacement =
68
- if method == :[]
69
- "#{send.method_name}_by { |a| a[#{arg.first.source}] }"
70
- else
71
- "#{send.method_name}_by(&:#{method})"
55
+ add_offense(range, message: message(send, method, var_a, var_b, args_a)) do |corrector|
56
+ replacement = if method == :[]
57
+ "#{send.method_name}_by { |a| a[#{args_a.first.source}] }"
58
+ else
59
+ "#{send.method_name}_by(&:#{method})"
60
+ end
61
+ corrector.replace(range, replacement)
72
62
  end
73
- corrector.replace(compare_range(send, node),
74
- replacement)
63
+ end
75
64
  end
76
65
  end
77
66
 
@@ -4,7 +4,7 @@ module RuboCop
4
4
  module Cop
5
5
  module Performance
6
6
  # This cop is used to identify usages of `count` on an `Enumerable` that
7
- # follow calls to `select` or `reject`. Querying logic can instead be
7
+ # follow calls to `select`, `find_all`, `filter` or `reject`. Querying logic can instead be
8
8
  # passed to the `count` call.
9
9
  #
10
10
  # @example
@@ -37,15 +37,16 @@ module RuboCop
37
37
  # becomes:
38
38
  #
39
39
  # `Model.where(id: [1, 2, 3]).to_a.count { |m| m.method == true }`
40
- class Count < Cop
40
+ class Count < Base
41
41
  include RangeHelp
42
+ extend AutoCorrector
42
43
 
43
44
  MSG = 'Use `count` instead of `%<selector>s...%<counter>s`.'
44
45
 
45
46
  def_node_matcher :count_candidate?, <<~PATTERN
46
47
  {
47
- (send (block $(send _ ${:select :reject}) ...) ${:count :length :size})
48
- (send $(send _ ${:select :reject} (:block_pass _)) ${:count :length :size})
48
+ (send (block $(send _ ${:select :filter :find_all :reject}) ...) ${:count :length :size})
49
+ (send $(send _ ${:select :filter :find_all :reject} (:block_pass _)) ${:count :length :size})
49
50
  }
50
51
  PATTERN
51
52
 
@@ -57,29 +58,25 @@ module RuboCop
57
58
  selector_node.loc.selector.begin_pos
58
59
  end
59
60
 
60
- add_offense(node,
61
- location: range,
62
- message: format(MSG, selector: selector,
63
- counter: counter))
61
+ add_offense(range, message: format(MSG, selector: selector, counter: counter)) do |corrector|
62
+ autocorrect(corrector, node, selector_node, selector)
63
+ end
64
64
  end
65
65
  end
66
66
 
67
- def autocorrect(node)
68
- selector_node, selector, _counter = count_candidate?(node)
67
+ private
68
+
69
+ def autocorrect(corrector, node, selector_node, selector)
69
70
  selector_loc = selector_node.loc.selector
70
71
 
71
72
  return if selector == :reject
72
73
 
73
74
  range = source_starting_at(node) { |n| n.loc.dot.begin_pos }
74
75
 
75
- lambda do |corrector|
76
- corrector.remove(range)
77
- corrector.replace(selector_loc, 'count')
78
- end
76
+ corrector.remove(range)
77
+ corrector.replace(selector_loc, 'count')
79
78
  end
80
79
 
81
- private
82
-
83
80
  def eligible_node?(node)
84
81
  !(node.parent && node.parent.block_type?)
85
82
  end
@@ -5,27 +5,48 @@ module RuboCop
5
5
  module Performance
6
6
  # In Ruby 2.5, `String#delete_prefix` has been added.
7
7
  #
8
- # This cop identifies places where `gsub(/\Aprefix/, '')`
8
+ # This cop identifies places where `gsub(/\Aprefix/, '')` and `sub(/\Aprefix/, '')`
9
9
  # can be replaced by `delete_prefix('prefix')`.
10
10
  #
11
- # The `delete_prefix('prefix')` method is faster than
12
- # `gsub(/\Aprefix/, '')`.
11
+ # This cop has `SafeMultiline` configuration option that `true` by default because
12
+ # `^prefix` is unsafe as it will behave incompatible with `delete_prefix`
13
+ # for receiver is multiline string.
14
+ #
15
+ # The `delete_prefix('prefix')` method is faster than `gsub(/\Aprefix/, '')`.
13
16
  #
14
17
  # @example
15
18
  #
16
19
  # # bad
17
20
  # str.gsub(/\Aprefix/, '')
18
21
  # str.gsub!(/\Aprefix/, '')
19
- # str.gsub(/^prefix/, '')
20
- # str.gsub!(/^prefix/, '')
22
+ #
23
+ # str.sub(/\Aprefix/, '')
24
+ # str.sub!(/\Aprefix/, '')
21
25
  #
22
26
  # # good
23
27
  # str.delete_prefix('prefix')
24
28
  # str.delete_prefix!('prefix')
25
29
  #
26
- class DeletePrefix < Cop
27
- extend TargetRubyVersion
30
+ # @example SafeMultiline: true (default)
31
+ #
32
+ # # good
33
+ # str.gsub(/^prefix/, '')
34
+ # str.gsub!(/^prefix/, '')
35
+ # str.sub(/^prefix/, '')
36
+ # str.sub!(/^prefix/, '')
37
+ #
38
+ # @example SafeMultiline: false
39
+ #
40
+ # # bad
41
+ # str.gsub(/^prefix/, '')
42
+ # str.gsub!(/^prefix/, '')
43
+ # str.sub(/^prefix/, '')
44
+ # str.sub!(/^prefix/, '')
45
+ #
46
+ class DeletePrefix < Base
28
47
  include RegexpMetacharacter
48
+ extend AutoCorrector
49
+ extend TargetRubyVersion
29
50
 
30
51
  minimum_target_ruby_version 2.5
31
52
 
@@ -33,37 +54,31 @@ module RuboCop
33
54
 
34
55
  PREFERRED_METHODS = {
35
56
  gsub: :delete_prefix,
36
- gsub!: :delete_prefix!
57
+ gsub!: :delete_prefix!,
58
+ sub: :delete_prefix,
59
+ sub!: :delete_prefix!
37
60
  }.freeze
38
61
 
39
- def_node_matcher :gsub_method?, <<~PATTERN
40
- (send $!nil? ${:gsub :gsub!} (regexp (str $#literal_at_start?) (regopt)) (str $_))
62
+ def_node_matcher :delete_prefix_candidate?, <<~PATTERN
63
+ (send $!nil? ${:gsub :gsub! :sub :sub!} (regexp (str $#literal_at_start?) (regopt)) (str $_))
41
64
  PATTERN
42
65
 
43
66
  def on_send(node)
44
- gsub_method?(node) do |_, bad_method, _, replace_string|
45
- return unless replace_string.blank?
46
-
47
- good_method = PREFERRED_METHODS[bad_method]
67
+ return unless (receiver, bad_method, regexp_str, replace_string = delete_prefix_candidate?(node))
68
+ return unless replace_string.blank?
48
69
 
49
- message = format(MSG, current: bad_method, prefer: good_method)
70
+ good_method = PREFERRED_METHODS[bad_method]
50
71
 
51
- add_offense(node, location: :selector, message: message)
52
- end
53
- end
72
+ message = format(MSG, current: bad_method, prefer: good_method)
54
73
 
55
- def autocorrect(node)
56
- gsub_method?(node) do |receiver, bad_method, regexp_str, _|
57
- lambda do |corrector|
58
- good_method = PREFERRED_METHODS[bad_method]
59
- regexp_str = drop_start_metacharacter(regexp_str)
60
- regexp_str = interpret_string_escapes(regexp_str)
61
- string_literal = to_string_literal(regexp_str)
74
+ add_offense(node.loc.selector, message: message) do |corrector|
75
+ regexp_str = drop_start_metacharacter(regexp_str)
76
+ regexp_str = interpret_string_escapes(regexp_str)
77
+ string_literal = to_string_literal(regexp_str)
62
78
 
63
- new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
79
+ new_code = "#{receiver.source}.#{good_method}(#{string_literal})"
64
80
 
65
- corrector.replace(node, new_code)
66
- end
81
+ corrector.replace(node, new_code)
67
82
  end
68
83
  end
69
84
  end