forked from rubocop/rubocop
/
redundant_regexp_escape.rb
136 lines (116 loc) · 3.72 KB
/
redundant_regexp_escape.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
# frozen_string_literal: true
module RuboCop
module Cop
module Style
# This cop checks for redundant escapes inside Regexp literals.
#
# @example
# # bad
# %r{foo\/bar}
#
# # good
# %r{foo/bar}
#
# # good
# /foo\/bar/
#
# # good
# %r/foo\/bar/
#
# # good
# %r!foo\!bar!
#
# # bad
# /a\-b/
#
# # good
# /a-b/
#
# # bad
# /[\+\-]\d/
#
# # good
# /[+\-]\d/
class RedundantRegexpEscape < Cop
include RangeHelp
include RegexpLiteralHelp
MSG_REDUNDANT_ESCAPE = 'Redundant escape inside regexp literal'
ALLOWED_ALWAYS_ESCAPES = ' []^\\#'.chars.freeze
ALLOWED_WITHIN_CHAR_CLASS_METACHAR_ESCAPES = '-'.chars.freeze
ALLOWED_OUTSIDE_CHAR_CLASS_METACHAR_ESCAPES = '.*+?{}()|$'.chars.freeze
def on_regexp(node)
each_escape(node) do |char, index, within_character_class|
next if allowed_escape?(node, char, within_character_class)
add_offense(
node,
location: escape_range_at_index(node, index),
message: MSG_REDUNDANT_ESCAPE
)
end
end
def autocorrect(node)
lambda do |corrector|
each_escape(node) do |char, index, within_character_class|
next if allowed_escape?(node, char, within_character_class)
corrector.remove_leading(escape_range_at_index(node, index), 1)
end
end
end
private
def allowed_escape?(node, char, within_character_class)
# Strictly speaking a few single-letter metachars are currently
# unnecessary to "escape", e.g. g, i, E, F, but enumerating them is
# rather difficult, and their behaviour could change over time with
# different versions of Ruby so that e.g. /\g/ != /g/
return true if /[[:alnum:]]/.match?(char)
return true if ALLOWED_ALWAYS_ESCAPES.include?(char) || delimiter?(node, char)
if within_character_class
ALLOWED_WITHIN_CHAR_CLASS_METACHAR_ESCAPES.include?(char)
else
ALLOWED_OUTSIDE_CHAR_CLASS_METACHAR_ESCAPES.include?(char)
end
end
def delimiter?(node, char)
delimiters = [
node.loc.begin.source.chars.last,
node.loc.end.source.chars.first
]
delimiters.include?(char)
end
def each_escape(node)
pattern_source(node).each_char.with_index.reduce(
[nil, false]
) do |(previous, within_character_class), (current, index)|
if previous == '\\'
yield [current, index - 1, within_character_class]
[nil, within_character_class]
elsif previous == '[' && current != ':'
[current, true]
elsif previous != ':' && current == ']'
[current, false]
else
[current, within_character_class]
end
end
end
def escape_range_at_index(node, index)
regexp_begin = node.loc.begin.end_pos
start = regexp_begin + index
range_between(start, start + 2)
end
def pattern_source(node)
freespace_mode = freespace_mode_regexp?(node)
node.children.reject(&:regopt_type?).map do |child|
source = child.source
if freespace_mode
# Remove comments to avoid misleading results
source.sub(/(?<!\\)#.*/, '')
else
source
end
end.join
end
end
end
end
end