Skip to content

Commit 4f41661

Browse files
Add insideCharacterClass option to prefer-d (#343)
1 parent 787f1dc commit 4f41661

File tree

3 files changed

+167
-30
lines changed

3 files changed

+167
-30
lines changed

docs/rules/prefer-d.md

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,79 @@ var foo = /[^0-9]/;
3434

3535
## :wrench: Options
3636

37-
Nothing.
37+
```json5
38+
{
39+
"regexp/prefer-d": [
40+
"error",
41+
{
42+
"insideCharacterClass": "d"
43+
}
44+
]
45+
}
46+
```
47+
48+
### `insideCharacterClass`
49+
50+
This option control how character class element equivalent to `\d` will be treated.
51+
52+
*Note:* This option does not affect character classes equivalent to `\d`. E.g. `[\d]`, `[0-9]`, and `[0123456789]` are unaffected.
53+
54+
- `insideCharacterClass: "d"` (_default_)
55+
56+
Character class element equivalent to `\d` will be reported and replaced with `\d`.
57+
58+
<eslint-code-block fix>
59+
60+
```js
61+
/* eslint regexp/prefer-d: ["error", { insideCharacterClass: "d" }] */
62+
63+
/* ✓ GOOD */
64+
var foo = /[\da-z]/;
65+
66+
/* ✗ BAD */
67+
var foo = /[0-9a-z]/;
68+
var foo = /[0-9]/;
69+
```
70+
71+
</eslint-code-block>
72+
73+
- `insideCharacterClass: "range"`
74+
75+
Character class element equivalent to `\d` will be reported and replaced with the range `0-9`.
76+
77+
<eslint-code-block fix>
78+
79+
```js
80+
/* eslint regexp/prefer-d: ["error", { insideCharacterClass: "range" }] */
81+
82+
/* ✓ GOOD */
83+
var foo = /[0-9a-z]/;
84+
85+
/* ✗ BAD */
86+
var foo = /[\da-z]/;
87+
var foo = /[0-9]/;
88+
```
89+
90+
</eslint-code-block>
91+
92+
- `insideCharacterClass: "ignore"`
93+
94+
Character class element will not be reported.
95+
96+
<eslint-code-block fix>
97+
98+
```js
99+
/* eslint regexp/prefer-d: ["error", { insideCharacterClass: "ignore" }] */
100+
101+
/* ✓ GOOD */
102+
var foo = /[\da-z]/;
103+
var foo = /[0-9a-z]/;
104+
105+
/* ✗ BAD */
106+
var foo = /[0-9]/;
107+
```
108+
109+
</eslint-code-block>
38110

39111
## :rocket: Version
40112

lib/rules/prefer-d.ts

Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@ import {
88
} from "../utils"
99
import { Chars, toCharSet } from "regexp-ast-analysis"
1010
import { mention } from "../utils/mention"
11+
import type {
12+
CharacterClassElement,
13+
CharacterClassRange,
14+
EscapeCharacterSet,
15+
} from "regexpp/ast"
16+
17+
/**
18+
* Returns whether the given character class element is equivalent to `\d`.
19+
*/
20+
function isDigits(
21+
element: CharacterClassElement,
22+
): element is EscapeCharacterSet | CharacterClassRange {
23+
return (
24+
(element.type === "CharacterSet" &&
25+
element.kind === "digit" &&
26+
!element.negate) ||
27+
(element.type === "CharacterClassRange" &&
28+
element.min.value === CP_DIGIT_ZERO &&
29+
element.max.value === CP_DIGIT_NINE)
30+
)
31+
}
1132

1233
export default createRule("prefer-d", {
1334
meta: {
@@ -17,14 +38,28 @@ export default createRule("prefer-d", {
1738
recommended: true,
1839
},
1940
fixable: "code",
20-
schema: [],
41+
schema: [
42+
{
43+
type: "object",
44+
properties: {
45+
insideCharacterClass: {
46+
type: "string",
47+
enum: ["ignore", "range", "d"],
48+
},
49+
},
50+
additionalProperties: false,
51+
},
52+
],
2153
messages: {
2254
unexpected:
2355
"Unexpected {{type}} {{expr}}. Use '{{instead}}' instead.",
2456
},
25-
type: "suggestion", // "problem",
57+
type: "suggestion",
2658
},
2759
create(context) {
60+
const insideCharacterClass: "ignore" | "range" | "d" =
61+
context.options[0]?.insideCharacterClass ?? "d"
62+
2863
/**
2964
* Create visitor
3065
*/
@@ -34,8 +69,6 @@ export default createRule("prefer-d", {
3469
getRegexpLocation,
3570
fixReplaceNode,
3671
}: RegExpContext): RegExpVisitor.Handlers {
37-
let reportedCharacterClass = false
38-
3972
return {
4073
onCharacterClassEnter(ccNode) {
4174
const charSet = toCharSet(ccNode, flags)
@@ -48,8 +81,6 @@ export default createRule("prefer-d", {
4881
}
4982

5083
if (predefined) {
51-
reportedCharacterClass = true
52-
5384
context.report({
5485
node,
5586
loc: getRegexpLocation(ccNode),
@@ -61,33 +92,34 @@ export default createRule("prefer-d", {
6192
},
6293
fix: fixReplaceNode(ccNode, predefined),
6394
})
95+
return
6496
}
65-
},
66-
onCharacterClassLeave() {
67-
reportedCharacterClass = false
68-
},
69-
onCharacterClassRangeEnter(ccrNode) {
70-
if (reportedCharacterClass) {
97+
98+
if (insideCharacterClass === "ignore") {
7199
return
72100
}
73101

74-
if (
75-
ccrNode.min.value === CP_DIGIT_ZERO &&
76-
ccrNode.max.value === CP_DIGIT_NINE
77-
) {
78-
const instead = "\\d"
102+
const expected =
103+
insideCharacterClass === "d" ? "\\d" : "0-9"
79104

80-
context.report({
81-
node,
82-
loc: getRegexpLocation(ccrNode),
83-
messageId: "unexpected",
84-
data: {
85-
type: "character class range",
86-
expr: mention(ccrNode),
87-
instead,
88-
},
89-
fix: fixReplaceNode(ccrNode, instead),
90-
})
105+
// check the elements in this character class
106+
for (const e of ccNode.elements) {
107+
if (isDigits(e) && e.raw !== expected) {
108+
context.report({
109+
node,
110+
loc: getRegexpLocation(e),
111+
messageId: "unexpected",
112+
data: {
113+
type:
114+
e.type === "CharacterSet"
115+
? "character set"
116+
: "character class range",
117+
expr: mention(e),
118+
instead: expected,
119+
},
120+
fix: fixReplaceNode(e, expected),
121+
})
122+
}
91123
}
92124
},
93125
}

tests/lib/rules/prefer-d.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,26 @@ const tester = new RuleTester({
99
})
1010

1111
tester.run("prefer-d", rule as any, {
12-
valid: ["/\\d/", "/[1-9]/"],
12+
valid: [
13+
String.raw`/\d/`,
14+
String.raw`/[1-9]/`,
15+
{
16+
code: String.raw`/[0-9a-z]/`,
17+
options: [{ insideCharacterClass: "ignore" }],
18+
},
19+
{
20+
code: String.raw`/[\da-z]/`,
21+
options: [{ insideCharacterClass: "ignore" }],
22+
},
23+
{
24+
code: String.raw`/[0-9a-z]/`,
25+
options: [{ insideCharacterClass: "range" }],
26+
},
27+
{
28+
code: String.raw`/[\da-z]/`,
29+
options: [{ insideCharacterClass: "d" }],
30+
},
31+
],
1332
invalid: [
1433
{
1534
code: "/[0-9]/",
@@ -66,5 +85,19 @@ tester.run("prefer-d", rule as any, {
6685
output: null,
6786
errors: ["Unexpected character class '[0-9]'. Use '\\d' instead."],
6887
},
88+
{
89+
code: String.raw`/[0-9a-z]/`,
90+
output: String.raw`/[\da-z]/`,
91+
options: [{ insideCharacterClass: "d" }],
92+
errors: [
93+
"Unexpected character class range '0-9'. Use '\\d' instead.",
94+
],
95+
},
96+
{
97+
code: String.raw`/[\da-z]/`,
98+
output: String.raw`/[0-9a-z]/`,
99+
options: [{ insideCharacterClass: "range" }],
100+
errors: ["Unexpected character set '\\d'. Use '0-9' instead."],
101+
},
69102
],
70103
})

0 commit comments

Comments
 (0)