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 162 163 164 165 166 167
|
# frozen_string_literal: true
module RuboCop
module Cop
module Performance
# This cop 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 < Cop
include RangeHelp
MSG = 'Use `%<prefer>s` instead of `%<current>s`.'
DETERMINISTIC_REGEX = /\A(?:#{LITERAL_REGEX})+\Z/.freeze
DELETE = 'delete'
TR = 'tr'
BANG = '!'
def_node_matcher :string_replacement?, <<~PATTERN
(send _ {: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
def autocorrect(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?
replacement_method =
replacement_method(node, first_source, second_source)
replace_method(node, first_source, second_source, first_param,
replacement_method)
end
def replace_method(node, first, second, first_param, replacement)
lambda do |corrector|
corrector.replace(node.loc.selector, replacement)
unless first_param.str_type?
corrector.replace(first_param.source_range,
to_string_literal(first))
end
remove_second_param(corrector, node, first_param) if second.empty? && first.length == 1
end
end
private
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 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(node, location: range(node), message: message)
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
|