-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
string_replacement.rb
161 lines (131 loc) · 5.02 KB
/
string_replacement.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
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# Identifies places where `gsub` can be replaced by `tr` or `delete`.
#
# @example
# # bad
# 'abc'.gsub('b', 'd')
# 'abc'.gsub('a', '')
# 'abc'.gsub(/a/, 'd')
# 'abc'.gsub!('a', 'd')
#
# # good
# 'abc'.gsub(/.*/, 'a')
# 'abc'.gsub(/a+/, 'd')
# 'abc'.tr('b', 'd')
# 'a b c'.delete(' ')
class StringReplacement < Base
include RangeHelp
extend AutoCorrector
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
RESTRICT_ON_SEND = %i[gsub gsub!].freeze
DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/.freeze
DELETE = 'delete'
TR = 'tr'
BANG = '!'
def_node_matcher :string_replacement?, <<~PATTERN
(call _ {:gsub :gsub!}
${regexp str (send (const nil? :Regexp) {:new :compile} _)}
$str)
PATTERN
def on_send(node)
string_replacement?(node) do |first_param, second_param|
return if accept_second_param?(second_param)
return if accept_first_param?(first_param)
offense(node, first_param, second_param)
end
end
alias on_csend on_send
private
def offense(node, first_param, second_param)
first_source, = first_source(first_param)
first_source = interpret_string_escapes(first_source) unless first_param.str_type?
second_source, = *second_param
message = message(node, first_source, second_source)
add_offense(range(node), message: message) do |corrector|
autocorrect(corrector, node)
end
end
def autocorrect(corrector, node)
_string, _method, first_param, second_param = *node
first_source, = first_source(first_param)
second_source, = *second_param
first_source = interpret_string_escapes(first_source) unless first_param.str_type?
replace_method(corrector, node, first_source, second_source, first_param)
end
def replace_method(corrector, node, first_source, second_source, first_param)
replacement_method = replacement_method(node, first_source, second_source)
corrector.replace(node.loc.selector, replacement_method)
corrector.replace(first_param, to_string_literal(first_source)) unless first_param.str_type?
remove_second_param(corrector, node, first_param) if second_source.empty? && first_source.length == 1
end
def accept_second_param?(second_param)
second_source, = *second_param
second_source.length > 1
end
def accept_first_param?(first_param)
first_source, options = first_source(first_param)
return true if first_source.nil?
unless first_param.str_type?
return true if options
return true unless first_source.is_a?(String) && first_source =~ DETERMINISTIC_REGEX
# This must be done after checking DETERMINISTIC_REGEX
# Otherwise things like \s will trip us up
first_source = interpret_string_escapes(first_source)
end
first_source.length != 1
end
def first_source(first_param)
case first_param.type
when :regexp
source_from_regex_literal(first_param)
when :send
source_from_regex_constructor(first_param)
when :str
first_param.children.first
end
end
def source_from_regex_literal(node)
regex, options = *node
source, = *regex
options, = *options
[source, options]
end
def source_from_regex_constructor(node)
_const, _init, regex = *node
case regex.type
when :regexp
source_from_regex_literal(regex)
when :str
source, = *regex
source
end
end
def range(node)
range_between(node.loc.selector.begin_pos, node.source_range.end_pos)
end
def replacement_method(node, first_source, second_source)
replacement = if second_source.empty? && first_source.length == 1
DELETE
else
TR
end
"#{replacement}#{BANG if node.bang_method?}"
end
def message(node, first_source, second_source)
replacement_method = replacement_method(node, first_source, second_source)
format(MSG, prefer: replacement_method, current: node.method_name)
end
def method_suffix(node)
node.loc.end ? node.loc.end.source : ''
end
def remove_second_param(corrector, node, first_param)
end_range = range_between(first_param.source_range.end_pos, node.source_range.end_pos)
corrector.replace(end_range, method_suffix(node))
end
end
end
end
end