forked from rubocop/rubocop-performance
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathredundant_string_chars.rb
131 lines (115 loc) · 3.59 KB
/
redundant_string_chars.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# Checks for redundant `String#chars`.
#
# @example
# # bad
# str.chars[0..2]
# str.chars.slice(0..2)
# str.chars.last
#
# # good
# str[0..2].chars
#
# # bad
# str.chars.first
# str.chars.first(2)
#
# # good
# str[0]
# str[0...2].chars
# str[-1]
#
# # bad
# str.chars.take(2)
# str.chars.length
# str.chars.size
# str.chars.empty?
#
# # good
# str[0...2].chars
# str.length
# str.size
# str.empty?
#
# # For example, if the receiver is an empty string, it will be incompatible.
# # If a negative value is specified for the receiver, `nil` is returned.
# str.chars.last(2) # Incompatible with `str[-2..-1].chars`.
# str.chars.drop(2) # Incompatible with `str[2..-1].chars`.
#
class RedundantStringChars < Base
include RangeHelp
extend AutoCorrector
MSG = 'Use `%<good_method>s` instead of `%<bad_method>s`.'
RESTRICT_ON_SEND = %i[[] slice first last take length size empty?].freeze
def_node_matcher :redundant_chars_call?, <<~PATTERN
(send $(send _ :chars) $_ $...)
PATTERN
def on_send(node)
return unless (receiver, method, args = redundant_chars_call?(node))
return if method == :last && !args.empty?
range = offense_range(receiver, node)
message = build_message(method, args)
add_offense(range, message: message) do |corrector|
range = correction_range(receiver, node)
replacement = build_good_method(method, args)
corrector.replace(range, replacement)
end
end
private
def offense_range(receiver, node)
range_between(receiver.loc.selector.begin_pos, node.source_range.end_pos)
end
def correction_range(receiver, node)
range_between(receiver.loc.dot.begin_pos, node.source_range.end_pos)
end
def build_message(method, args)
good_method = build_good_method(method, args)
bad_method = build_bad_method(method, args)
format(MSG, good_method: good_method, bad_method: bad_method)
end
def build_good_method(method, args)
case method
when :slice
"[#{build_call_args(args)}].chars"
when :[], :first
build_good_method_for_brackets_or_first_method(method, args)
when :last
'[-1]'
when :take
"[0...#{args.first.source}].chars"
else
".#{method}"
end
end
def build_good_method_for_brackets_or_first_method(method, args)
first_arg = args.first
if first_arg&.range_type?
"[#{build_call_args(args)}].chars"
elsif method == :first && args.any?
"[0...#{args.first.source}].chars"
else
first_arg ? "[#{first_arg.source}]" : '[0]'
end
end
def build_bad_method(method, args)
case method
when :[]
"chars[#{build_call_args(args)}]"
else
if args.any?
"chars.#{method}(#{build_call_args(args)})"
else
"chars.#{method}"
end
end
end
def build_call_args(call_args_node)
call_args_node.map(&:source).join(', ')
end
end
end
end
end