rubocop-performance 1.5.2 → 1.8.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # In Ruby 2.7, `UnboundMethod#bind_call` has been added.
7
+ #
8
+ # This cop identifies places where `bind(obj).call(args, ...)`
9
+ # can be replaced by `bind_call(obj, args, ...)`.
10
+ #
11
+ # The `bind_call(obj, args, ...)` method is faster than
12
+ # `bind(obj).call(args, ...)`.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # umethod.bind(obj).call(foo, bar)
17
+ # umethod.bind(obj).(foo, bar)
18
+ #
19
+ # # good
20
+ # umethod.bind_call(obj, foo, bar)
21
+ #
22
+ class BindCall < Base
23
+ include RangeHelp
24
+ extend AutoCorrector
25
+ extend TargetRubyVersion
26
+
27
+ minimum_target_ruby_version 2.7
28
+
29
+ MSG = 'Use `bind_call(%<bind_arg>s%<comma>s%<call_args>s)` ' \
30
+ 'instead of `bind(%<bind_arg>s).call(%<call_args>s)`.'
31
+
32
+ def_node_matcher :bind_with_call_method?, <<~PATTERN
33
+ (send
34
+ $(send
35
+ (send nil? _) :bind
36
+ $(...)) :call
37
+ $...)
38
+ PATTERN
39
+
40
+ def on_send(node)
41
+ return unless (receiver, bind_arg, call_args_node = bind_with_call_method?(node))
42
+
43
+ range = correction_range(receiver, node)
44
+ call_args = build_call_args(call_args_node)
45
+ message = message(bind_arg.source, call_args)
46
+
47
+ add_offense(range, message: message) do |corrector|
48
+ call_args = ", #{call_args}" unless call_args.empty?
49
+
50
+ replacement_method = "bind_call(#{bind_arg.source}#{call_args})"
51
+
52
+ corrector.replace(range, replacement_method)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def message(bind_arg, call_args)
59
+ comma = call_args.empty? ? '' : ', '
60
+
61
+ format(MSG, bind_arg: bind_arg, comma: comma, call_args: call_args)
62
+ end
63
+
64
+ def correction_range(receiver, node)
65
+ location_of_bind = receiver.loc.selector.begin_pos
66
+ location_of_call = node.loc.end.end_pos
67
+
68
+ range_between(location_of_bind, location_of_call)
69
+ end
70
+
71
+ def build_call_args(call_args_node)
72
+ call_args_node.map(&:source).join(', ')
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -18,20 +18,20 @@ 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`' \
25
25
  ' instead of `%<method>s.first`.'
26
26
 
27
- def_node_matcher :slow_caller?, <<-PATTERN
27
+ def_node_matcher :slow_caller?, <<~PATTERN
28
28
  {
29
29
  (send nil? {:caller :caller_locations})
30
30
  (send nil? {:caller :caller_locations} int)
31
31
  }
32
32
  PATTERN
33
33
 
34
- def_node_matcher :caller_with_scope_method?, <<-PATTERN
34
+ def_node_matcher :caller_with_scope_method?, <<~PATTERN
35
35
  {
36
36
  (send #slow_caller? :first)
37
37
  (send #slow_caller? :[] int)
@@ -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
@@ -5,6 +5,8 @@ module RuboCop
5
5
  module Performance
6
6
  # This cop identifies places where a case-insensitive string comparison
7
7
  # can better be implemented using `casecmp`.
8
+ # This cop is unsafe because `String#casecmp` and `String#casecmp?` behave
9
+ # differently when using Non-ASCII characters.
8
10
  #
9
11
  # @example
10
12
  # # bad
@@ -17,25 +19,27 @@ module RuboCop
17
19
  # # good
18
20
  # str.casecmp('ABC').zero?
19
21
  # 'abc'.casecmp(str).zero?
20
- class Casecmp < Cop
22
+ class Casecmp < Base
23
+ extend AutoCorrector
24
+
21
25
  MSG = 'Use `%<good>s` instead of `%<bad>s`.'
22
26
  CASE_METHODS = %i[downcase upcase].freeze
23
27
 
24
- def_node_matcher :downcase_eq, <<-PATTERN
28
+ def_node_matcher :downcase_eq, <<~PATTERN
25
29
  (send
26
30
  $(send _ ${:downcase :upcase})
27
31
  ${:== :eql? :!=}
28
32
  ${str (send _ {:downcase :upcase} ...) (begin str)})
29
33
  PATTERN
30
34
 
31
- def_node_matcher :eq_downcase, <<-PATTERN
35
+ def_node_matcher :eq_downcase, <<~PATTERN
32
36
  (send
33
37
  {str (send _ {:downcase :upcase} ...) (begin str)}
34
38
  ${:== :eql? :!=}
35
39
  $(send _ ${:downcase :upcase}))
36
40
  PATTERN
37
41
 
38
- def_node_matcher :downcase_downcase, <<-PATTERN
42
+ def_node_matcher :downcase_downcase, <<~PATTERN
39
43
  (send
40
44
  $(send _ ${:downcase :upcase})
41
45
  ${:== :eql? :!=}
@@ -46,21 +50,13 @@ module RuboCop
46
50
  return unless downcase_eq(node) || eq_downcase(node)
47
51
  return unless (parts = take_method_apart(node))
48
52
 
49
- _, _, arg, variable = parts
53
+ _receiver, method, arg, variable = parts
50
54
  good_method = build_good_method(arg, variable)
51
55
 
52
- add_offense(
53
- node,
54
- message: format(MSG, good: good_method, bad: node.source)
55
- )
56
- end
57
-
58
- def autocorrect(node)
59
- return unless (parts = take_method_apart(node))
60
-
61
- receiver, method, arg, variable = parts
62
-
63
- 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
64
60
  end
65
61
 
66
62
  private
@@ -82,14 +78,12 @@ module RuboCop
82
78
  [receiver, method, arg, variable]
83
79
  end
84
80
 
85
- def correction(node, _receiver, method, arg, variable)
86
- lambda do |corrector|
87
- 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 == :!=
88
83
 
89
- replacement = build_good_method(arg, variable)
84
+ replacement = build_good_method(arg, variable)
90
85
 
91
- corrector.replace(node.loc.expression, replacement)
92
- end
86
+ corrector.replace(node.loc.expression, replacement)
93
87
  end
94
88
 
95
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
@@ -51,7 +51,7 @@ module RuboCop
51
51
  '(followed by `return array` if required) instead of chaining '\
52
52
  '`%<method>s...%<second_method>s`.'
53
53
 
54
- def_node_matcher :flat_map_candidate?, <<-PATTERN
54
+ def_node_matcher :flat_map_candidate?, <<~PATTERN
55
55
  {
56
56
  (send (send _ ${#{RETURN_NEW_ARRAY_WHEN_ARGS}} {int lvar ivar cvar gvar}) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
57
57
  (send (block (send _ ${#{ALWAYS_RETURNS_NEW_ARRAY} }) ...) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
@@ -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,21 +23,22 @@ 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 ' \
31
32
  '<=> %<str_b>s }`.'
32
33
 
33
- def_node_matcher :compare?, <<-PATTERN
34
+ def_node_matcher :compare?, <<~PATTERN
34
35
  (block
35
36
  $(send _ {:sort :min :max})
36
37
  (args (arg $_a) (arg $_b))
37
38
  $send)
38
39
  PATTERN
39
40
 
40
- def_node_matcher :replaceable_body?, <<-PATTERN
41
+ def_node_matcher :replaceable_body?, <<~PATTERN
41
42
  (send
42
43
  (send (lvar %1) $_method $...)
43
44
  :<=>
@@ -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