Skip to content

Commit dada98d

Browse files
authored
Add regexp/no-useless-character-class rule (#28)
* Add `regexp/no-useless-character-class` rule * fixes * fix * Update
1 parent d3205c7 commit dada98d

File tree

10 files changed

+378
-8
lines changed

10 files changed

+378
-8
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
8888
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
8989
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
9090
| [regexp/no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions | |
91+
| [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: |
9192
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
9293
| [regexp/no-useless-two-nums-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-two-nums-quantifier.html) | disallow unnecessary `{n,m}` quantifier | :star: |
9394
| [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2020
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
2121
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
2222
| [regexp/no-useless-backreference](./no-useless-backreference.md) | disallow useless backreferences in regular expressions | |
23+
| [regexp/no-useless-character-class](./no-useless-character-class.md) | disallow character class with one character | :wrench: |
2324
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
2425
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star: |
2526
| [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-useless-character-class"
5+
description: "disallow character class with one character"
6+
---
7+
# regexp/no-useless-character-class
8+
9+
> disallow character class with one character
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+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
13+
14+
## :book: Rule Details
15+
16+
This rule reports character classes that defines only one character.
17+
18+
Character classes that define only one character have the same effect even if you remove the brackets.
19+
20+
<eslint-code-block fix>
21+
22+
```js
23+
/* eslint regexp/no-useless-character-class: "error" */
24+
25+
/* ✓ GOOD */
26+
var foo = /abc/;
27+
28+
/* ✗ BAD */
29+
var foo = /a[b]c/;
30+
```
31+
32+
</eslint-code-block>
33+
34+
## :wrench: Options
35+
36+
```json
37+
{
38+
"regexp/no-useless-character-class": ["error", {
39+
"ignores": ["="]
40+
}]
41+
}
42+
```
43+
44+
- `"ignores"` ... An array of characters and character classes to ignores. Default `["="]`.
45+
46+
The default value is `"="` to prevent conflicts with the [no-div-regex] rule. Note that if you do not specify `"="`, there may be conflicts with the [no-div-regex] rule.
47+
48+
### `"ignores": ["a"]`
49+
50+
<eslint-code-block fix>
51+
52+
```js
53+
/* eslint regexp/no-useless-character-class: ["error", { "ignores": ["a"] }] */
54+
55+
/* ✓ GOOD */
56+
var foo = /[a]bc/;
57+
58+
/* ✗ BAD */
59+
var foo = /a[b]c/;
60+
```
61+
62+
</eslint-code-block>
63+
64+
## :couple: Related rules
65+
66+
- [no-empty-character-class]
67+
- [no-div-regex]
68+
69+
[no-empty-character-class]: https://eslint.org/docs/rules/no-empty-character-class
70+
[no-div-regex]: https://eslint.org/docs/rules/no-div-regex
71+
72+
## :mag: Implementation
73+
74+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-character-class.ts)
75+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-character-class.ts)

lib/rules/no-escape-backspace.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { Expression } from "estree"
22
import type { RegExpVisitor } from "regexpp/visitor"
3-
import { createRule, defineRegexpVisitor, getRegexpLocation } from "../utils"
3+
import {
4+
CP_BACKSPACE,
5+
createRule,
6+
defineRegexpVisitor,
7+
getRegexpLocation,
8+
} from "../utils"
49

510
export default createRule("no-escape-backspace", {
611
meta: {
@@ -24,7 +29,7 @@ export default createRule("no-escape-backspace", {
2429
function createVisitor(node: Expression): RegExpVisitor.Handlers {
2530
return {
2631
onCharacterEnter(cNode) {
27-
if (cNode.value === 8 && cNode.raw === "\\b") {
32+
if (cNode.value === CP_BACKSPACE && cNode.raw === "\\b") {
2833
context.report({
2934
node,
3035
loc: getRegexpLocation(sourceCode, node, cNode),
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import {
4+
createRule,
5+
defineRegexpVisitor,
6+
fixerApplyEscape,
7+
getRegexpLocation,
8+
getRegexpRange,
9+
} from "../utils"
10+
11+
export default createRule("no-useless-character-class", {
12+
meta: {
13+
docs: {
14+
description: "disallow character class with one character",
15+
// TODO In the major version
16+
// recommended: true,
17+
recommended: false,
18+
},
19+
fixable: "code",
20+
schema: [
21+
{
22+
type: "object",
23+
properties: {
24+
ignores: {
25+
type: "array",
26+
items: {
27+
type: "string",
28+
minLength: 1,
29+
},
30+
uniqueItems: true,
31+
},
32+
},
33+
additionalProperties: false,
34+
},
35+
],
36+
messages: {
37+
unexpected:
38+
"Unexpected character class with one {{type}}. Can remove brackets{{additional}}.",
39+
},
40+
type: "suggestion", // "problem",
41+
},
42+
create(context) {
43+
const sourceCode = context.getSourceCode()
44+
const ignores: string[] = context.options[0]?.ignores ?? ["="]
45+
46+
/**
47+
* Create visitor
48+
* @param node
49+
*/
50+
function createVisitor(
51+
node: Expression,
52+
_pattern: string,
53+
flags: string,
54+
): RegExpVisitor.Handlers {
55+
return {
56+
// eslint-disable-next-line complexity -- X(
57+
onCharacterClassEnter(ccNode) {
58+
if (ccNode.elements.length !== 1) {
59+
return
60+
}
61+
if (ccNode.negate) {
62+
return
63+
}
64+
const element = ccNode.elements[0]
65+
if (ignores.length > 0 && ignores.includes(element.raw)) {
66+
return
67+
}
68+
if (element.type === "Character") {
69+
if (element.raw === "\\b") {
70+
// Backspace escape
71+
return
72+
}
73+
if (
74+
/^\\\d+$/.test(element.raw) &&
75+
!element.raw.startsWith("\\0")
76+
) {
77+
// Avoid back reference
78+
return
79+
}
80+
if (
81+
ignores.length > 0 &&
82+
ignores.includes(
83+
String.fromCodePoint(element.value),
84+
)
85+
) {
86+
return
87+
}
88+
} else if (element.type === "CharacterClassRange") {
89+
if (element.min.value !== element.max.value) {
90+
return
91+
}
92+
} else if (element.type !== "CharacterSet") {
93+
return
94+
}
95+
96+
context.report({
97+
node,
98+
loc: getRegexpLocation(sourceCode, node, ccNode),
99+
messageId: "unexpected",
100+
data: {
101+
type:
102+
element.type === "Character"
103+
? "character"
104+
: element.type === "CharacterClassRange"
105+
? "character class range"
106+
: "character set",
107+
additional:
108+
element.type === "CharacterClassRange"
109+
? " and range"
110+
: "",
111+
},
112+
fix(fixer) {
113+
const range = getRegexpRange(
114+
sourceCode,
115+
node,
116+
ccNode,
117+
)
118+
if (range == null) {
119+
return null
120+
}
121+
let text: string =
122+
element.type === "CharacterClassRange"
123+
? element.min.raw
124+
: element.raw
125+
if (
126+
element.type === "Character" ||
127+
element.type === "CharacterClassRange"
128+
) {
129+
if (
130+
/^[.*+?${()|[/]$/u.test(text) ||
131+
(flags.includes("u") && text === "}")
132+
) {
133+
text = fixerApplyEscape("\\", node) + text
134+
}
135+
}
136+
return fixer.replaceTextRange(range, text)
137+
},
138+
})
139+
},
140+
}
141+
}
142+
143+
return defineRegexpVisitor(context, {
144+
createVisitor,
145+
})
146+
},
147+
})

lib/rules/prefer-quantifier.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,6 @@ export default createRule("prefer-quantifier", {
134134
meta: {
135135
docs: {
136136
description: "enforce using quantifier",
137-
// TODO In the major version, it will be changed to "recommended".
138137
recommended: false,
139138
},
140139
fixable: "code",

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import noEscapeBackspace from "../rules/no-escape-backspace"
88
import noInvisibleCharacter from "../rules/no-invisible-character"
99
import noOctal from "../rules/no-octal"
1010
import noUselessBackreference from "../rules/no-useless-backreference"
11+
import noUselessCharacterClass from "../rules/no-useless-character-class"
1112
import noUselessExactlyQuantifier from "../rules/no-useless-exactly-quantifier"
1213
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
1314
import preferD from "../rules/prefer-d"
@@ -28,6 +29,7 @@ export const rules = [
2829
noInvisibleCharacter,
2930
noOctal,
3031
noUselessBackreference,
32+
noUselessCharacterClass,
3133
noUselessExactlyQuantifier,
3234
noUselessTwoNumsQuantifier,
3335
preferD,

lib/utils/unicode.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export const CP_BACKSPACE = 8
12
export const CP_TAB = 9
23
export const CP_LF = 10
34
export const CP_VT = 11
@@ -8,9 +9,9 @@ export const CP_BAN = "!".codePointAt(0)!
89
export const CP_SLASH = "/".codePointAt(0)!
910
export const CP_COLON = ":".codePointAt(0)!
1011
export const CP_AT = "@".codePointAt(0)!
11-
export const CP_OPENING_BRACE = "[".codePointAt(0)!
12+
export const CP_OPENING_BRACKET = "[".codePointAt(0)!
1213
export const CP_BACKTICK = "`".codePointAt(0)!
13-
export const CP_OPENING_BRACKET = "{".codePointAt(0)!
14+
export const CP_OPENING_BRACE = "{".codePointAt(0)!
1415
export const CP_TILDE = "~".codePointAt(0)!
1516
export const CP_NEL = "\u0085".codePointAt(0)!
1617
export const CP_NBSP = "\u00a0".codePointAt(0)!
@@ -108,8 +109,8 @@ export function isSymbol(codePoint: number): boolean {
108109
return (
109110
isCodePointInRange(codePoint, [CP_BAN, CP_SLASH]) ||
110111
isCodePointInRange(codePoint, [CP_COLON, CP_AT]) ||
111-
isCodePointInRange(codePoint, [CP_OPENING_BRACE, CP_BACKTICK]) ||
112-
isCodePointInRange(codePoint, [CP_OPENING_BRACKET, CP_TILDE])
112+
isCodePointInRange(codePoint, [CP_OPENING_BRACKET, CP_BACKTICK]) ||
113+
isCodePointInRange(codePoint, [CP_OPENING_BRACE, CP_TILDE])
113114
)
114115
}
115116

0 commit comments

Comments
 (0)