File: string_replacement.rb

package info (click to toggle)
ruby-rubocop-performance 1.7.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 792 kB
  • sloc: ruby: 6,722; makefile: 8
file content (167 lines) | stat: -rw-r--r-- 5,196 bytes parent folder | download | duplicates (3)
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