rubocop-performance 1.6.1 → 1.9.0

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/config/default.yml +95 -8
  4. data/lib/rubocop/cop/mixin/regexp_metacharacter.rb +4 -4
  5. data/lib/rubocop/cop/mixin/sort_block.rb +28 -0
  6. data/lib/rubocop/cop/performance/ancestors_include.rb +49 -0
  7. data/lib/rubocop/cop/performance/array_semi_infinite_range_slice.rb +74 -0
  8. data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +46 -0
  9. data/lib/rubocop/cop/performance/bind_call.rb +9 -18
  10. data/lib/rubocop/cop/performance/block_given_with_explicit_block.rb +52 -0
  11. data/lib/rubocop/cop/performance/caller.rb +14 -15
  12. data/lib/rubocop/cop/performance/case_when_splat.rb +18 -11
  13. data/lib/rubocop/cop/performance/casecmp.rb +13 -20
  14. data/lib/rubocop/cop/performance/chain_array_allocation.rb +4 -10
  15. data/lib/rubocop/cop/performance/collection_literal_in_loop.rb +140 -0
  16. data/lib/rubocop/cop/performance/compare_with_block.rb +10 -21
  17. data/lib/rubocop/cop/performance/constant_regexp.rb +68 -0
  18. data/lib/rubocop/cop/performance/count.rb +14 -16
  19. data/lib/rubocop/cop/performance/delete_prefix.rb +14 -22
  20. data/lib/rubocop/cop/performance/delete_suffix.rb +14 -22
  21. data/lib/rubocop/cop/performance/detect.rb +65 -32
  22. data/lib/rubocop/cop/performance/double_start_end_with.rb +16 -24
  23. data/lib/rubocop/cop/performance/end_with.rb +9 -13
  24. data/lib/rubocop/cop/performance/fixed_size.rb +2 -1
  25. data/lib/rubocop/cop/performance/flat_map.rb +21 -22
  26. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +15 -14
  27. data/lib/rubocop/cop/performance/io_readlines.rb +112 -0
  28. data/lib/rubocop/cop/performance/method_object_as_block.rb +32 -0
  29. data/lib/rubocop/cop/performance/open_struct.rb +3 -2
  30. data/lib/rubocop/cop/performance/range_include.rb +15 -11
  31. data/lib/rubocop/cop/performance/redundant_block_call.rb +15 -10
  32. data/lib/rubocop/cop/performance/redundant_match.rb +12 -6
  33. data/lib/rubocop/cop/performance/redundant_merge.rb +19 -17
  34. data/lib/rubocop/cop/performance/redundant_sort_block.rb +43 -0
  35. data/lib/rubocop/cop/performance/redundant_string_chars.rb +129 -0
  36. data/lib/rubocop/cop/performance/regexp_match.rb +20 -20
  37. data/lib/rubocop/cop/performance/reverse_each.rb +10 -5
  38. data/lib/rubocop/cop/performance/reverse_first.rb +73 -0
  39. data/lib/rubocop/cop/performance/size.rb +42 -43
  40. data/lib/rubocop/cop/performance/sort_reverse.rb +45 -0
  41. data/lib/rubocop/cop/performance/squeeze.rb +67 -0
  42. data/lib/rubocop/cop/performance/start_with.rb +9 -13
  43. data/lib/rubocop/cop/performance/string_include.rb +56 -0
  44. data/lib/rubocop/cop/performance/string_replacement.rb +24 -27
  45. data/lib/rubocop/cop/performance/sum.rb +236 -0
  46. data/lib/rubocop/cop/performance/times_map.rb +12 -18
  47. data/lib/rubocop/cop/performance/unfreeze_string.rb +20 -2
  48. data/lib/rubocop/cop/performance/uri_default_parser.rb +7 -12
  49. data/lib/rubocop/cop/performance_cops.rb +16 -0
  50. data/lib/rubocop/performance/version.rb +6 -1
  51. metadata +35 -13
@@ -19,14 +19,16 @@ module RuboCop
19
19
  # # good
20
20
  # umethod.bind_call(obj, foo, bar)
21
21
  #
22
- class BindCall < Cop
22
+ class BindCall < Base
23
23
  include RangeHelp
24
+ extend AutoCorrector
24
25
  extend TargetRubyVersion
25
26
 
26
27
  minimum_target_ruby_version 2.7
27
28
 
28
29
  MSG = 'Use `bind_call(%<bind_arg>s%<comma>s%<call_args>s)` ' \
29
30
  'instead of `bind(%<bind_arg>s).call(%<call_args>s)`.'
31
+ RESTRICT_ON_SEND = %i[call].freeze
30
32
 
31
33
  def_node_matcher :bind_with_call_method?, <<~PATTERN
32
34
  (send
@@ -37,28 +39,17 @@ module RuboCop
37
39
  PATTERN
38
40
 
39
41
  def on_send(node)
40
- bind_with_call_method?(node) do |receiver, bind_arg, call_args_node|
41
- range = correction_range(receiver, node)
42
-
43
- call_args = build_call_args(call_args_node)
44
-
45
- message = message(bind_arg.source, call_args)
46
-
47
- add_offense(node, location: range, message: message)
48
- end
49
- end
50
-
51
- def autocorrect(node)
52
- receiver, bind_arg, call_args_node = bind_with_call_method?(node)
42
+ return unless (receiver, bind_arg, call_args_node = bind_with_call_method?(node))
53
43
 
54
44
  range = correction_range(receiver, node)
55
-
56
45
  call_args = build_call_args(call_args_node)
57
- call_args = ", #{call_args}" unless call_args.empty?
46
+ message = message(bind_arg.source, call_args)
47
+
48
+ add_offense(range, message: message) do |corrector|
49
+ call_args = ", #{call_args}" unless call_args.empty?
58
50
 
59
- replacement_method = "bind_call(#{bind_arg.source}#{call_args})"
51
+ replacement_method = "bind_call(#{bind_arg.source}#{call_args})"
60
52
 
61
- lambda do |corrector|
62
53
  corrector.replace(range, replacement_method)
63
54
  end
64
55
  end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop identifies unnecessary use of a `block_given?` where explicit check
7
+ # of block argument would suffice.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # def method(&block)
12
+ # do_something if block_given?
13
+ # end
14
+ #
15
+ # # good
16
+ # def method(&block)
17
+ # do_something if block
18
+ # end
19
+ #
20
+ # # good - block is reassigned
21
+ # def method(&block)
22
+ # block ||= -> { do_something }
23
+ # warn "Using default ..." unless block_given?
24
+ # # ...
25
+ # end
26
+ #
27
+ class BlockGivenWithExplicitBlock < Base
28
+ extend AutoCorrector
29
+
30
+ RESTRICT_ON_SEND = %i[block_given?].freeze
31
+ MSG = 'Check block argument explicitly instead of using `block_given?`.'
32
+
33
+ def_node_matcher :reassigns_block_arg?, '`(lvasgn %1 ...)'
34
+
35
+ def on_send(node)
36
+ def_node = node.each_ancestor(:def, :defs).first
37
+ return unless def_node
38
+
39
+ block_arg = def_node.arguments.find(&:blockarg_type?)
40
+ return unless block_arg
41
+
42
+ block_arg_name = block_arg.loc.name.source.to_sym
43
+ return if reassigns_block_arg?(def_node, block_arg_name)
44
+
45
+ add_offense(node) do |corrector|
46
+ corrector.replace(node, block_arg_name)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -18,11 +18,11 @@ 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
22
- MSG_BRACE = 'Use `%<method>s(%<n>d..%<n>d).first`' \
23
- ' instead of `%<method>s[%<m>d]`.'
24
- MSG_FIRST = 'Use `%<method>s(%<n>d..%<n>d).first`' \
25
- ' instead of `%<method>s.first`.'
21
+ class Caller < Base
22
+ extend AutoCorrector
23
+
24
+ MSG = 'Use `%<preferred_method>s` instead of `%<current_method>s`.'
25
+ RESTRICT_ON_SEND = %i[first []].freeze
26
26
 
27
27
  def_node_matcher :slow_caller?, <<~PATTERN
28
28
  {
@@ -41,25 +41,24 @@ module RuboCop
41
41
  def on_send(node)
42
42
  return unless caller_with_scope_method?(node)
43
43
 
44
- add_offense(node)
45
- end
46
-
47
- private
48
-
49
- def message(node)
50
44
  method_name = node.receiver.method_name
51
45
  caller_arg = node.receiver.first_argument
52
46
  n = caller_arg ? int_value(caller_arg) : 1
53
-
54
47
  if node.method?(:[])
55
48
  m = int_value(node.first_argument)
56
49
  n += m
57
- format(MSG_BRACE, n: n, m: m, method: method_name)
58
- else
59
- format(MSG_FIRST, n: n, method: method_name)
50
+ end
51
+
52
+ preferred_method = "#{method_name}(#{n}..#{n}).first"
53
+
54
+ message = format(MSG, preferred_method: preferred_method, current_method: node.source)
55
+ add_offense(node, message: message) do |corrector|
56
+ corrector.replace(node, preferred_method)
60
57
  end
61
58
  end
62
59
 
60
+ private
61
+
63
62
  def int_value(node)
64
63
  node.children[0]
65
64
  end
@@ -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,8 +19,11 @@ 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`.'
26
+ RESTRICT_ON_SEND = %i[== eql? !=].freeze
24
27
  CASE_METHODS = %i[downcase upcase].freeze
25
28
 
26
29
  def_node_matcher :downcase_eq, <<~PATTERN
@@ -48,21 +51,13 @@ module RuboCop
48
51
  return unless downcase_eq(node) || eq_downcase(node)
49
52
  return unless (parts = take_method_apart(node))
50
53
 
51
- _, _, arg, variable = parts
54
+ _receiver, method, arg, variable = parts
52
55
  good_method = build_good_method(arg, variable)
53
56
 
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)
57
+ message = format(MSG, good: good_method, bad: node.source)
58
+ add_offense(node, message: message) do |corrector|
59
+ correction(corrector, node, method, arg, variable)
60
+ end
66
61
  end
67
62
 
68
63
  private
@@ -84,14 +79,12 @@ module RuboCop
84
79
  [receiver, method, arg, variable]
85
80
  end
86
81
 
87
- def correction(node, _receiver, method, arg, variable)
88
- lambda do |corrector|
89
- corrector.insert_before(node.loc.expression, '!') if method == :!=
82
+ def correction(corrector, node, method, arg, variable)
83
+ corrector.insert_before(node.loc.expression, '!') if method == :!=
90
84
 
91
- replacement = build_good_method(arg, variable)
85
+ replacement = build_good_method(arg, variable)
92
86
 
93
- corrector.replace(node.loc.expression, replacement)
94
- end
87
+ corrector.replace(node.loc.expression, replacement)
95
88
  end
96
89
 
97
90
  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