File: casecmp.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 (110 lines) | stat: -rw-r--r-- 3,281 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
# frozen_string_literal: true

module RuboCop
  module Cop
    module Performance
      # This cop identifies places where a case-insensitive string comparison
      # can better be implemented using `casecmp`.
      # This cop is unsafe because `String#casecmp` and `String#casecmp?` behave
      # differently when using Non-ASCII characters.
      #
      # @example
      #   # bad
      #   str.downcase == 'abc'
      #   str.upcase.eql? 'ABC'
      #   'abc' == str.downcase
      #   'ABC'.eql? str.upcase
      #   str.downcase == str.downcase
      #
      #   # good
      #   str.casecmp('ABC').zero?
      #   'abc'.casecmp(str).zero?
      class Casecmp < Cop
        MSG = 'Use `%<good>s` instead of `%<bad>s`.'
        CASE_METHODS = %i[downcase upcase].freeze

        def_node_matcher :downcase_eq, <<~PATTERN
          (send
            $(send _ ${:downcase :upcase})
            ${:== :eql? :!=}
            ${str (send _ {:downcase :upcase} ...) (begin str)})
        PATTERN

        def_node_matcher :eq_downcase, <<~PATTERN
          (send
            {str (send _ {:downcase :upcase} ...) (begin str)}
            ${:== :eql? :!=}
            $(send _ ${:downcase :upcase}))
        PATTERN

        def_node_matcher :downcase_downcase, <<~PATTERN
          (send
            $(send _ ${:downcase :upcase})
            ${:== :eql? :!=}
            $(send _ ${:downcase :upcase}))
        PATTERN

        def on_send(node)
          return unless downcase_eq(node) || eq_downcase(node)
          return unless (parts = take_method_apart(node))

          _, _, arg, variable = parts
          good_method = build_good_method(arg, variable)

          add_offense(
            node,
            message: format(MSG, good: good_method, bad: node.source)
          )
        end

        def autocorrect(node)
          return unless (parts = take_method_apart(node))

          receiver, method, arg, variable = parts

          correction(node, receiver, method, arg, variable)
        end

        private

        def take_method_apart(node)
          if downcase_downcase(node)
            receiver, method, rhs = *node
            arg, = *rhs
          elsif downcase_eq(node)
            receiver, method, arg = *node
          elsif eq_downcase(node)
            arg, method, receiver = *node
          else
            return
          end

          variable, = *receiver

          [receiver, method, arg, variable]
        end

        def correction(node, _receiver, method, arg, variable)
          lambda do |corrector|
            corrector.insert_before(node.loc.expression, '!') if method == :!=

            replacement = build_good_method(arg, variable)

            corrector.replace(node.loc.expression, replacement)
          end
        end

        def build_good_method(arg, variable)
          # We want resulting call to be parenthesized
          # if arg already includes one or more sets of parens, don't add more
          # or if method call already used parens, again, don't add more
          if arg.send_type? || !parentheses?(arg)
            "#{variable.source}.casecmp(#{arg.source}).zero?"
          else
            "#{variable.source}.casecmp#{arg.source}.zero?"
          end
        end
      end
    end
  end
end