Skip to content

Commit 6732d59

Browse files
authored
Add support for v flag to regexp/no-useless-escape rule (#585)
* Add support for v flag to `regexp/no-useless-escape` rule * Create six-squids-look.md * fix for string
1 parent 1471ea5 commit 6732d59

File tree

4 files changed

+567
-22
lines changed

4 files changed

+567
-22
lines changed

.changeset/six-squids-look.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": minor
3+
---
4+
5+
Add support for v flag to `regexp/no-useless-escape` rule

lib/rules/no-useless-escape.ts

Lines changed: 131 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
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"
37
import type { RegExpContext } from "../utils"
48
import {
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

2643
const 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+
])
3160
const REGEX_ESCAPES = new Set([
3261
CP_BACK_SLASH, // \\
3362
CP_SLASH, // /
@@ -47,6 +76,33 @@ const REGEX_ESCAPES = new Set([
4776
])
4877

4978
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+
])
50106

51107
export 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
}

lib/utils/unicode.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,23 @@ export const CP_FF = 12
88
export const CP_CR = 13
99
export const CP_SPACE = " ".codePointAt(0)!
1010
export const CP_BAN = "!".codePointAt(0)!
11+
export const CP_HASH = "#".codePointAt(0)
1112
export const CP_DOLLAR = "$".codePointAt(0)!
13+
export const CP_PERCENT = "%".codePointAt(0)!
14+
export const CP_AMP = "&".codePointAt(0)!
1215
export const CP_OPENING_PAREN = "(".codePointAt(0)!
1316
export const CP_CLOSING_PAREN = ")".codePointAt(0)!
1417
export const CP_STAR = "*".codePointAt(0)!
1518
export const CP_PLUS = "+".codePointAt(0)!
19+
export const CP_COMMA = ",".codePointAt(0)!
1620
export const CP_MINUS = "-".codePointAt(0)!
1721
export const CP_DOT = ".".codePointAt(0)!
1822
export const CP_SLASH = "/".codePointAt(0)!
1923
export const CP_COLON = ":".codePointAt(0)!
24+
export const CP_SEMI = ";".codePointAt(0)!
25+
export const CP_LT = "<".codePointAt(0)!
26+
export const CP_EQ = "=".codePointAt(0)!
27+
export const CP_GT = ">".codePointAt(0)!
2028
export const CP_QUESTION = "?".codePointAt(0)!
2129
export const CP_AT = "@".codePointAt(0)!
2230
export const CP_OPENING_BRACKET = "[".codePointAt(0)!

0 commit comments

Comments
 (0)