rubocop-performance 1.8.1 → 1.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) 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/constant_regexp.rb +73 -0
  15. data/lib/rubocop/cop/performance/count.rb +1 -0
  16. data/lib/rubocop/cop/performance/delete_prefix.rb +1 -0
  17. data/lib/rubocop/cop/performance/delete_suffix.rb +1 -0
  18. data/lib/rubocop/cop/performance/detect.rb +3 -2
  19. data/lib/rubocop/cop/performance/end_with.rb +1 -0
  20. data/lib/rubocop/cop/performance/fixed_size.rb +1 -0
  21. data/lib/rubocop/cop/performance/flat_map.rb +1 -0
  22. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +2 -0
  23. data/lib/rubocop/cop/performance/io_readlines.rb +3 -7
  24. data/lib/rubocop/cop/performance/method_object_as_block.rb +32 -0
  25. data/lib/rubocop/cop/performance/open_struct.rb +1 -0
  26. data/lib/rubocop/cop/performance/range_include.rb +1 -0
  27. data/lib/rubocop/cop/performance/redundant_block_call.rb +4 -4
  28. data/lib/rubocop/cop/performance/redundant_equality_comparison_block.rb +80 -0
  29. data/lib/rubocop/cop/performance/redundant_match.rb +1 -0
  30. data/lib/rubocop/cop/performance/redundant_merge.rb +1 -0
  31. data/lib/rubocop/cop/performance/redundant_split_regexp_argument.rb +64 -0
  32. data/lib/rubocop/cop/performance/redundant_string_chars.rb +2 -6
  33. data/lib/rubocop/cop/performance/reverse_each.rb +7 -10
  34. data/lib/rubocop/cop/performance/reverse_first.rb +1 -0
  35. data/lib/rubocop/cop/performance/size.rb +1 -0
  36. data/lib/rubocop/cop/performance/squeeze.rb +2 -1
  37. data/lib/rubocop/cop/performance/start_with.rb +1 -0
  38. data/lib/rubocop/cop/performance/string_include.rb +2 -1
  39. data/lib/rubocop/cop/performance/string_replacement.rb +1 -0
  40. data/lib/rubocop/cop/performance/sum.rb +123 -15
  41. data/lib/rubocop/cop/performance/times_map.rb +1 -0
  42. data/lib/rubocop/cop/performance/unfreeze_string.rb +19 -1
  43. data/lib/rubocop/cop/performance/uri_default_parser.rb +1 -0
  44. data/lib/rubocop/cop/performance_cops.rb +6 -0
  45. data/lib/rubocop/performance/version.rb +6 -1
  46. metadata +25 -27
@@ -29,38 +29,40 @@ module RuboCop
29
29
  # [1,2].first # => 1
30
30
  # [1,2].first(1) # => [1]
31
31
  #
32
- RETURN_NEW_ARRAY_WHEN_ARGS = ':first :last :pop :sample :shift '
32
+ RETURN_NEW_ARRAY_WHEN_ARGS = %i[first last pop sample shift].to_set.freeze
33
33
 
34
34
  # These methods return a new array only when called without a block.
35
- RETURNS_NEW_ARRAY_WHEN_NO_BLOCK = ':zip :product '
35
+ RETURNS_NEW_ARRAY_WHEN_NO_BLOCK = %i[zip product].to_set.freeze
36
36
 
37
37
  # These methods ALWAYS return a new array
38
38
  # after they're called it's safe to mutate the the resulting array
39
- ALWAYS_RETURNS_NEW_ARRAY = ':* :+ :- :collect :compact :drop '\
40
- ':drop_while :flatten :map :reject ' \
41
- ':reverse :rotate :select :shuffle :sort ' \
42
- ':take :take_while :transpose :uniq ' \
43
- ':values_at :| '
39
+ ALWAYS_RETURNS_NEW_ARRAY = %i[* + - collect compact drop
40
+ drop_while flatten map reject
41
+ reverse rotate select shuffle sort
42
+ take take_while transpose uniq
43
+ values_at |].to_set.freeze
44
44
 
45
45
  # These methods have a mutation alternative. For example :collect
46
46
  # can be called as :collect!
47
- HAS_MUTATION_ALTERNATIVE = ':collect :compact :flatten :map :reject '\
48
- ':reverse :rotate :select :shuffle :sort '\
49
- ':uniq '
50
- MSG = 'Use unchained `%<method>s!` and `%<second_method>s!` '\
47
+ HAS_MUTATION_ALTERNATIVE = %i[collect compact flatten map reject
48
+ reverse rotate select shuffle sort uniq].to_set.freeze
49
+
50
+ RETURNS_NEW_ARRAY = (ALWAYS_RETURNS_NEW_ARRAY + RETURNS_NEW_ARRAY_WHEN_NO_BLOCK).freeze
51
+
52
+ MSG = 'Use unchained `%<method>s` and `%<second_method>s!` '\
51
53
  '(followed by `return array` if required) instead of chaining '\
52
54
  '`%<method>s...%<second_method>s`.'
53
55
 
54
- def_node_matcher :flat_map_candidate?, <<~PATTERN
55
- {
56
- (send (send _ ${#{RETURN_NEW_ARRAY_WHEN_ARGS}} {int lvar ivar cvar gvar}) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
57
- (send (block (send _ ${#{ALWAYS_RETURNS_NEW_ARRAY} }) ...) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
58
- (send (send _ ${#{ALWAYS_RETURNS_NEW_ARRAY + RETURNS_NEW_ARRAY_WHEN_NO_BLOCK}} ...) ${#{HAS_MUTATION_ALTERNATIVE}} $...)
59
- }
56
+ def_node_matcher :chain_array_allocation?, <<~PATTERN
57
+ (send {
58
+ (send _ $%RETURN_NEW_ARRAY_WHEN_ARGS {int lvar ivar cvar gvar})
59
+ (block (send _ $%ALWAYS_RETURNS_NEW_ARRAY) ...)
60
+ (send _ $%RETURNS_NEW_ARRAY ...)
61
+ } $%HAS_MUTATION_ALTERNATIVE ...)
60
62
  PATTERN
61
63
 
62
64
  def on_send(node)
63
- flat_map_candidate?(node) do |fm, sm, _|
65
+ chain_array_allocation?(node) do |fm, sm|
64
66
  range = range_between(node.loc.dot.begin_pos, node.source_range.end_pos)
65
67
 
66
68
  add_offense(range, message: format(MSG, method: fm, second_method: sm))
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop finds regular expressions with dynamic components that are all constants.
7
+ #
8
+ # Ruby allocates a new Regexp object every time it executes a code containing such
9
+ # a regular expression. It is more efficient to extract it into a constant,
10
+ # memoize it, or add an `/o` option to perform `#{}` interpolation only once and
11
+ # reuse that Regexp object.
12
+ #
13
+ # @example
14
+ #
15
+ # # bad
16
+ # def tokens(pattern)
17
+ # pattern.scan(TOKEN).reject { |token| token.match?(/\A#{SEPARATORS}\Z/) }
18
+ # end
19
+ #
20
+ # # good
21
+ # ALL_SEPARATORS = /\A#{SEPARATORS}\Z/
22
+ # def tokens(pattern)
23
+ # pattern.scan(TOKEN).reject { |token| token.match?(ALL_SEPARATORS) }
24
+ # end
25
+ #
26
+ # # good
27
+ # def tokens(pattern)
28
+ # pattern.scan(TOKEN).reject { |token| token.match?(/\A#{SEPARATORS}\Z/o) }
29
+ # end
30
+ #
31
+ # # good
32
+ # def separators
33
+ # @separators ||= /\A#{SEPARATORS}\Z/
34
+ # end
35
+ #
36
+ class ConstantRegexp < Base
37
+ extend AutoCorrector
38
+
39
+ MSG = 'Extract this regexp into a constant, memoize it, or append an `/o` option to its options.'
40
+
41
+ def on_regexp(node)
42
+ return if within_allowed_assignment?(node) ||
43
+ !include_interpolated_const?(node) ||
44
+ node.single_interpolation?
45
+
46
+ add_offense(node) do |corrector|
47
+ corrector.insert_after(node, 'o')
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def within_allowed_assignment?(node)
54
+ node.each_ancestor(:casgn, :or_asgn).any?
55
+ end
56
+
57
+ def_node_matcher :regexp_escape?, <<~PATTERN
58
+ (send
59
+ (const nil? :Regexp) :escape const_type?)
60
+ PATTERN
61
+
62
+ def include_interpolated_const?(node)
63
+ return false unless node.interpolation?
64
+
65
+ node.each_child_node(:begin).all? do |begin_node|
66
+ inner_node = begin_node.children.first
67
+ inner_node && (inner_node.const_type? || regexp_escape?(inner_node))
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -42,6 +42,7 @@ module RuboCop
42
42
  extend AutoCorrector
43
43
 
44
44
  MSG = 'Use `count` instead of `%<selector>s...%<counter>s`.'
45
+ RESTRICT_ON_SEND = %i[count length size].freeze
45
46
 
46
47
  def_node_matcher :count_candidate?, <<~PATTERN
47
48
  {
@@ -51,6 +51,7 @@ module RuboCop
51
51
  minimum_target_ruby_version 2.5
52
52
 
53
53
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
54
+ RESTRICT_ON_SEND = %i[gsub gsub! sub sub!].freeze
54
55
 
55
56
  PREFERRED_METHODS = {
56
57
  gsub: :delete_prefix,
@@ -51,6 +51,7 @@ module RuboCop
51
51
  minimum_target_ruby_version 2.5
52
52
 
53
53
  MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
54
+ RESTRICT_ON_SEND = %i[gsub gsub! sub sub!].freeze
54
55
 
55
56
  PREFERRED_METHODS = {
56
57
  gsub: :delete_suffix,
@@ -4,8 +4,8 @@ module RuboCop
4
4
  module Cop
5
5
  module Performance
6
6
  # This cop is used to identify usages of `first`, `last`, `[0]` or `[-1]`
7
- # chained to `select`, `find_all`, or `find_all`
8
- # and change them to use `detect` instead.
7
+ # chained to `select`, `find_all` or `filter` and change them to use
8
+ # `detect` instead.
9
9
  #
10
10
  # @example
11
11
  # # bad
@@ -39,6 +39,7 @@ module RuboCop
39
39
  '`%<first_method>s[%<index>i]`.'
40
40
  INDEX_REVERSE_MSG = 'Use `reverse.%<prefer>s` instead of ' \
41
41
  '`%<first_method>s[%<index>i]`.'
42
+ RESTRICT_ON_SEND = %i[first last []].freeze
42
43
 
43
44
  def_node_matcher :detect_candidate?, <<~PATTERN
44
45
  {
@@ -47,6 +47,7 @@ module RuboCop
47
47
 
48
48
  MSG = 'Use `String#end_with?` instead of a regex match anchored to ' \
49
49
  'the end 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_end?) (regopt)))
@@ -47,6 +47,7 @@ module RuboCop
47
47
  #
48
48
  class FixedSize < Base
49
49
  MSG = 'Do not compute the size of statically sized objects.'
50
+ RESTRICT_ON_SEND = %i[count length size].freeze
50
51
 
51
52
  def_node_matcher :counter, <<~MATCHER
52
53
  (send ${array hash str sym} {:count :length :size} $...)
@@ -19,6 +19,7 @@ module RuboCop
19
19
  extend AutoCorrector
20
20
 
21
21
  MSG = 'Use `flat_map` instead of `%<method>s...%<flatten>s`.'
22
+ RESTRICT_ON_SEND = %i[flatten flatten!].freeze
22
23
  FLATTEN_MULTIPLE_LEVELS = ' Beware, `flat_map` only flattens 1 level ' \
23
24
  'and `flatten` can be used to flatten ' \
24
25
  'multiple levels.'
@@ -39,6 +39,8 @@ module RuboCop
39
39
  class InefficientHashSearch < Base
40
40
  extend AutoCorrector
41
41
 
42
+ RESTRICT_ON_SEND = %i[include?].freeze
43
+
42
44
  def_node_matcher :inefficient_include?, <<~PATTERN
43
45
  (send (send $_ {:keys :values}) :include? _)
44
46
  PATTERN
@@ -29,14 +29,14 @@ module RuboCop
29
29
  extend AutoCorrector
30
30
 
31
31
  MSG = 'Use `%<good>s` instead of `%<bad>s`.'
32
- ENUMERABLE_METHODS = (Enumerable.instance_methods + [:each]).freeze
32
+ RESTRICT_ON_SEND = (Enumerable.instance_methods + [:each]).freeze
33
33
 
34
34
  def_node_matcher :readlines_on_class?, <<~PATTERN
35
- $(send $(send (const nil? {:IO :File}) :readlines ...) #enumerable_method?)
35
+ $(send $(send (const nil? {:IO :File}) :readlines ...) _)
36
36
  PATTERN
37
37
 
38
38
  def_node_matcher :readlines_on_instance?, <<~PATTERN
39
- $(send $(send ${nil? !const_type?} :readlines ...) #enumerable_method? ...)
39
+ $(send $(send ${nil? !const_type?} :readlines ...) _ ...)
40
40
  PATTERN
41
41
 
42
42
  def on_send(node)
@@ -55,10 +55,6 @@ module RuboCop
55
55
 
56
56
  private
57
57
 
58
- def enumerable_method?(node)
59
- ENUMERABLE_METHODS.include?(node.to_sym)
60
- end
61
-
62
58
  def autocorrect(corrector, enumerable_call, readlines_call, receiver)
63
59
  # We cannot safely correct `.readlines` method called on IO/File classes
64
60
  # due to its signature and we are not sure with implicit receiver
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop identifies places where methods are converted to blocks, with the
7
+ # use of `&method`, and passed as arguments to method calls.
8
+ # It is faster to replace those with explicit blocks, calling those methods inside.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # array.map(&method(:do_something))
13
+ # [1, 2, 3].each(&out.method(:puts))
14
+ #
15
+ # # good
16
+ # array.map { |x| do_something(x) }
17
+ # [1, 2, 3].each { |x| out.puts(x) }
18
+ #
19
+ class MethodObjectAsBlock < Base
20
+ MSG = 'Use block explicitly instead of block-passing a method object.'
21
+
22
+ def_node_matcher :method_object_as_argument?, <<~PATTERN
23
+ (^send (send _ :method sym))
24
+ PATTERN
25
+
26
+ def on_block_pass(node)
27
+ add_offense(node) if method_object_as_argument?(node)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -30,6 +30,7 @@ module RuboCop
30
30
  class OpenStruct < Base
31
31
  MSG = 'Consider using `Struct` over `OpenStruct` ' \
32
32
  'to optimize the performance.'
33
+ RESTRICT_ON_SEND = %i[new].freeze
33
34
 
34
35
  def_node_matcher :open_struct, <<~PATTERN
35
36
  (send (const {nil? cbase} :OpenStruct) :new ...)
@@ -28,6 +28,7 @@ module RuboCop
28
28
  extend AutoCorrector
29
29
 
30
30
  MSG = 'Use `Range#cover?` instead of `Range#%<bad_method>s`.'
31
+ RESTRICT_ON_SEND = %i[include? member?].freeze
31
32
 
32
33
  # TODO: If we traced out assignments of variables to their uses, we
33
34
  # might pick up on a few more instances of this issue
@@ -80,11 +80,11 @@ module RuboCop
80
80
  def calls_to_report(argname, body)
81
81
  return [] if blockarg_assigned?(body, argname)
82
82
 
83
- calls = to_enum(:blockarg_calls, body, argname)
83
+ blockarg_calls(body, argname).map do |call|
84
+ return [] if args_include_block_pass?(call)
84
85
 
85
- return [] if calls.any? { |call| args_include_block_pass?(call) }
86
-
87
- calls
86
+ call
87
+ end
88
88
  end
89
89
 
90
90
  def args_include_block_pass?(blockcall)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop checks for uses `Enumerable#all?`, `Enumerable#any?`, `Enumerable#one?`,
7
+ # and `Enumerable#none?` are compared with `===` or similar methods in block.
8
+ #
9
+ # By default, `Object#===` behaves the same as `Object#==`, but this
10
+ # behavior is appropriately overridden in subclass. For example,
11
+ # `Range#===` returns `true` when argument is within the range.
12
+ # Therefore, It is marked as unsafe by default because `===` and `==`
13
+ # do not always behave the same.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # items.all? { |item| pattern === item }
18
+ # items.all? { |item| item == other }
19
+ # items.all? { |item| item.is_a?(Klass) }
20
+ # items.all? { |item| item.kind_of?(Klass) }
21
+ #
22
+ # # good
23
+ # items.all?(pattern)
24
+ #
25
+ class RedundantEqualityComparisonBlock < Base
26
+ extend AutoCorrector
27
+ extend TargetRubyVersion
28
+
29
+ minimum_target_ruby_version 2.5
30
+
31
+ MSG = 'Use `%<prefer>s` instead of block.'
32
+
33
+ TARGET_METHODS = %i[all? any? one? none?].freeze
34
+ COMPARISON_METHODS = %i[== === is_a? kind_of?].freeze
35
+ IS_A_METHODS = %i[is_a? kind_of?].freeze
36
+
37
+ def on_block(node)
38
+ return unless TARGET_METHODS.include?(node.method_name) && node.arguments.one?
39
+
40
+ block_argument = node.arguments.first
41
+ block_body = node.body
42
+ return unless use_equality_comparison_block?(block_body)
43
+ return if same_block_argument_and_is_a_argument?(block_body, block_argument)
44
+ return unless (new_argument = new_argument(block_argument, block_body))
45
+
46
+ range = offense_range(node)
47
+ prefer = "#{node.method_name}(#{new_argument})"
48
+
49
+ add_offense(range, message: format(MSG, prefer: prefer)) do |corrector|
50
+ corrector.replace(range, prefer)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def use_equality_comparison_block?(block_body)
57
+ block_body.send_type? && COMPARISON_METHODS.include?(block_body.method_name)
58
+ end
59
+
60
+ def same_block_argument_and_is_a_argument?(block_body, block_argument)
61
+ return false unless IS_A_METHODS.include?(block_body.method_name)
62
+
63
+ block_argument.source == block_body.first_argument.source
64
+ end
65
+
66
+ def new_argument(block_argument, block_body)
67
+ if block_argument.source == block_body.receiver.source
68
+ block_body.first_argument.source
69
+ elsif block_argument.source == block_body.first_argument.source
70
+ block_body.receiver.source
71
+ end
72
+ end
73
+
74
+ def offense_range(node)
75
+ node.send_node.loc.selector.join(node.source_range.end)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -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,64 @@
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 (regexp_node = split_call_with_regexp?(node))
29
+ return if regexp_node.ignore_case?
30
+ return unless determinist_regexp?(regexp_node)
31
+
32
+ add_offense(regexp_node) do |corrector|
33
+ new_argument = replacement(regexp_node)
34
+
35
+ corrector.replace(regexp_node, "\"#{new_argument}\"")
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def determinist_regexp?(regexp_node)
42
+ DETERMINISTIC_REGEX.match?(regexp_node.source)
43
+ end
44
+
45
+ def replacement(regexp_node)
46
+ regexp_content = regexp_node.content
47
+ stack = []
48
+ chars = regexp_content.chars.each_with_object([]) do |char, strings|
49
+ if stack.empty? && char == '\\'
50
+ stack.push(char)
51
+ else
52
+ strings << "#{stack.pop}#{char}"
53
+ end
54
+ end
55
+ chars.map do |char|
56
+ char = char.dup
57
+ char.delete!('\\') unless STR_SPECIAL_CHARS.include?(char)
58
+ char
59
+ end.join
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end