1
1
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
2
- import type { Character } from "@eslint-community/regexpp/ast"
2
+ import type {
3
+ Character ,
4
+ CharacterClass ,
5
+ ExpressionCharacterClass ,
6
+ } from "@eslint-community/regexpp/ast"
3
7
import type { RegExpContext } from "../utils"
4
8
import {
5
9
createRule ,
@@ -21,13 +25,38 @@ import {
21
25
CP_PIPE ,
22
26
CP_MINUS ,
23
27
canUnwrapped ,
28
+ CP_HASH ,
29
+ CP_PERCENT ,
30
+ CP_BAN ,
31
+ CP_AMP ,
32
+ CP_COMMA ,
33
+ CP_COLON ,
34
+ CP_SEMI ,
35
+ CP_LT ,
36
+ CP_EQ ,
37
+ CP_GT ,
38
+ CP_AT ,
39
+ CP_TILDE ,
40
+ CP_BACKTICK ,
24
41
} from "../utils"
25
42
26
43
const REGEX_CHAR_CLASS_ESCAPES = new Set ( [
27
44
CP_BACK_SLASH , // \\
28
45
CP_CLOSING_BRACKET , // ]
29
46
CP_MINUS , // -
30
47
] )
48
+ const REGEX_CLASS_SET_CHAR_CLASS_ESCAPE = new Set ( [
49
+ CP_BACK_SLASH , // \\
50
+ CP_SLASH , // /
51
+ CP_OPENING_BRACKET , // [
52
+ CP_CLOSING_BRACKET , // ]
53
+ CP_OPENING_BRACE , // {
54
+ CP_CLOSING_BRACE , // }
55
+ CP_PIPE , // |
56
+ CP_OPENING_PAREN , // (
57
+ CP_CLOSING_PAREN , // )
58
+ CP_MINUS , // -,
59
+ ] )
31
60
const REGEX_ESCAPES = new Set ( [
32
61
CP_BACK_SLASH , // \\
33
62
CP_SLASH , // /
@@ -47,6 +76,33 @@ const REGEX_ESCAPES = new Set([
47
76
] )
48
77
49
78
const POTENTIAL_ESCAPE_SEQUENCE = new Set ( "uxkpP" )
79
+ const POTENTIAL_ESCAPE_SEQUENCE_FOR_CHAR_CLASS = new Set ( [
80
+ ...POTENTIAL_ESCAPE_SEQUENCE ,
81
+ "q" ,
82
+ ] )
83
+ // A single character set of ClassSetReservedDoublePunctuator.
84
+ // && !! ## $$ %% ** ++ ,, .. :: ;; << == >> ?? @@ ^^ `` ~~ are ClassSetReservedDoublePunctuator
85
+ const REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR = new Set ( [
86
+ CP_BAN , // !
87
+ CP_HASH , // #
88
+ CP_DOLLAR , // $
89
+ CP_PERCENT , // %
90
+ CP_AMP , // &
91
+ CP_STAR , // *
92
+ CP_PLUS , // +
93
+ CP_COMMA , // ,
94
+ CP_DOT , // .
95
+ CP_COLON , // :
96
+ CP_SEMI , // ;
97
+ CP_LT , // <
98
+ CP_EQ , // =
99
+ CP_GT , // >
100
+ CP_QUESTION , // ?
101
+ CP_AT , // @
102
+ CP_CARET , // ^
103
+ CP_BACKTICK , // `
104
+ CP_TILDE , // ~
105
+ ] )
50
106
51
107
export default createRule ( "no-useless-escape" , {
52
108
meta : {
@@ -65,6 +121,8 @@ export default createRule("no-useless-escape", {
65
121
create ( context ) {
66
122
function createVisitor ( {
67
123
node,
124
+ flags,
125
+ pattern,
68
126
getRegexpLocation,
69
127
fixReplaceNode,
70
128
} : RegExpContext ) : RegExpVisitor . Handlers {
@@ -85,37 +143,85 @@ export default createRule("no-useless-escape", {
85
143
} )
86
144
}
87
145
88
- let inCharacterClass = false
146
+ const characterClassStack : (
147
+ | CharacterClass
148
+ | ExpressionCharacterClass
149
+ ) [ ] = [ ]
89
150
return {
90
- onCharacterClassEnter ( ) {
91
- inCharacterClass = true
92
- } ,
93
- onCharacterClassLeave ( ) {
94
- inCharacterClass = false
95
- } ,
151
+ onCharacterClassEnter : ( characterClassNode ) =>
152
+ characterClassStack . unshift ( characterClassNode ) ,
153
+ onCharacterClassLeave : ( ) => characterClassStack . shift ( ) ,
154
+ onExpressionCharacterClassEnter : ( characterClassNode ) =>
155
+ characterClassStack . unshift ( characterClassNode ) ,
156
+ onExpressionCharacterClassLeave : ( ) =>
157
+ characterClassStack . shift ( ) ,
96
158
onCharacterEnter ( cNode ) {
97
159
if ( cNode . raw . startsWith ( "\\" ) ) {
98
160
// escapes
99
161
const char = cNode . raw . slice ( 1 )
100
- if ( char === String . fromCodePoint ( cNode . value ) ) {
101
- const allowedEscapes = inCharacterClass
102
- ? REGEX_CHAR_CLASS_ESCAPES
103
- : REGEX_ESCAPES
162
+ const escapedChar = String . fromCodePoint ( cNode . value )
163
+ if ( char === escapedChar ) {
164
+ let allowedEscapes : Set < number >
165
+ if ( characterClassStack . length ) {
166
+ allowedEscapes = flags . unicodeSets
167
+ ? REGEX_CLASS_SET_CHAR_CLASS_ESCAPE
168
+ : REGEX_CHAR_CLASS_ESCAPES
169
+ } else {
170
+ allowedEscapes = REGEX_ESCAPES
171
+ }
104
172
if ( allowedEscapes . has ( cNode . value ) ) {
105
173
return
106
174
}
107
- if ( inCharacterClass && cNode . value === CP_CARET ) {
108
- const target =
109
- cNode . parent . type === "CharacterClassRange"
110
- ? cNode . parent
111
- : cNode
112
- const parent = target . parent
113
- if ( parent . type === "CharacterClass" ) {
114
- if ( parent . elements . indexOf ( target ) === 0 ) {
175
+ if ( characterClassStack . length ) {
176
+ const characterClassNode =
177
+ characterClassStack [ 0 ]
178
+ if ( cNode . value === CP_CARET ) {
179
+ if (
180
+ characterClassNode . start + 1 ===
181
+ cNode . start
182
+ ) {
115
183
// e.g. /[\^]/
116
184
return
117
185
}
118
186
}
187
+ if ( flags . unicodeSets ) {
188
+ if (
189
+ REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR . has (
190
+ cNode . value ,
191
+ )
192
+ ) {
193
+ if (
194
+ pattern [ cNode . end ] === escapedChar
195
+ ) {
196
+ // Escaping is valid if it is a ClassSetReservedDoublePunctuator.
197
+ return
198
+ }
199
+ const prevIndex = cNode . start - 1
200
+ if (
201
+ pattern [ prevIndex ] === escapedChar
202
+ ) {
203
+ if ( escapedChar !== "^" ) {
204
+ // e.g. [&\&]
205
+ // ^ // If it's the second character, it's a valid escape.
206
+ return
207
+ }
208
+ const elementStartIndex =
209
+ characterClassNode . start +
210
+ 1 + // opening bracket(`[`)
211
+ ( characterClassNode . negate
212
+ ? 1 // `negate` caret(`^`)
213
+ : 0 )
214
+ if (
215
+ elementStartIndex <= prevIndex
216
+ ) {
217
+ // [^^\^], [_^\^]
218
+ // ^ ^ // If it's the second caret(`^`) character, it's a valid escape.
219
+ // But [^\^] is unnecessary escape.
220
+ return
221
+ }
222
+ }
223
+ }
224
+ }
119
225
}
120
226
if ( ! canUnwrapped ( cNode , char ) ) {
121
227
return
@@ -124,7 +230,11 @@ export default createRule("no-useless-escape", {
124
230
cNode ,
125
231
0 ,
126
232
char ,
127
- ! POTENTIAL_ESCAPE_SEQUENCE . has ( char ) ,
233
+ ! (
234
+ characterClassStack . length
235
+ ? POTENTIAL_ESCAPE_SEQUENCE_FOR_CHAR_CLASS
236
+ : POTENTIAL_ESCAPE_SEQUENCE
237
+ ) . has ( char ) ,
128
238
)
129
239
}
130
240
}
0 commit comments