Skip to content

Commit 49b024f

Browse files
authored
Add target option to regexp/prefer-range rule, and change default. (#56)
* Add `target` option to `regexp/prefer-range` rule, and change default. * fix option schema
1 parent 7b373ad commit 49b024f

File tree

3 files changed

+237
-5
lines changed

3 files changed

+237
-5
lines changed

docs/rules/prefer-range.md

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,94 @@ var foo = /[a-f]/
2727
/* ✗ BAD */
2828
var foo = /[abc]/
2929
var foo = /[a-cd-f]/
30-
3130
```
3231

3332
</eslint-code-block>
3433

3534
## :wrench: Options
3635

37-
Nothing.
36+
```json5
37+
{
38+
"regexp/prefer-range": ["error",
39+
{
40+
"target": "alphanumeric" // or "all" or [...]
41+
}
42+
]
43+
}
44+
```
45+
46+
- `target` ... Specify the range of characters you want to check with this rule.
47+
- `"alphanumeric"` ... Check only alphanumeric characters (`0-9`,`a-z` and `A-Z`). This is the default.
48+
- `"all"` ... Check all characters. Use `"all"`, if you want to focus on regular expression optimization.
49+
- `[...]` (Array) ... Specify as an array of character ranges. List the character ranges that your team is familiar with in this option, and replace redundant contiguous characters with ranges.
50+
Specify the range as a three-character string in which the from and to characters are connected with a hyphen (`-`) using. e.g. `"!-/"` (U+0021 - U+002F), `"😀-😏"` (U+1F600 - U+1F60F)
51+
You can also use `"alphanumeric"`.
52+
53+
### `"target": "alphanumeric"` (Default)
54+
55+
<eslint-code-block fix>
56+
57+
```js
58+
/* eslint regexp/prefer-range: ["error", { "target": "alphanumeric" }] */
59+
60+
/* ✓ GOOD */
61+
var foo = /[a-c]/
62+
var foo = /[a-f]/
63+
var foo = /[!-$]/
64+
var foo = /[!"#$]/
65+
var foo = /[😀-😄]/u
66+
var foo = /[😀😁😂😃😄]/u
67+
68+
/* ✗ BAD */
69+
var foo = /[abc]/
70+
var foo = /[a-cd-f]/
71+
```
72+
73+
</eslint-code-block>
74+
75+
### `"target": "all"`
76+
77+
<eslint-code-block fix>
78+
79+
```js
80+
/* eslint regexp/prefer-range: ["error", { "target": "all" }] */
81+
82+
/* ✓ GOOD */
83+
var foo = /[a-c]/
84+
var foo = /[a-f]/
85+
var foo = /[!-$]/
86+
var foo = /[😀-😄]/u
87+
88+
/* ✗ BAD */
89+
var foo = /[abc]/
90+
var foo = /[a-cd-f]/
91+
var foo = /[!"#$]/
92+
var foo = /[😀😁😂😃😄]/u
93+
```
94+
95+
</eslint-code-block>
96+
97+
### `"target": [ "alphanumeric", "😀-😏" ]`
98+
99+
<eslint-code-block fix>
100+
101+
```js
102+
/* eslint regexp/prefer-range: ["error", { "target": [ "alphanumeric", "😀-😏" ] }] */
103+
104+
/* ✓ GOOD */
105+
var foo = /[a-c]/
106+
var foo = /[a-f]/
107+
var foo = /[!-$]/
108+
var foo = /[!"#$]/
109+
var foo = /[😀-😄]/u
110+
111+
/* ✗ BAD */
112+
var foo = /[abc]/
113+
var foo = /[a-cd-f]/
114+
var foo = /[😀😁😂😃😄]/u
115+
```
116+
117+
</eslint-code-block>
38118

39119
## :rocket: Version
40120

lib/rules/prefer-range.ts

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,48 @@
11
import type { Expression } from "estree"
22
import type { RegExpVisitor } from "regexpp/visitor"
33
import type { Character, CharacterClassRange } from "regexpp/ast"
4-
import { createRule, defineRegexpVisitor, getRegexpRange } from "../utils"
4+
import {
5+
createRule,
6+
defineRegexpVisitor,
7+
getRegexpRange,
8+
isDigit,
9+
isLetter,
10+
} from "../utils"
11+
12+
const reOptionRange = /^([\ud800-\udbff][\udc00-\udfff]|[^\ud800-\udfff])-([\ud800-\udbff][\udc00-\udfff]|[^\ud800-\udfff])$/
13+
14+
/**
15+
* Parse option
16+
*/
17+
function parseOption(
18+
option:
19+
| undefined
20+
| {
21+
target?: "all" | "alphanumeric" | string[]
22+
},
23+
): (cp: number) => boolean {
24+
const target = option?.target ?? ["alphanumeric"]
25+
if (typeof target === "string") {
26+
return parseOption({ target: [target] })
27+
}
28+
const predicates: ((cp: number) => boolean)[] = []
29+
for (const t of target) {
30+
if (t === "all") {
31+
return () => true
32+
}
33+
if (t === "alphanumeric") {
34+
predicates.push((cp) => isDigit(cp) || isLetter(cp))
35+
}
36+
const res = reOptionRange.exec(t)
37+
if (!res) {
38+
continue
39+
}
40+
const from = res[1].codePointAt(0)!
41+
const to = res[2].codePointAt(0)!
42+
predicates.push((cp) => from <= cp && cp <= to)
43+
}
44+
return (cp) => predicates.some((p) => p(cp))
45+
}
546

647
export default createRule("prefer-range", {
748
meta: {
@@ -12,14 +53,48 @@ export default createRule("prefer-range", {
1253
recommended: false,
1354
},
1455
fixable: "code",
15-
schema: [],
56+
schema: [
57+
{
58+
type: "object",
59+
properties: {
60+
target: {
61+
anyOf: [
62+
{ enum: ["all", "alphanumeric"] },
63+
{
64+
type: "array",
65+
items: [{ enum: ["all", "alphanumeric"] }],
66+
minItems: 1,
67+
additionalItems: false,
68+
},
69+
{
70+
type: "array",
71+
items: {
72+
anyOf: [
73+
{ const: "alphanumeric" },
74+
{
75+
type: "string",
76+
pattern: reOptionRange.source,
77+
},
78+
],
79+
},
80+
uniqueItems: true,
81+
minItems: 1,
82+
additionalItems: false,
83+
},
84+
],
85+
},
86+
},
87+
additionalProperties: false,
88+
},
89+
],
1690
messages: {
1791
unexpected:
1892
'Unexpected multiple adjacent characters. Use "{{range}}" instead.',
1993
},
2094
type: "suggestion", // "problem",
2195
},
2296
create(context) {
97+
const isTarget = parseOption(context.options[0])
2398
const sourceCode = context.getSourceCode()
2499

25100
type CharacterGroup = {
@@ -66,8 +141,17 @@ export default createRule("prefer-range", {
66141
for (const element of ccNode.elements) {
67142
let data: { min: Character; max: Character }
68143
if (element.type === "Character") {
144+
if (!isTarget(element.value)) {
145+
continue
146+
}
69147
data = { min: element, max: element }
70148
} else if (element.type === "CharacterClassRange") {
149+
if (
150+
!isTarget(element.min.value) &&
151+
!isTarget(element.max.value)
152+
) {
153+
continue
154+
}
71155
data = {
72156
min: element.min,
73157
max: element.max,

tests/lib/rules/prefer-range.ts

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

1111
tester.run("prefer-range", rule as any, {
12-
valid: [`/[a]/`, `/[ab]/`, `/[a-c]/`, `/[a-b]/`],
12+
valid: [
13+
`/[a]/`,
14+
`/[ab]/`,
15+
`/[a-c]/`,
16+
`/[a-b]/`,
17+
`/[0-9]/`,
18+
`/[A-Z]/`,
19+
`/[ !"#$]/`,
20+
{
21+
code: `/[ !"#$]/`,
22+
options: [{ target: "alphanumeric" }],
23+
},
24+
{
25+
code: `/[ !"#$]/`,
26+
options: [{ target: ["alphanumeric"] }],
27+
},
28+
{
29+
code: `/[ !"#$]/`,
30+
options: [{ target: ["alphanumeric", "①-⑳"] }],
31+
},
32+
`/[ -$]/`,
33+
{
34+
code: `/[ -$]/`,
35+
options: [{ target: "all" }],
36+
},
37+
{
38+
code: `/[ -$]/`,
39+
options: [{ target: ["all"] }],
40+
},
41+
{
42+
code: `/[0123456789 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ]/`,
43+
options: [{ target: ["😀-😏"] }],
44+
},
45+
],
1346
invalid: [
1447
{
1548
code: `/[abc]/`,
@@ -25,6 +58,14 @@ tester.run("prefer-range", rule as any, {
2558
},
2659
],
2760
},
61+
{
62+
code: `/[ABC abc]/`,
63+
output: `/[A-C a-c]/`,
64+
errors: [
65+
'Unexpected multiple adjacent characters. Use "A-C" instead.',
66+
'Unexpected multiple adjacent characters. Use "a-c" instead.',
67+
],
68+
},
2869
{
2970
code: `/[abc-f]/`,
3071
output: `/[a-f]/`,
@@ -156,5 +197,32 @@ tester.run("prefer-range", rule as any, {
156197
'Unexpected multiple adjacent characters. Use "0-4" instead.',
157198
],
158199
},
200+
{
201+
code: `/[ !"#$]/`,
202+
output: `/[ -$]/`,
203+
options: [{ target: "all" }],
204+
errors: [
205+
'Unexpected multiple adjacent characters. Use " -$" instead.',
206+
],
207+
},
208+
{
209+
code: `/[abcd ①②③④⑤⑥⑦⑧⑨10⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳]/`,
210+
output: `/[a-d ①-⑨10⑪-⑳]/`,
211+
options: [{ target: ["alphanumeric", "①-⑳"] }],
212+
errors: [
213+
'Unexpected multiple adjacent characters. Use "a-d" instead.',
214+
'Unexpected multiple adjacent characters. Use "①-⑨" instead.',
215+
'Unexpected multiple adjacent characters. Use "⑪-⑳" instead.',
216+
],
217+
},
218+
{
219+
code: `/[😀😁😂😃😄 😆😇😈😉😊]/u`,
220+
output: `/[😀-😄 😆-😊]/u`,
221+
options: [{ target: ["alphanumeric", "😀-😏"] }],
222+
errors: [
223+
'Unexpected multiple adjacent characters. Use "😀-😄" instead.',
224+
'Unexpected multiple adjacent characters. Use "😆-😊" instead.',
225+
],
226+
},
159227
],
160228
})

0 commit comments

Comments
 (0)