11import 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"
37import type { RegExpContext } from "../utils"
48import {
59 createRule ,
@@ -21,13 +25,38 @@ import {
2125 CP_PIPE ,
2226 CP_MINUS ,
2327 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 ,
2441} from "../utils"
2542
2643const REGEX_CHAR_CLASS_ESCAPES = new Set ( [
2744 CP_BACK_SLASH , // \\
2845 CP_CLOSING_BRACKET , // ]
2946 CP_MINUS , // -
3047] )
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+ ] )
3160const REGEX_ESCAPES = new Set ( [
3261 CP_BACK_SLASH , // \\
3362 CP_SLASH , // /
@@ -47,6 +76,33 @@ const REGEX_ESCAPES = new Set([
4776] )
4877
4978const 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+ ] )
50106
51107export default createRule ( "no-useless-escape" , {
52108 meta : {
@@ -65,6 +121,8 @@ export default createRule("no-useless-escape", {
65121 create ( context ) {
66122 function createVisitor ( {
67123 node,
124+ flags,
125+ pattern,
68126 getRegexpLocation,
69127 fixReplaceNode,
70128 } : RegExpContext ) : RegExpVisitor . Handlers {
@@ -85,37 +143,85 @@ export default createRule("no-useless-escape", {
85143 } )
86144 }
87145
88- let inCharacterClass = false
146+ const characterClassStack : (
147+ | CharacterClass
148+ | ExpressionCharacterClass
149+ ) [ ] = [ ]
89150 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 ( ) ,
96158 onCharacterEnter ( cNode ) {
97159 if ( cNode . raw . startsWith ( "\\" ) ) {
98160 // escapes
99161 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+ }
104172 if ( allowedEscapes . has ( cNode . value ) ) {
105173 return
106174 }
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+ ) {
115183 // e.g. /[\^]/
116184 return
117185 }
118186 }
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+ }
119225 }
120226 if ( ! canUnwrapped ( cNode , char ) ) {
121227 return
@@ -124,7 +230,11 @@ export default createRule("no-useless-escape", {
124230 cNode ,
125231 0 ,
126232 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 ) ,
128238 )
129239 }
130240 }
0 commit comments