rubocop-performance 0.0.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 (34) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +20 -0
  3. data/README.md +1 -0
  4. data/lib/rubocop-performance.rb +5 -0
  5. data/lib/rubocop/cop/performance/caller.rb +69 -0
  6. data/lib/rubocop/cop/performance/case_when_splat.rb +173 -0
  7. data/lib/rubocop/cop/performance/casecmp.rb +117 -0
  8. data/lib/rubocop/cop/performance/compare_with_block.rb +119 -0
  9. data/lib/rubocop/cop/performance/count.rb +102 -0
  10. data/lib/rubocop/cop/performance/detect.rb +110 -0
  11. data/lib/rubocop/cop/performance/double_start_end_with.rb +94 -0
  12. data/lib/rubocop/cop/performance/end_with.rb +56 -0
  13. data/lib/rubocop/cop/performance/fixed_size.rb +97 -0
  14. data/lib/rubocop/cop/performance/flat_map.rb +78 -0
  15. data/lib/rubocop/cop/performance/inefficient_hash_search.rb +99 -0
  16. data/lib/rubocop/cop/performance/lstrip_rstrip.rb +46 -0
  17. data/lib/rubocop/cop/performance/range_include.rb +47 -0
  18. data/lib/rubocop/cop/performance/redundant_block_call.rb +93 -0
  19. data/lib/rubocop/cop/performance/redundant_match.rb +56 -0
  20. data/lib/rubocop/cop/performance/redundant_merge.rb +169 -0
  21. data/lib/rubocop/cop/performance/redundant_sort_by.rb +50 -0
  22. data/lib/rubocop/cop/performance/regexp_match.rb +264 -0
  23. data/lib/rubocop/cop/performance/reverse_each.rb +42 -0
  24. data/lib/rubocop/cop/performance/sample.rb +142 -0
  25. data/lib/rubocop/cop/performance/size.rb +71 -0
  26. data/lib/rubocop/cop/performance/start_with.rb +59 -0
  27. data/lib/rubocop/cop/performance/string_replacement.rb +172 -0
  28. data/lib/rubocop/cop/performance/times_map.rb +71 -0
  29. data/lib/rubocop/cop/performance/unfreeze_string.rb +50 -0
  30. data/lib/rubocop/cop/performance/unneeded_sort.rb +165 -0
  31. data/lib/rubocop/cop/performance/uri_default_parser.rb +47 -0
  32. data/lib/rubocop/cop/performance_cops.rb +37 -0
  33. data/lib/rubocop/performance/version.rb +9 -0
  34. metadata +114 -0
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop is used to identify usages of `count` on an
7
+ # `Array` and `Hash` and change them to `size`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # [1, 2, 3].count
12
+ #
13
+ # # bad
14
+ # {a: 1, b: 2, c: 3}.count
15
+ #
16
+ # # good
17
+ # [1, 2, 3].size
18
+ #
19
+ # # good
20
+ # {a: 1, b: 2, c: 3}.size
21
+ #
22
+ # # good
23
+ # [1, 2, 3].count { |e| e > 2 }
24
+ # TODO: Add advanced detection of variables that could
25
+ # have been assigned to an array or a hash.
26
+ class Size < Cop
27
+ MSG = 'Use `size` instead of `count`.'.freeze
28
+
29
+ def on_send(node)
30
+ return unless eligible_node?(node)
31
+
32
+ add_offense(node, location: :selector)
33
+ end
34
+
35
+ def autocorrect(node)
36
+ ->(corrector) { corrector.replace(node.loc.selector, 'size') }
37
+ end
38
+
39
+ private
40
+
41
+ def eligible_node?(node)
42
+ return false unless node.method?(:count) && !node.arguments?
43
+
44
+ eligible_receiver?(node.receiver) && !allowed_parent?(node.parent)
45
+ end
46
+
47
+ def eligible_receiver?(node)
48
+ return false unless node
49
+
50
+ array?(node) || hash?(node)
51
+ end
52
+
53
+ def allowed_parent?(node)
54
+ node && node.block_type?
55
+ end
56
+
57
+ def array?(node)
58
+ _, constant = *node.receiver
59
+
60
+ node.array_type? || constant == :Array || node.method_name == :to_a
61
+ end
62
+
63
+ def hash?(node)
64
+ _, constant = *node.receiver
65
+
66
+ node.hash_type? || constant == :Hash || node.method_name == :to_h
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop identifies unnecessary use of a regex where
7
+ # `String#start_with?` would suffice.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # 'abc'.match?(/\Aab/)
12
+ # 'abc' =~ /\Aab/
13
+ # 'abc'.match(/\Aab/)
14
+ #
15
+ # # good
16
+ # 'abc'.start_with?('ab')
17
+ class StartWith < Cop
18
+ MSG = 'Use `String#start_with?` instead of a regex match anchored to ' \
19
+ 'the beginning of the string.'.freeze
20
+ SINGLE_QUOTE = "'".freeze
21
+
22
+ def_node_matcher :redundant_regex?, <<-PATTERN
23
+ {(send $!nil? {:match :=~ :match?} (regexp (str $#literal_at_start?) (regopt)))
24
+ (send (regexp (str $#literal_at_start?) (regopt)) {:match :=~} $_)}
25
+ PATTERN
26
+
27
+ def literal_at_start?(regex_str)
28
+ # is this regexp 'literal' in the sense of only matching literal
29
+ # chars, rather than using metachars like . and * and so on?
30
+ # also, is it anchored at the start of the string?
31
+ # (tricky: \s, \d, and so on are metacharacters, but other characters
32
+ # escaped with a slash are just literals. LITERAL_REGEX takes all
33
+ # that into account.)
34
+ regex_str =~ /\A\\A(?:#{LITERAL_REGEX})+\z/
35
+ end
36
+
37
+ def on_send(node)
38
+ return unless redundant_regex?(node)
39
+
40
+ add_offense(node)
41
+ end
42
+
43
+ def autocorrect(node)
44
+ redundant_regex?(node) do |receiver, regex_str|
45
+ receiver, regex_str = regex_str, receiver if receiver.is_a?(String)
46
+ regex_str = regex_str[2..-1] # drop \A anchor
47
+ regex_str = interpret_string_escapes(regex_str)
48
+
49
+ lambda do |corrector|
50
+ new_source = receiver.source + '.start_with?(' +
51
+ to_string_literal(regex_str) + ')'
52
+ corrector.replace(node.source_range, new_source)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop identifies places where `gsub` can be replaced by
7
+ # `tr` or `delete`.
8
+ #
9
+ # @example
10
+ # # bad
11
+ # 'abc'.gsub('b', 'd')
12
+ # 'abc'.gsub('a', '')
13
+ # 'abc'.gsub(/a/, 'd')
14
+ # 'abc'.gsub!('a', 'd')
15
+ #
16
+ # # good
17
+ # 'abc'.gsub(/.*/, 'a')
18
+ # 'abc'.gsub(/a+/, 'd')
19
+ # 'abc'.tr('b', 'd')
20
+ # 'a b c'.delete(' ')
21
+ class StringReplacement < Cop
22
+ include RangeHelp
23
+
24
+ MSG = 'Use `%<prefer>s` instead of `%<current>s`.'.freeze
25
+ DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/
26
+ DELETE = 'delete'.freeze
27
+ TR = 'tr'.freeze
28
+ BANG = '!'.freeze
29
+ SINGLE_QUOTE = "'".freeze
30
+
31
+ def_node_matcher :string_replacement?, <<-PATTERN
32
+ (send _ {:gsub :gsub!}
33
+ ${regexp str (send (const nil? :Regexp) {:new :compile} _)}
34
+ $str)
35
+ PATTERN
36
+
37
+ def on_send(node)
38
+ string_replacement?(node) do |first_param, second_param|
39
+ return if accept_second_param?(second_param)
40
+ return if accept_first_param?(first_param)
41
+
42
+ offense(node, first_param, second_param)
43
+ end
44
+ end
45
+
46
+ def autocorrect(node)
47
+ _string, _method, first_param, second_param = *node
48
+ first_source, = first_source(first_param)
49
+ second_source, = *second_param
50
+
51
+ unless first_param.str_type?
52
+ first_source = interpret_string_escapes(first_source)
53
+ end
54
+
55
+ replacement_method =
56
+ replacement_method(node, first_source, second_source)
57
+
58
+ replace_method(node, first_source, second_source, first_param,
59
+ replacement_method)
60
+ end
61
+
62
+ def replace_method(node, first, second, first_param, replacement)
63
+ lambda do |corrector|
64
+ corrector.replace(node.loc.selector, replacement)
65
+ unless first_param.str_type?
66
+ corrector.replace(first_param.source_range,
67
+ to_string_literal(first))
68
+ end
69
+
70
+ if second.empty? && first.length == 1
71
+ remove_second_param(corrector, node, first_param)
72
+ end
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def accept_second_param?(second_param)
79
+ second_source, = *second_param
80
+ second_source.length > 1
81
+ end
82
+
83
+ def accept_first_param?(first_param)
84
+ first_source, options = first_source(first_param)
85
+ return true if first_source.nil?
86
+
87
+ unless first_param.str_type?
88
+ return true if options
89
+ return true unless first_source =~ DETERMINISTIC_REGEX
90
+ # This must be done after checking DETERMINISTIC_REGEX
91
+ # Otherwise things like \s will trip us up
92
+ first_source = interpret_string_escapes(first_source)
93
+ end
94
+
95
+ first_source.length != 1
96
+ end
97
+
98
+ def offense(node, first_param, second_param)
99
+ first_source, = first_source(first_param)
100
+ unless first_param.str_type?
101
+ first_source = interpret_string_escapes(first_source)
102
+ end
103
+ second_source, = *second_param
104
+ message = message(node, first_source, second_source)
105
+
106
+ add_offense(node, location: range(node), message: message)
107
+ end
108
+
109
+ def first_source(first_param)
110
+ case first_param.type
111
+ when :regexp
112
+ source_from_regex_literal(first_param)
113
+ when :send
114
+ source_from_regex_constructor(first_param)
115
+ when :str
116
+ first_param.children.first
117
+ end
118
+ end
119
+
120
+ def source_from_regex_literal(node)
121
+ regex, options = *node
122
+ source, = *regex
123
+ options, = *options
124
+ [source, options]
125
+ end
126
+
127
+ def source_from_regex_constructor(node)
128
+ _const, _init, regex = *node
129
+ case regex.type
130
+ when :regexp
131
+ source_from_regex_literal(regex)
132
+ when :str
133
+ source, = *regex
134
+ source
135
+ end
136
+ end
137
+
138
+ def range(node)
139
+ range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
140
+ end
141
+
142
+ def replacement_method(node, first_source, second_source)
143
+ replacement = if second_source.empty? && first_source.length == 1
144
+ DELETE
145
+ else
146
+ TR
147
+ end
148
+
149
+ "#{replacement}#{BANG if node.bang_method?}"
150
+ end
151
+
152
+ def message(node, first_source, second_source)
153
+ replacement_method =
154
+ replacement_method(node, first_source, second_source)
155
+
156
+ format(MSG, prefer: replacement_method, current: node.method_name)
157
+ end
158
+
159
+ def method_suffix(node)
160
+ node.loc.end ? node.loc.end.source : ''
161
+ end
162
+
163
+ def remove_second_param(corrector, node, first_param)
164
+ end_range = range_between(first_param.source_range.end_pos,
165
+ node.source_range.end_pos)
166
+
167
+ corrector.replace(end_range, method_suffix(node))
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop checks for .times.map calls.
7
+ # In most cases such calls can be replaced
8
+ # with an explicit array creation.
9
+ #
10
+ # @example
11
+ # # bad
12
+ # 9.times.map do |i|
13
+ # i.to_s
14
+ # end
15
+ #
16
+ # # good
17
+ # Array.new(9) do |i|
18
+ # i.to_s
19
+ # end
20
+ class TimesMap < Cop
21
+ MESSAGE = 'Use `Array.new(%<count>s)` with a block ' \
22
+ 'instead of `.times.%<map_or_collect>s`'.freeze
23
+ MESSAGE_ONLY_IF = 'only if `%<count>s` is always 0 or more'.freeze
24
+
25
+ def on_send(node)
26
+ check(node)
27
+ end
28
+
29
+ def on_block(node)
30
+ check(node)
31
+ end
32
+
33
+ def autocorrect(node)
34
+ map_or_collect, count = times_map_call(node)
35
+
36
+ replacement =
37
+ "Array.new(#{count.source}" \
38
+ "#{map_or_collect.arguments.map { |arg| ", #{arg.source}" }.join})"
39
+
40
+ lambda do |corrector|
41
+ corrector.replace(map_or_collect.loc.expression, replacement)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def check(node)
48
+ times_map_call(node) do |map_or_collect, count|
49
+ add_offense(node, message: message(map_or_collect, count))
50
+ end
51
+ end
52
+
53
+ def message(map_or_collect, count)
54
+ template = if count.literal?
55
+ MESSAGE + '.'
56
+ else
57
+ "#{MESSAGE} #{MESSAGE_ONLY_IF}."
58
+ end
59
+ format(template,
60
+ count: count.source,
61
+ map_or_collect: map_or_collect.method_name)
62
+ end
63
+
64
+ def_node_matcher :times_map_call, <<-PATTERN
65
+ {(block $(send (send $!nil? :times) {:map :collect}) ...)
66
+ $(send (send $!nil? :times) {:map :collect} (block_pass ...))}
67
+ PATTERN
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # In Ruby 2.3 or later, use unary plus operator to unfreeze a string
7
+ # literal instead of `String#dup` and `String.new`.
8
+ # Unary plus operator is faster than `String#dup`.
9
+ #
10
+ # Note: `String.new` (without operator) is not exactly the same as `+''`.
11
+ # These differ in encoding. `String.new.encoding` is always `ASCII-8BIT`.
12
+ # However, `(+'').encoding` is the same as script encoding(e.g. `UTF-8`).
13
+ # So, if you expect `ASCII-8BIT` encoding, disable this cop.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # ''.dup
18
+ # "something".dup
19
+ # String.new
20
+ # String.new('')
21
+ # String.new('something')
22
+ #
23
+ # # good
24
+ # +'something'
25
+ # +''
26
+ class UnfreezeString < Cop
27
+ extend TargetRubyVersion
28
+
29
+ minimum_target_ruby_version 2.3
30
+
31
+ MSG = 'Use unary plus to get an unfrozen string literal.'.freeze
32
+
33
+ def_node_matcher :dup_string?, <<-PATTERN
34
+ (send {str dstr} :dup)
35
+ PATTERN
36
+
37
+ def_node_matcher :string_new?, <<-PATTERN
38
+ {
39
+ (send (const nil? :String) :new {str dstr})
40
+ (send (const nil? :String) :new)
41
+ }
42
+ PATTERN
43
+
44
+ def on_send(node)
45
+ add_offense(node) if dup_string?(node) || string_new?(node)
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Performance
6
+ # This cop is used to identify instances of sorting and then
7
+ # taking only the first or last element. The same behavior can
8
+ # be accomplished without a relatively expensive sort by using
9
+ # `Enumerable#min` instead of sorting and taking the first
10
+ # element and `Enumerable#max` instead of sorting and taking the
11
+ # last element. Similarly, `Enumerable#min_by` and
12
+ # `Enumerable#max_by` can replace `Enumerable#sort_by` calls
13
+ # after which only the first or last element is used.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # [2, 1, 3].sort.first
18
+ # [2, 1, 3].sort[0]
19
+ # [2, 1, 3].sort.at(0)
20
+ # [2, 1, 3].sort.slice(0)
21
+ #
22
+ # # good
23
+ # [2, 1, 3].min
24
+ #
25
+ # # bad
26
+ # [2, 1, 3].sort.last
27
+ # [2, 1, 3].sort[-1]
28
+ # [2, 1, 3].sort.at(-1)
29
+ # [2, 1, 3].sort.slice(-1)
30
+ #
31
+ # # good
32
+ # [2, 1, 3].max
33
+ #
34
+ # # bad
35
+ # arr.sort_by(&:foo).first
36
+ # arr.sort_by(&:foo)[0]
37
+ # arr.sort_by(&:foo).at(0)
38
+ # arr.sort_by(&:foo).slice(0)
39
+ #
40
+ # # good
41
+ # arr.min_by(&:foo)
42
+ #
43
+ # # bad
44
+ # arr.sort_by(&:foo).last
45
+ # arr.sort_by(&:foo)[-1]
46
+ # arr.sort_by(&:foo).at(-1)
47
+ # arr.sort_by(&:foo).slice(-1)
48
+ #
49
+ # # good
50
+ # arr.max_by(&:foo)
51
+ #
52
+ class UnneededSort < Cop
53
+ include RangeHelp
54
+
55
+ MSG = 'Use `%<suggestion>s` instead of '\
56
+ '`%<sorter>s...%<accessor_source>s`.'.freeze
57
+
58
+ def_node_matcher :unneeded_sort?, <<-MATCHER
59
+ {
60
+ (send $(send _ $:sort ...) ${:last :first})
61
+ (send $(send _ $:sort ...) ${:[] :at :slice} {(int 0) (int -1)})
62
+
63
+ (send $(send _ $:sort_by _) ${:last :first})
64
+ (send $(send _ $:sort_by _) ${:[] :at :slice} {(int 0) (int -1)})
65
+
66
+ (send (block $(send _ ${:sort_by :sort}) ...) ${:last :first})
67
+ (send
68
+ (block $(send _ ${:sort_by :sort}) ...)
69
+ ${:[] :at :slice} {(int 0) (int -1)}
70
+ )
71
+ }
72
+ MATCHER
73
+
74
+ def on_send(node)
75
+ unneeded_sort?(node) do |sort_node, sorter, accessor|
76
+ range = range_between(
77
+ sort_node.loc.selector.begin_pos,
78
+ node.loc.expression.end_pos
79
+ )
80
+
81
+ add_offense(node,
82
+ location: range,
83
+ message: message(node,
84
+ sorter,
85
+ accessor))
86
+ end
87
+ end
88
+
89
+ def autocorrect(node)
90
+ sort_node, sorter, accessor = unneeded_sort?(node)
91
+
92
+ lambda do |corrector|
93
+ # Remove accessor, e.g. `first` or `[-1]`.
94
+ corrector.remove(
95
+ range_between(
96
+ accessor_start(node),
97
+ node.loc.expression.end_pos
98
+ )
99
+ )
100
+
101
+ # Replace "sort" or "sort_by" with the appropriate min/max method.
102
+ corrector.replace(
103
+ sort_node.loc.selector,
104
+ suggestion(sorter, accessor, arg_value(node))
105
+ )
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def message(node, sorter, accessor)
112
+ accessor_source = range_between(
113
+ node.loc.selector.begin_pos,
114
+ node.loc.expression.end_pos
115
+ ).source
116
+
117
+ format(MSG,
118
+ suggestion: suggestion(sorter,
119
+ accessor,
120
+ arg_value(node)),
121
+ sorter: sorter,
122
+ accessor_source: accessor_source)
123
+ end
124
+
125
+ def suggestion(sorter, accessor, arg)
126
+ base(accessor, arg) + suffix(sorter)
127
+ end
128
+
129
+ def base(accessor, arg)
130
+ if accessor == :first || (arg && arg.zero?)
131
+ 'min'
132
+ elsif accessor == :last || arg == -1
133
+ 'max'
134
+ end
135
+ end
136
+
137
+ def suffix(sorter)
138
+ if sorter == :sort
139
+ ''
140
+ elsif sorter == :sort_by
141
+ '_by'
142
+ end
143
+ end
144
+
145
+ def arg_node(node)
146
+ node.arguments.first
147
+ end
148
+
149
+ def arg_value(node)
150
+ arg_node(node).nil? ? nil : arg_node(node).node_parts.first
151
+ end
152
+
153
+ # This gets the start of the accessor whether it has a dot
154
+ # (e.g. `.first`) or doesn't (e.g. `[0]`)
155
+ def accessor_start(node)
156
+ if node.loc.dot
157
+ node.loc.dot.begin_pos
158
+ else
159
+ node.loc.selector.begin_pos
160
+ end
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end