@@ -5,6 +5,7 @@ import type {
5
5
CharacterClass ,
6
6
CharacterClassElement ,
7
7
ExpressionCharacterClass ,
8
+ UnicodeSetsCharacterClass ,
8
9
} from "@eslint-community/regexpp/ast"
9
10
10
11
const ESCAPES_OUTSIDE_CHARACTER_CLASS = new Set ( "$()*+./?[{|" )
@@ -89,7 +90,7 @@ export default createRule("no-useless-character-class", {
89
90
? getEscapedFirstRawIfNeeded ( element )
90
91
: null ) ??
91
92
( index === ccNode . elements . length - 1
92
- ? getEscapedLatsRawIfNeeded ( element )
93
+ ? getEscapedLastRawIfNeeded ( element )
93
94
: null ) ??
94
95
element . raw
95
96
)
@@ -103,6 +104,21 @@ export default createRule("no-useless-character-class", {
103
104
messageData = {
104
105
type : "unnecessary nesting character class" ,
105
106
}
107
+ if ( ! ccNode . elements . length ) {
108
+ // empty character class
109
+ const nextElement =
110
+ ccNode . parent . elements [
111
+ ccNode . parent . elements . indexOf (
112
+ ccNode as UnicodeSetsCharacterClass ,
113
+ ) + 1
114
+ ]
115
+ if (
116
+ nextElement &&
117
+ isNeedEscapedForFirstElement ( nextElement )
118
+ ) {
119
+ unwrapped . push ( "\\" ) // Add a backslash to escape the next character.
120
+ }
121
+ }
106
122
} else {
107
123
if ( ccNode . elements . length !== 1 ) {
108
124
return
@@ -148,7 +164,7 @@ export default createRule("no-useless-character-class", {
148
164
}
149
165
unwrapped [ 0 ] =
150
166
getEscapedFirstRawIfNeeded ( element . min ) ??
151
- getEscapedLatsRawIfNeeded ( element . min ) ??
167
+ getEscapedLastRawIfNeeded ( element . min ) ??
152
168
element . min . raw
153
169
} else if ( element . type === "ClassStringDisjunction" ) {
154
170
if ( ! characterClassStack . length ) {
@@ -181,82 +197,114 @@ export default createRule("no-useless-character-class", {
181
197
} )
182
198
183
199
/**
184
- * Returns the escaped raw text, if the given first element requires escaping.
185
- * Otherwise, returns null .
200
+ * Checks whether an escape is required if the given element is placed first
201
+ * after character class replacement .
186
202
*/
187
- function getEscapedFirstRawIfNeeded (
188
- firstElement : CharacterClassElement ,
203
+ function isNeedEscapedForFirstElement (
204
+ element : CharacterClassElement ,
189
205
) {
190
- const firstRaw =
191
- firstElement . type === "Character"
192
- ? firstElement . raw
193
- : firstElement . type === "CharacterClassRange"
194
- ? firstElement . min . raw
206
+ const char =
207
+ element . type === "Character"
208
+ ? element . raw
209
+ : element . type === "CharacterClassRange"
210
+ ? element . min . raw
195
211
: null
196
- if ( firstRaw == null ) {
197
- return null
212
+ if ( char == null ) {
213
+ return false
198
214
}
199
215
if ( characterClassStack . length ) {
200
216
// Nesting character class
201
217
202
218
// Avoid [A&&[&]] => [A&&&]
203
219
if (
204
220
REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR . has (
205
- firstRaw ,
221
+ char ,
206
222
) &&
207
223
// The previous character is the same
208
- pattern [ ccNode . start - 1 ] === firstRaw
224
+ pattern [ ccNode . start - 1 ] === char
209
225
) {
210
- return `\\ ${ firstElement . raw } `
226
+ return true
211
227
}
212
- return null
228
+
229
+ // Avoid [[]^] => [^]
230
+ return (
231
+ char === "^" &&
232
+ ccNode . parent . type === "CharacterClass" &&
233
+ ccNode . parent . elements [ 0 ] === ccNode
234
+ )
213
235
}
214
236
215
237
// Flat character class
216
- if (
217
- ( flags . unicode
238
+ return (
239
+ flags . unicode
218
240
? ESCAPES_OUTSIDE_CHARACTER_CLASS_WITH_U
219
241
: ESCAPES_OUTSIDE_CHARACTER_CLASS
220
- ) . has ( firstRaw )
221
- ) {
222
- return `\\${ firstElement . raw } `
223
- }
224
- return null
242
+ ) . has ( char )
225
243
}
226
244
227
245
/**
228
- * Returns the escaped raw text, if the given last element requires escaping.
229
- * Otherwise, returns null .
246
+ * Checks whether an escape is required if the given element is placed last
247
+ * after character class replacement .
230
248
*/
231
- function getEscapedLatsRawIfNeeded (
232
- lastElement : CharacterClassElement ,
249
+ function needEscapedForLastElement (
250
+ element : CharacterClassElement ,
233
251
) {
234
- const lastRaw =
235
- lastElement . type === "Character"
236
- ? lastElement . raw
237
- : lastElement . type === "CharacterClassRange"
238
- ? lastElement . max . raw
252
+ const char =
253
+ element . type === "Character"
254
+ ? element . raw
255
+ : element . type === "CharacterClassRange"
256
+ ? element . max . raw
239
257
: null
240
- if ( lastRaw == null ) {
241
- return null
258
+ if ( char == null ) {
259
+ return false
242
260
}
243
261
if ( characterClassStack . length ) {
244
262
// Nesting character class
245
263
246
264
// Avoid [A[&]&B] => [A&&B]
247
- if (
265
+ return (
248
266
REGEX_CLASS_SET_RESERVED_DOUBLE_PUNCTUATOR . has (
249
- lastRaw ,
267
+ char ,
250
268
) &&
251
269
// The next character is the same
252
- pattern [ ccNode . end ] === lastRaw
253
- ) {
254
- const prefix = lastElement . raw . slice (
255
- 0 ,
256
- - lastRaw . length ,
257
- )
258
- return `${ prefix } \\${ lastRaw } `
259
- }
270
+ pattern [ ccNode . end ] === char
271
+ )
272
+ }
273
+ return false
274
+ }
275
+
276
+ /**
277
+ * Returns the escaped raw text, if the given first element requires escaping.
278
+ * Otherwise, returns null.
279
+ */
280
+ function getEscapedFirstRawIfNeeded (
281
+ firstElement : CharacterClassElement ,
282
+ ) {
283
+ if ( isNeedEscapedForFirstElement ( firstElement ) ) {
284
+ return `\\${ firstElement . raw } `
285
+ }
286
+ return null
287
+ }
288
+
289
+ /**
290
+ * Returns the escaped raw text, if the given last element requires escaping.
291
+ * Otherwise, returns null.
292
+ */
293
+ function getEscapedLastRawIfNeeded (
294
+ lastElement : CharacterClassElement ,
295
+ ) {
296
+ if ( needEscapedForLastElement ( lastElement ) ) {
297
+ const lastRaw =
298
+ lastElement . type === "Character"
299
+ ? lastElement . raw
300
+ : lastElement . type === "CharacterClassRange"
301
+ ? lastElement . max . raw
302
+ : "" // never
303
+ const prefix = lastElement . raw . slice (
304
+ 0 ,
305
+ - lastRaw . length ,
306
+ )
307
+ return `${ prefix } \\${ lastRaw } `
260
308
}
261
309
return null
262
310
}
0 commit comments