rubocop-performance 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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