rubocop-performance 1.8.0 → 1.10.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +10 -2
  4. data/config/default.yml +47 -6
  5. data/lib/rubocop/cop/mixin/regexp_metacharacter.rb +4 -4
  6. data/lib/rubocop/cop/performance/ancestors_include.rb +1 -0
  7. data/lib/rubocop/cop/performance/array_semi_infinite_range_slice.rb +77 -0
  8. data/lib/rubocop/cop/performance/big_decimal_with_numeric_argument.rb +1 -0
  9. data/lib/rubocop/cop/performance/bind_call.rb +3 -2
  10. data/lib/rubocop/cop/performance/block_given_with_explicit_block.rb +52 -0
  11. data/lib/rubocop/cop/performance/caller.rb +13 -15
  12. data/lib/rubocop/cop/performance/casecmp.rb +1 -0
  13. data/lib/rubocop/cop/performance/chain_array_allocation.rb +20 -18
  14. data/lib/rubocop/cop/performance/collection_literal_in_loop.rb +1 -1
  15. data/lib/rubocop/cop/performance/constant_regexp.rb +73 -0
  16. data/lib/rubocop/cop/performance/count.rb +1 -0
  17. data/lib/rubocop/cop/performance/delete_prefix.rb +1 -0
  18. data/lib/rubocop/cop/performance/delete_suffix.rb +1 -0
  19. data/lib/rubocop/cop/performance/detect.rb +47 -17
  20. data/lib/rubocop/cop/performance/end_with.rb +1 -0
  21. data/lib/rubocop/cop/performance/fixed_size.rb +1 -0
  22. data/lib/rubocop/cop/performance/flat_map.rb +1 -0
  23. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +2 -0
  24. data/lib/rubocop/cop/performance/io_readlines.rb +3 -7
  25. data/lib/rubocop/cop/performance/method_object_as_block.rb +32 -0
  26. data/lib/rubocop/cop/performance/open_struct.rb +1 -0
  27. data/lib/rubocop/cop/performance/range_include.rb +1 -0
  28. data/lib/rubocop/cop/performance/redundant_block_call.rb +4 -4
  29. data/lib/rubocop/cop/performance/redundant_equality_comparison_block.rb +72 -0
  30. data/lib/rubocop/cop/performance/redundant_match.rb +1 -0
  31. data/lib/rubocop/cop/performance/redundant_merge.rb +1 -0
  32. data/lib/rubocop/cop/performance/redundant_split_regexp_argument.rb +67 -0
  33. data/lib/rubocop/cop/performance/redundant_string_chars.rb +2 -6
  34. data/lib/rubocop/cop/performance/reverse_each.rb +7 -10
  35. data/lib/rubocop/cop/performance/reverse_first.rb +1 -0
  36. data/lib/rubocop/cop/performance/size.rb +1 -0
  37. data/lib/rubocop/cop/performance/squeeze.rb +2 -1
  38. data/lib/rubocop/cop/performance/start_with.rb +1 -0
  39. data/lib/rubocop/cop/performance/string_include.rb +2 -1
  40. data/lib/rubocop/cop/performance/string_replacement.rb +1 -0
  41. data/lib/rubocop/cop/performance/sum.rb +131 -18
  42. data/lib/rubocop/cop/performance/times_map.rb +1 -0
  43. data/lib/rubocop/cop/performance/unfreeze_string.rb +19 -1
  44. data/lib/rubocop/cop/performance/uri_default_parser.rb +1 -0
  45. data/lib/rubocop/cop/performance_cops.rb +6 -0
  46. data/lib/rubocop/performance/version.rb +6 -1
  47. metadata +29 -17
@@ -22,6 +22,7 @@ module RuboCop
22
22
 
23
23
  MSG = 'Use `=~` in places where the `MatchData` returned by ' \
24
24
  '`#match` will not be used.'
25
+ RESTRICT_ON_SEND = %i[match].freeze
25
26
 
26
27
  # 'match' is a fairly generic name, so we don't flag it unless we see
27
28
  # a string or regexp literal on one side or the other
@@ -29,6 +29,7 @@ module RuboCop
29
29
 
30
30
  AREF_ASGN = '%<receiver>s[%<key>s] = %<value>s'
31
31
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
32
+ RESTRICT_ON_SEND = %i[merge!].freeze
32
33
 
33
34
  WITH_MODIFIER_CORRECTION = <<~RUBY
34
35
  %<keyword>s %<condition>s
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop identifies places where `split` argument can be replaced from
7
+ # a deterministic regexp to a string.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # 'a,b,c'.split(/,/)
12
+ #
13
+ # # good
14
+ # 'a,b,c'.split(',')
15
+ class RedundantSplitRegexpArgument < Base
16
+ extend AutoCorrector
17
+
18
+ MSG = 'Use string as argument instead of regexp.'
19
+ RESTRICT_ON_SEND = %i[split].freeze
20
+ DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/.freeze
21
+ STR_SPECIAL_CHARS = %w[\n \" \' \\\\ \t \b \f \r].freeze
22
+
23
+ def_node_matcher :split_call_with_regexp?, <<~PATTERN
24
+ {(send !nil? :split {regexp})}
25
+ PATTERN
26
+
27
+ def on_send(node)
28
+ return unless split_call_with_regexp?(node)
29
+ return unless determinist_regexp?(node.first_argument)
30
+
31
+ add_offense(node.first_argument) do |corrector|
32
+ autocorrect(corrector, node)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def determinist_regexp?(first_argument)
39
+ DETERMINISTIC_REGEX.match?(first_argument.source)
40
+ end
41
+
42
+ def autocorrect(corrector, node)
43
+ new_argument = replacement(node)
44
+
45
+ corrector.replace(node.first_argument, "\"#{new_argument}\"")
46
+ end
47
+
48
+ def replacement(node)
49
+ regexp_content = node.first_argument.content
50
+ stack = []
51
+ chars = regexp_content.chars.each_with_object([]) do |char, strings|
52
+ if stack.empty? && char == '\\'
53
+ stack.push(char)
54
+ else
55
+ strings << "#{stack.pop}#{char}"
56
+ end
57
+ end
58
+ chars.map do |char|
59
+ char = char.dup
60
+ char.delete!('\\') unless STR_SPECIAL_CHARS.include?(char)
61
+ char
62
+ end.join
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -44,10 +44,10 @@ module RuboCop
44
44
  extend AutoCorrector
45
45
 
46
46
  MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
47
- REPLACEABLE_METHODS = %i[[] slice first last take drop length size empty?].freeze
47
+ RESTRICT_ON_SEND = %i[[] slice first last take drop length size empty?].freeze
48
48
 
49
49
  def_node_matcher :redundant_chars_call?, <<~PATTERN
50
- (send $(send _ :chars) $#replaceable_method? $...)
50
+ (send $(send _ :chars) $_ $...)
51
51
  PATTERN
52
52
 
53
53
  def on_send(node)
@@ -66,10 +66,6 @@ module RuboCop
66
66
 
67
67
  private
68
68
 
69
- def replaceable_method?(method_name)
70
- REPLACEABLE_METHODS.include?(method_name)
71
- end
72
-
73
69
  def offense_range(receiver, node)
74
70
  range_between(receiver.loc.selector.begin_pos, node.loc.expression.end_pos)
75
71
  end
@@ -17,29 +17,26 @@ module RuboCop
17
17
  extend AutoCorrector
18
18
 
19
19
  MSG = 'Use `reverse_each` instead of `reverse.each`.'
20
- UNDERSCORE = '_'
20
+ RESTRICT_ON_SEND = %i[each].freeze
21
21
 
22
22
  def_node_matcher :reverse_each?, <<~MATCHER
23
- (send $(send _ :reverse) :each)
23
+ (send (send _ :reverse) :each)
24
24
  MATCHER
25
25
 
26
26
  def on_send(node)
27
- reverse_each?(node) do |receiver|
28
- location_of_reverse = receiver.loc.selector.begin_pos
29
- end_location = node.loc.selector.end_pos
30
-
31
- range = range_between(location_of_reverse, end_location)
27
+ reverse_each?(node) do
28
+ range = offense_range(node)
32
29
 
33
30
  add_offense(range) do |corrector|
34
- corrector.replace(replacement_range(node), UNDERSCORE)
31
+ corrector.replace(range, 'reverse_each')
35
32
  end
36
33
  end
37
34
  end
38
35
 
39
36
  private
40
37
 
41
- def replacement_range(node)
42
- range_between(node.loc.dot.begin_pos, node.loc.selector.begin_pos)
38
+ def offense_range(node)
39
+ range_between(node.children.first.loc.selector.begin_pos, node.loc.selector.end_pos)
43
40
  end
44
41
  end
45
42
  end
@@ -21,6 +21,7 @@ module RuboCop
21
21
  extend AutoCorrector
22
22
 
23
23
  MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
24
+ RESTRICT_ON_SEND = %i[first].freeze
24
25
 
25
26
  def_node_matcher :reverse_first_candidate?, <<~PATTERN
26
27
  (send $(send _ :reverse) :first (int _)?)
@@ -39,6 +39,7 @@ module RuboCop
39
39
  extend AutoCorrector
40
40
 
41
41
  MSG = 'Use `size` instead of `count`.'
42
+ RESTRICT_ON_SEND = %i[count].freeze
42
43
 
43
44
  def_node_matcher :array?, <<~PATTERN
44
45
  {
@@ -22,6 +22,7 @@ module RuboCop
22
22
  extend AutoCorrector
23
23
 
24
24
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
25
+ RESTRICT_ON_SEND = %i[gsub gsub!].freeze
25
26
 
26
27
  PREFERRED_METHODS = {
27
28
  gsub: :squeeze,
@@ -58,7 +59,7 @@ module RuboCop
58
59
  private
59
60
 
60
61
  def repeating_literal?(regex_str)
61
- regex_str.match?(/\A(?:#{Util::LITERAL_REGEX})\+\z/)
62
+ regex_str.match?(/\A(?:#{Util::LITERAL_REGEX})\+\z/o)
62
63
  end
63
64
  end
64
65
  end
@@ -47,6 +47,7 @@ module RuboCop
47
47
 
48
48
  MSG = 'Use `String#start_with?` instead of a regex match anchored to ' \
49
49
  'the beginning of the string.'
50
+ RESTRICT_ON_SEND = %i[match =~ match?].freeze
50
51
 
51
52
  def_node_matcher :redundant_regex?, <<~PATTERN
52
53
  {(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_start?) (regopt)))
@@ -23,6 +23,7 @@ module RuboCop
23
23
  extend AutoCorrector
24
24
 
25
25
  MSG = 'Use `String#include?` instead of a regex match with literal-only pattern.'
26
+ RESTRICT_ON_SEND = %i[match =~ match?].freeze
26
27
 
27
28
  def_node_matcher :redundant_regex?, <<~PATTERN
28
29
  {(send $!nil? {:match :=~ :match?} (regexp (str $#literal?) (regopt)))
@@ -47,7 +48,7 @@ module RuboCop
47
48
  private
48
49
 
49
50
  def literal?(regex_str)
50
- regex_str.match?(/\A#{Util::LITERAL_REGEX}+\z/)
51
+ regex_str.match?(/\A#{Util::LITERAL_REGEX}+\z/o)
51
52
  end
52
53
  end
53
54
  end
@@ -23,6 +23,7 @@ module RuboCop
23
23
  extend AutoCorrector
24
24
 
25
25
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
26
+ RESTRICT_ON_SEND = %i[gsub gsub!].freeze
26
27
  DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/.freeze
27
28
  DELETE = 'delete'
28
29
  TR = 'tr'
@@ -6,25 +6,65 @@ module RuboCop
6
6
  # This cop identifies places where custom code finding the sum of elements
7
7
  # in some Enumerable object can be replaced by `Enumerable#sum` method.
8
8
  #
9
+ # This cop can change auto-correction scope depending on the value of
10
+ # `SafeAutoCorrect`.
11
+ # Its auto-correction is marked as safe by default (`SafeAutoCorrect: true`)
12
+ # to prevent `TypeError` in auto-correced code when initial value is not
13
+ # specified as shown below:
14
+ #
15
+ # [source,ruby]
16
+ # ----
17
+ # ['a', 'b'].sum # => (String can't be coerced into Integer)
18
+ # ----
19
+ #
20
+ # Therefore if initial value is not specified, unsafe auto-corrected will not occur.
21
+ #
22
+ # If you always want to enable auto-correction, you can set `SafeAutoCorrect: false`.
23
+ #
24
+ # [source,yaml]
25
+ # ----
26
+ # Performance/Sum:
27
+ # SafeAutoCorrect: false
28
+ # ----
29
+ #
30
+ # Please note that the auto-correction command line option will be changed from
31
+ # `rubocop -a` to `rubocop -A`, which includes unsafe auto-correction.
32
+ #
9
33
  # @example
10
34
  # # bad
11
- # [1, 2, 3].inject(:+)
35
+ # [1, 2, 3].inject(:+) # These bad cases with no initial value are unsafe and
36
+ # [1, 2, 3].inject(&:+) # will not be auto-correced by default. If you want to
37
+ # [1, 2, 3].reduce { |acc, elem| acc + elem } # auto-corrected, you can set `SafeAutoCorrect: false`.
12
38
  # [1, 2, 3].reduce(10, :+)
13
- # [1, 2, 3].reduce { |acc, elem| acc + elem }
39
+ # [1, 2, 3].map { |elem| elem ** 2 }.sum
40
+ # [1, 2, 3].collect(&:count).sum(10)
14
41
  #
15
42
  # # good
16
43
  # [1, 2, 3].sum
17
44
  # [1, 2, 3].sum(10)
18
- # [1, 2, 3].sum
45
+ # [1, 2, 3].sum { |elem| elem ** 2 }
46
+ # [1, 2, 3].sum(10, &:count)
19
47
  #
20
48
  class Sum < Base
21
49
  include RangeHelp
22
50
  extend AutoCorrector
23
51
 
24
52
  MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
53
+ MSG_IF_NO_INIT_VALUE =
54
+ 'Use `%<good_method>s` instead of `%<bad_method>s`, unless calling `%<bad_method>s` on an empty array.'
55
+ RESTRICT_ON_SEND = %i[inject reduce sum].freeze
25
56
 
26
57
  def_node_matcher :sum_candidate?, <<~PATTERN
27
- (send _ ${:inject :reduce} $_init ? (sym :+))
58
+ (send _ ${:inject :reduce} $_init ? ${(sym :+) (block_pass (sym :+))})
59
+ PATTERN
60
+
61
+ def_node_matcher :sum_map_candidate?, <<~PATTERN
62
+ (send
63
+ {
64
+ (block $(send _ {:map :collect}) ...)
65
+ $(send _ {:map :collect} (block_pass _))
66
+ }
67
+ :sum $_init ?)
28
68
  PATTERN
29
69
 
30
70
  def_node_matcher :sum_with_block_candidate?, <<~PATTERN
@@ -40,14 +80,10 @@ module RuboCop
40
80
  alias elem_plus_acc? acc_plus_elem?
41
81
 
42
82
  def on_send(node)
43
- sum_candidate?(node) do |method, init|
44
- range = sum_method_range(node)
45
- message = build_method_message(method, init)
83
+ return if empty_array_literal?(node)
46
84
 
47
- add_offense(range, message: message) do |corrector|
48
- autocorrect(corrector, init, range)
49
- end
50
- end
85
+ handle_sum_candidate(node)
86
+ handle_sum_map_candidate(node)
51
87
  end
52
88
 
53
89
  def on_block(node)
@@ -65,25 +101,87 @@ module RuboCop
65
101
 
66
102
  private
67
103
 
104
+ def handle_sum_candidate(node)
105
+ sum_candidate?(node) do |method, init, operation|
106
+ range = sum_method_range(node)
107
+ message = build_method_message(node, method, init, operation)
108
+
109
+ add_offense(range, message: message) do |corrector|
110
+ autocorrect(corrector, init, range)
111
+ end
112
+ end
113
+ end
114
+
115
+ def handle_sum_map_candidate(node)
116
+ sum_map_candidate?(node) do |map, init|
117
+ next if node.block_literal? || node.block_argument?
118
+
119
+ message = build_sum_map_message(map.method_name, init)
120
+
121
+ add_offense(sum_map_range(map, node), message: message) do |corrector|
122
+ autocorrect_sum_map(corrector, node, map, init)
123
+ end
124
+ end
125
+ end
126
+
127
+ def empty_array_literal?(node)
128
+ receiver = node.children.first
129
+ array_literal?(node) && receiver && receiver.children.empty?
130
+ end
131
+
132
+ def array_literal?(node)
133
+ receiver = node.children.first
134
+ receiver&.literal? && receiver&.array_type?
135
+ end
136
+
68
137
  def autocorrect(corrector, init, range)
69
- return if init.empty?
138
+ return if init.empty? && safe_autocorrect?
70
139
 
71
140
  replacement = build_good_method(init)
72
141
 
73
142
  corrector.replace(range, replacement)
74
143
  end
75
144
 
145
+ def autocorrect_sum_map(corrector, sum, map, init)
146
+ sum_range = method_call_with_args_range(sum)
147
+ map_range = method_call_with_args_range(map)
148
+
149
+ block_pass = map.last_argument if map.last_argument&.block_pass_type?
150
+ replacement = build_good_method(init, block_pass)
151
+
152
+ corrector.remove(sum_range)
153
+
154
+ dot = '.' if map.receiver
155
+ corrector.replace(map_range, "#{dot}#{replacement}")
156
+ end
157
+
76
158
  def sum_method_range(node)
77
159
  range_between(node.loc.selector.begin_pos, node.loc.end.end_pos)
78
160
  end
79
161
 
162
+ def sum_map_range(map, sum)
163
+ range_between(map.loc.selector.begin_pos, sum.source_range.end.end_pos)
164
+ end
165
+
80
166
  def sum_block_range(send, node)
81
167
  range_between(send.loc.selector.begin_pos, node.loc.end.end_pos)
82
168
  end
83
169
 
84
- def build_method_message(method, init)
170
+ def build_method_message(node, method, init, operation)
85
171
  good_method = build_good_method(init)
86
- bad_method = build_method_bad_method(init, method)
172
+ bad_method = build_method_bad_method(init, method, operation)
173
+ msg = if init.empty? && !array_literal?(node)
174
+ MSG_IF_NO_INIT_VALUE
175
+ else
176
+ MSG
177
+ end
178
+ format(msg, good_method: good_method, bad_method: bad_method)
179
+ end
180
+
181
+ def build_sum_map_message(method, init)
182
+ sum_method = build_good_method(init)
183
+ good_method = "#{sum_method} { ... }"
184
+ bad_method = "#{method} { ... }.#{sum_method}"
87
185
  format(MSG, good_method: good_method, bad_method: bad_method)
88
186
  end
89
187
 
@@ -93,23 +191,30 @@ module RuboCop
93
191
  format(MSG, good_method: good_method, bad_method: bad_method)
94
192
  end
95
193
 
96
- def build_good_method(init)
194
+ def build_good_method(init, block_pass = nil)
97
195
  good_method = 'sum'
98
196
 
197
+ args = []
99
198
  unless init.empty?
100
199
  init = init.first
101
- good_method += "(#{init.source})" if init.source.to_i != 0
200
+ args << init.source unless init.int_type? && init.value.zero?
102
201
  end
202
+ args << block_pass.source if block_pass
203
+ good_method += "(#{args.join(', ')})" unless args.empty?
103
204
  good_method
104
205
  end
105
206
 
106
- def build_method_bad_method(init, method)
207
+ def build_method_bad_method(init, method, operation)
107
208
  bad_method = "#{method}("
108
209
  unless init.empty?
109
210
  init = init.first
110
211
  bad_method += "#{init.source}, "
111
212
  end
112
- bad_method += ':+)'
213
+ bad_method += if operation.block_pass_type?
214
+ '&:+)'
215
+ else
216
+ ':+)'
217
+ end
113
218
  bad_method
114
219
  end
115
220
 
@@ -123,6 +228,14 @@ module RuboCop
123
228
  bad_method += " { |#{var_acc}, #{var_elem}| #{body.source} }"
124
229
  bad_method
125
230
  end
231
+
232
+ def method_call_with_args_range(node)
233
+ if (receiver = node.receiver)
234
+ receiver.source_range.end.join(node.source_range.end)
235
+ else
236
+ node.source_range
237
+ end
238
+ end
126
239
  end
127
240
  end
128
241
  end
@@ -23,6 +23,7 @@ module RuboCop
23
23
  MESSAGE = 'Use `Array.new(%<count>s)` with a block ' \
24
24
  'instead of `.times.%<map_or_collect>s`'
25
25
  MESSAGE_ONLY_IF = 'only if `%<count>s` is always 0 or more'
26
+ RESTRICT_ON_SEND = %i[map collect].freeze
26
27
 
27
28
  def on_send(node)
28
29
  check(node)