Skip to content

Commit b63bee2

Browse files
authored
Add regexp/no-useless-escape rule (#44)
* Add `regexp/no-useless-escape` rule * update
1 parent 1f020fa commit b63bee2

File tree

8 files changed

+403
-40
lines changed

8 files changed

+403
-40
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
9191
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
9292
| [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | |
9393
| [regexp/no-useless-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-character-class.html) | disallow character class with one character | :wrench: |
94+
| [regexp/no-useless-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-escape.html) | disallow unnecessary escape characters in RegExp | |
9495
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
9596
| [regexp/no-useless-non-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-capturing-group.html) | disallow unnecessary Non-capturing group | :wrench: |
9697
| [regexp/no-useless-non-greedy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-greedy.html) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2323
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
2424
| [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | |
2525
| [regexp/no-useless-character-class](./no-useless-character-class.md) | disallow character class with one character | :wrench: |
26+
| [regexp/no-useless-escape](./no-useless-escape.md) | disallow unnecessary escape characters in RegExp | |
2627
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
2728
| [regexp/no-useless-non-capturing-group](./no-useless-non-capturing-group.md) | disallow unnecessary Non-capturing group | :wrench: |
2829
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |

docs/rules/no-useless-escape.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-useless-escape"
5+
description: "disallow unnecessary escape characters in RegExp"
6+
---
7+
# regexp/no-useless-escape
8+
9+
> disallow unnecessary escape characters in RegExp
10+
11+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
12+
13+
## :book: Rule Details
14+
15+
This rule reports unnecessary escape characters in RegExp.
16+
You may be able to find another mistake by finding unnecessary escapes.
17+
18+
<eslint-code-block>
19+
20+
```js
21+
/* eslint regexp/no-useless-escape: "error" */
22+
23+
/* ✓ GOOD */
24+
var foo = /\[/
25+
var foo = /\\/
26+
27+
/* ✗ BAD */
28+
var foo = /\a/
29+
var foo = /\x7/
30+
var foo = /\u41/
31+
var foo = /\u{[41]}/
32+
```
33+
34+
</eslint-code-block>
35+
36+
This rule checks for unnecessary escapes with deeper regular expression parsing than the ESLint core's [no-useless-escape] rule.
37+
38+
<eslint-code-block>
39+
40+
```js
41+
/* eslint no-useless-escape: "error" */
42+
43+
// no-useless-escape rule also reports it.
44+
var foo = /\a/
45+
46+
// no-useless-escape rule DOES NOT report it.
47+
var foo = /\x7/
48+
var foo = /\u41/
49+
var foo = /\u{[41]}/
50+
```
51+
52+
</eslint-code-block>
53+
54+
## :wrench: Options
55+
56+
Nothing.
57+
58+
## :books: Further reading
59+
60+
- [no-useless-escape]
61+
62+
[no-useless-escape]: https://eslint.org/docs/rules/no-useless-escape
63+
64+
## :mag: Implementation
65+
66+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-escape.ts)
67+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-escape.ts)

lib/rules/no-useless-escape.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { Character } from "regexpp/ast"
4+
import {
5+
createRule,
6+
defineRegexpVisitor,
7+
getRegexpLocation,
8+
CP_BACK_SLASH,
9+
CP_STAR,
10+
CP_CLOSING_BRACKET,
11+
CP_SLASH,
12+
CP_CARET,
13+
CP_DOT,
14+
CP_DOLLAR,
15+
CP_PLUS,
16+
CP_QUESTION,
17+
CP_CLOSING_BRACE,
18+
CP_CLOSING_PAREN,
19+
CP_OPENING_BRACE,
20+
CP_OPENING_BRACKET,
21+
CP_OPENING_PAREN,
22+
CP_PIPE,
23+
CP_MINUS,
24+
canUnwrapped,
25+
} from "../utils"
26+
27+
const REGEX_CHAR_CLASS_ESCAPES = new Set([
28+
CP_BACK_SLASH, // \\
29+
CP_CLOSING_BRACKET, // ]
30+
CP_MINUS, // -
31+
])
32+
const REGEX_ESCAPES = new Set([
33+
CP_BACK_SLASH, // \\
34+
CP_SLASH, // /
35+
CP_CARET, // ^
36+
CP_DOT, // .
37+
CP_DOLLAR, // $
38+
CP_STAR, // *
39+
CP_PLUS, // +
40+
CP_QUESTION, // ?
41+
CP_OPENING_BRACKET, // [
42+
CP_CLOSING_BRACKET, // ]
43+
CP_OPENING_BRACE, // {
44+
CP_CLOSING_BRACE, // }
45+
CP_PIPE, // |
46+
CP_OPENING_PAREN, // (
47+
CP_CLOSING_PAREN, // )
48+
])
49+
50+
export default createRule("no-useless-escape", {
51+
meta: {
52+
docs: {
53+
description: "disallow unnecessary escape characters in RegExp",
54+
// TODO In the major version
55+
// recommended: true,
56+
recommended: false,
57+
},
58+
schema: [],
59+
messages: {
60+
unnecessary: "Unnecessary escape character: \\{{character}}.",
61+
},
62+
type: "suggestion", // "problem",
63+
},
64+
create(context) {
65+
const sourceCode = context.getSourceCode()
66+
67+
/**
68+
* Create visitor
69+
* @param node
70+
*/
71+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
72+
/** Report */
73+
function report(
74+
cNode: Character,
75+
offset: number,
76+
character: string,
77+
) {
78+
context.report({
79+
node,
80+
loc: getRegexpLocation(sourceCode, node, cNode, [
81+
offset,
82+
offset + 1,
83+
]),
84+
messageId: "unnecessary",
85+
data: {
86+
character,
87+
},
88+
})
89+
}
90+
91+
let inCharacterClass = false
92+
return {
93+
onCharacterClassEnter() {
94+
inCharacterClass = true
95+
},
96+
onCharacterClassLeave() {
97+
inCharacterClass = false
98+
},
99+
onCharacterEnter(cNode) {
100+
if (cNode.raw.startsWith("\\")) {
101+
// escapes
102+
const char = cNode.raw.slice(1)
103+
if (char === String.fromCodePoint(cNode.value)) {
104+
const allowedEscapes = inCharacterClass
105+
? REGEX_CHAR_CLASS_ESCAPES
106+
: REGEX_ESCAPES
107+
if (allowedEscapes.has(cNode.value)) {
108+
return
109+
}
110+
if (inCharacterClass && cNode.value === CP_CARET) {
111+
const target =
112+
cNode.parent.type === "CharacterClassRange"
113+
? cNode.parent
114+
: cNode
115+
const parent = target.parent
116+
if (parent.type === "CharacterClass") {
117+
if (parent.elements.indexOf(target) === 0) {
118+
// e.g. /[\^]/
119+
return
120+
}
121+
}
122+
}
123+
if (!canUnwrapped(cNode, char)) {
124+
return
125+
}
126+
report(cNode, 0, char)
127+
}
128+
// else if (cNode.value === CP_BACK_SLASH) {
129+
// // Invalid escape for /\c/
130+
// if (cNode.raw === "\\") {
131+
// const parent = cNode.parent
132+
// if (
133+
// parent.type === "Alternative" ||
134+
// parent.type === "CharacterClass"
135+
// ) {
136+
// const next =
137+
// parent.elements[
138+
// parent.elements.indexOf(cNode) + 1
139+
// ]
140+
// if (next && next.raw.length === 1) {
141+
// report(cNode, 0, next.raw)
142+
// }
143+
// }
144+
// }
145+
// }
146+
}
147+
},
148+
}
149+
}
150+
151+
return defineRegexpVisitor(context, {
152+
createVisitor,
153+
})
154+
},
155+
})

lib/utils/index.ts

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@ import type { RuleListener, RuleModule, PartialRuleModule } from "../types"
33
import type { RegExpVisitor } from "regexpp/visitor"
44
import type {
55
Alternative,
6-
AnyCharacterSet,
7-
Assertion,
8-
Backreference,
9-
CapturingGroup,
10-
CharacterClass,
11-
Group,
6+
Element,
127
Node as RegExpNode,
138
Quantifier,
149
} from "regexpp/ast"
@@ -362,17 +357,21 @@ export function getQuantifierOffsets(qNode: Quantifier): [number, number] {
362357
*/
363358
export function canUnwrapped(
364359
/* eslint-enable complexity -- X( */
365-
node:
366-
| CharacterClass
367-
| Group
368-
| CapturingGroup
369-
| Assertion
370-
| Quantifier
371-
| AnyCharacterSet
372-
| Backreference,
360+
node: Element,
373361
text: string,
374362
): boolean {
375-
const { alternative, index } = getAlternativeAndIndex()
363+
const parent = node.parent
364+
let target: Element, alternative: Alternative
365+
if (parent.type === "Quantifier") {
366+
alternative = parent.parent
367+
target = parent
368+
} else if (parent.type === "Alternative") {
369+
alternative = parent
370+
target = node
371+
} else {
372+
return true
373+
}
374+
const index = alternative.elements.indexOf(target)
376375
if (index === 0) {
377376
return true
378377
}
@@ -438,29 +437,4 @@ export function canUnwrapped(
438437
}
439438

440439
return true
441-
442-
/** Get alternative and element index */
443-
function getAlternativeAndIndex() {
444-
const parent = node.parent
445-
let target:
446-
| CharacterClass
447-
| Group
448-
| CapturingGroup
449-
| Assertion
450-
| Quantifier
451-
| AnyCharacterSet
452-
| Backreference,
453-
alt: Alternative
454-
if (parent.type === "Quantifier") {
455-
alt = parent.parent
456-
target = parent
457-
} else {
458-
alt = parent
459-
target = node
460-
}
461-
return {
462-
alternative: alt,
463-
index: alt.elements.indexOf(target),
464-
}
465-
}
466440
}

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import noInvisibleCharacter from "../rules/no-invisible-character"
1111
import noOctal from "../rules/no-octal"
1212
import noUselessBackreference from "../rules/no-useless-backreference"
1313
import noUselessCharacterClass from "../rules/no-useless-character-class"
14+
import noUselessEscape from "../rules/no-useless-escape"
1415
import noUselessExactlyQuantifier from "../rules/no-useless-exactly-quantifier"
1516
import noUselessNonCapturingGroup from "../rules/no-useless-non-capturing-group"
1617
import noUselessNonGreedy from "../rules/no-useless-non-greedy"
@@ -42,6 +43,7 @@ export const rules = [
4243
noOctal,
4344
noUselessBackreference,
4445
noUselessCharacterClass,
46+
noUselessEscape,
4547
noUselessExactlyQuantifier,
4648
noUselessNonCapturingGroup,
4749
noUselessNonGreedy,

lib/utils/unicode.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,25 @@ export const CP_FF = 12
66
export const CP_CR = 13
77
export const CP_SPACE = " ".codePointAt(0)!
88
export const CP_BAN = "!".codePointAt(0)!
9+
export const CP_DOLLAR = "$".codePointAt(0)!
10+
export const CP_OPENING_PAREN = "(".codePointAt(0)!
11+
export const CP_CLOSING_PAREN = ")".codePointAt(0)!
12+
export const CP_STAR = "*".codePointAt(0)!
13+
export const CP_PLUS = "+".codePointAt(0)!
14+
export const CP_MINUS = "-".codePointAt(0)!
15+
export const CP_DOT = ".".codePointAt(0)!
916
export const CP_SLASH = "/".codePointAt(0)!
1017
export const CP_COLON = ":".codePointAt(0)!
18+
export const CP_QUESTION = "?".codePointAt(0)!
1119
export const CP_AT = "@".codePointAt(0)!
1220
export const CP_OPENING_BRACKET = "[".codePointAt(0)!
21+
export const CP_BACK_SLASH = "\\".codePointAt(0)!
22+
export const CP_CLOSING_BRACKET = "]".codePointAt(0)!
23+
export const CP_CARET = "^".codePointAt(0)!
1324
export const CP_BACKTICK = "`".codePointAt(0)!
1425
export const CP_OPENING_BRACE = "{".codePointAt(0)!
26+
export const CP_PIPE = "|".codePointAt(0)!
27+
export const CP_CLOSING_BRACE = "}".codePointAt(0)!
1528
export const CP_TILDE = "~".codePointAt(0)!
1629
export const CP_NEL = "\u0085".codePointAt(0)!
1730
export const CP_NBSP = "\u00a0".codePointAt(0)!

0 commit comments

Comments
 (0)