Skip to content

Commit

Permalink
Add insideCharacterClass option to prefer-d (#343)
Browse files Browse the repository at this point in the history
  • Loading branch information
RunDevelopment committed Sep 29, 2021
1 parent 787f1dc commit 4f41661
Show file tree
Hide file tree
Showing 3 changed files with 167 additions and 30 deletions.
74 changes: 73 additions & 1 deletion docs/rules/prefer-d.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,79 @@ var foo = /[^0-9]/;

## :wrench: Options

Nothing.
```json5
{
"regexp/prefer-d": [
"error",
{
"insideCharacterClass": "d"
}
]
}
```

### `insideCharacterClass`

This option control how character class element equivalent to `\d` will be treated.

*Note:* This option does not affect character classes equivalent to `\d`. E.g. `[\d]`, `[0-9]`, and `[0123456789]` are unaffected.

- `insideCharacterClass: "d"` (_default_)

Character class element equivalent to `\d` will be reported and replaced with `\d`.

<eslint-code-block fix>

```js
/* eslint regexp/prefer-d: ["error", { insideCharacterClass: "d" }] */

/* ✓ GOOD */
var foo = /[\da-z]/;

/* ✗ BAD */
var foo = /[0-9a-z]/;
var foo = /[0-9]/;
```

</eslint-code-block>

- `insideCharacterClass: "range"`

Character class element equivalent to `\d` will be reported and replaced with the range `0-9`.

<eslint-code-block fix>

```js
/* eslint regexp/prefer-d: ["error", { insideCharacterClass: "range" }] */

/* ✓ GOOD */
var foo = /[0-9a-z]/;

/* ✗ BAD */
var foo = /[\da-z]/;
var foo = /[0-9]/;
```

</eslint-code-block>

- `insideCharacterClass: "ignore"`

Character class element will not be reported.

<eslint-code-block fix>

```js
/* eslint regexp/prefer-d: ["error", { insideCharacterClass: "ignore" }] */

/* ✓ GOOD */
var foo = /[\da-z]/;
var foo = /[0-9a-z]/;

/* ✗ BAD */
var foo = /[0-9]/;
```

</eslint-code-block>

## :rocket: Version

Expand Down
88 changes: 60 additions & 28 deletions lib/rules/prefer-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,27 @@ import {
} from "../utils"
import { Chars, toCharSet } from "regexp-ast-analysis"
import { mention } from "../utils/mention"
import type {
CharacterClassElement,
CharacterClassRange,
EscapeCharacterSet,
} from "regexpp/ast"

/**
* Returns whether the given character class element is equivalent to `\d`.
*/
function isDigits(
element: CharacterClassElement,
): element is EscapeCharacterSet | CharacterClassRange {
return (
(element.type === "CharacterSet" &&
element.kind === "digit" &&
!element.negate) ||
(element.type === "CharacterClassRange" &&
element.min.value === CP_DIGIT_ZERO &&
element.max.value === CP_DIGIT_NINE)
)
}

export default createRule("prefer-d", {
meta: {
Expand All @@ -17,14 +38,28 @@ export default createRule("prefer-d", {
recommended: true,
},
fixable: "code",
schema: [],
schema: [
{
type: "object",
properties: {
insideCharacterClass: {
type: "string",
enum: ["ignore", "range", "d"],
},
},
additionalProperties: false,
},
],
messages: {
unexpected:
"Unexpected {{type}} {{expr}}. Use '{{instead}}' instead.",
},
type: "suggestion", // "problem",
type: "suggestion",
},
create(context) {
const insideCharacterClass: "ignore" | "range" | "d" =
context.options[0]?.insideCharacterClass ?? "d"

/**
* Create visitor
*/
Expand All @@ -34,8 +69,6 @@ export default createRule("prefer-d", {
getRegexpLocation,
fixReplaceNode,
}: RegExpContext): RegExpVisitor.Handlers {
let reportedCharacterClass = false

return {
onCharacterClassEnter(ccNode) {
const charSet = toCharSet(ccNode, flags)
Expand All @@ -48,8 +81,6 @@ export default createRule("prefer-d", {
}

if (predefined) {
reportedCharacterClass = true

context.report({
node,
loc: getRegexpLocation(ccNode),
Expand All @@ -61,33 +92,34 @@ export default createRule("prefer-d", {
},
fix: fixReplaceNode(ccNode, predefined),
})
return
}
},
onCharacterClassLeave() {
reportedCharacterClass = false
},
onCharacterClassRangeEnter(ccrNode) {
if (reportedCharacterClass) {

if (insideCharacterClass === "ignore") {
return
}

if (
ccrNode.min.value === CP_DIGIT_ZERO &&
ccrNode.max.value === CP_DIGIT_NINE
) {
const instead = "\\d"
const expected =
insideCharacterClass === "d" ? "\\d" : "0-9"

context.report({
node,
loc: getRegexpLocation(ccrNode),
messageId: "unexpected",
data: {
type: "character class range",
expr: mention(ccrNode),
instead,
},
fix: fixReplaceNode(ccrNode, instead),
})
// check the elements in this character class
for (const e of ccNode.elements) {
if (isDigits(e) && e.raw !== expected) {
context.report({
node,
loc: getRegexpLocation(e),
messageId: "unexpected",
data: {
type:
e.type === "CharacterSet"
? "character set"
: "character class range",
expr: mention(e),
instead: expected,
},
fix: fixReplaceNode(e, expected),
})
}
}
},
}
Expand Down
35 changes: 34 additions & 1 deletion tests/lib/rules/prefer-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,26 @@ const tester = new RuleTester({
})

tester.run("prefer-d", rule as any, {
valid: ["/\\d/", "/[1-9]/"],
valid: [
String.raw`/\d/`,
String.raw`/[1-9]/`,
{
code: String.raw`/[0-9a-z]/`,
options: [{ insideCharacterClass: "ignore" }],
},
{
code: String.raw`/[\da-z]/`,
options: [{ insideCharacterClass: "ignore" }],
},
{
code: String.raw`/[0-9a-z]/`,
options: [{ insideCharacterClass: "range" }],
},
{
code: String.raw`/[\da-z]/`,
options: [{ insideCharacterClass: "d" }],
},
],
invalid: [
{
code: "/[0-9]/",
Expand Down Expand Up @@ -66,5 +85,19 @@ tester.run("prefer-d", rule as any, {
output: null,
errors: ["Unexpected character class '[0-9]'. Use '\\d' instead."],
},
{
code: String.raw`/[0-9a-z]/`,
output: String.raw`/[\da-z]/`,
options: [{ insideCharacterClass: "d" }],
errors: [
"Unexpected character class range '0-9'. Use '\\d' instead.",
],
},
{
code: String.raw`/[\da-z]/`,
output: String.raw`/[0-9a-z]/`,
options: [{ insideCharacterClass: "range" }],
errors: ["Unexpected character set '\\d'. Use '0-9' instead."],
},
],
})

0 comments on commit 4f41661

Please sign in to comment.