Skip to content

Commit ac56358

Browse files
authored
Add regexp/prefer-range rule (#36)
1 parent 84242f0 commit ac56358

File tree

6 files changed

+352
-0
lines changed

6 files changed

+352
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
9898
| [regexp/prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | :star::wrench: |
9999
| [regexp/prefer-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-quantifier.html) | enforce using quantifier | :wrench: |
100100
| [regexp/prefer-question-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html) | enforce using `?` quantifier | :star::wrench: |
101+
| [regexp/prefer-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-range.html) | enforce using character class range | :wrench: |
101102
| [regexp/prefer-regexp-exec](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-exec.html) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | |
102103
| [regexp/prefer-regexp-test](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-regexp-test.html) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | :wrench: |
103104
| [regexp/prefer-star-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html) | enforce using `*` quantifier | :star::wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
3030
| [regexp/prefer-plus-quantifier](./prefer-plus-quantifier.md) | enforce using `+` quantifier | :star::wrench: |
3131
| [regexp/prefer-quantifier](./prefer-quantifier.md) | enforce using quantifier | :wrench: |
3232
| [regexp/prefer-question-quantifier](./prefer-question-quantifier.md) | enforce using `?` quantifier | :star::wrench: |
33+
| [regexp/prefer-range](./prefer-range.md) | enforce using character class range | :wrench: |
3334
| [regexp/prefer-regexp-exec](./prefer-regexp-exec.md) | enforce that `RegExp#exec` is used instead of `String#match` if no global flag is provided | |
3435
| [regexp/prefer-regexp-test](./prefer-regexp-test.md) | enforce that `RegExp#test` is used instead of `String#match` and `RegExp#exec` | :wrench: |
3536
| [regexp/prefer-star-quantifier](./prefer-star-quantifier.md) | enforce using `*` quantifier | :star::wrench: |

docs/rules/prefer-range.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/prefer-range"
5+
description: "enforce using character class range"
6+
---
7+
# regexp/prefer-range
8+
9+
> enforce using character class range
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 is aimed to use ranges instead of multiple adjacent characters in character class.
17+
18+
<eslint-code-block fix>
19+
20+
```js
21+
/* eslint regexp/prefer-range: "error" */
22+
23+
/* ✓ GOOD */
24+
var foo = /[a-c]/
25+
var foo = /[a-f]/
26+
27+
/* ✗ BAD */
28+
var foo = /[abc]/
29+
var foo = /[a-cd-f]/
30+
31+
```
32+
33+
</eslint-code-block>
34+
35+
## :wrench: Options
36+
37+
Nothing.
38+
39+
## :mag: Implementation
40+
41+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/prefer-range.ts)
42+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-range.ts)

lib/rules/prefer-range.ts

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { Character, CharacterClassRange } from "regexpp/ast"
4+
import { createRule, defineRegexpVisitor, getRegexpRange } from "../utils"
5+
6+
export default createRule("prefer-range", {
7+
meta: {
8+
docs: {
9+
description: "enforce using character class range",
10+
// TODO In the major version
11+
// recommended: true,
12+
recommended: false,
13+
},
14+
fixable: "code",
15+
schema: [],
16+
messages: {
17+
unexpected:
18+
'Unexpected multiple adjacent characters. Use "{{range}}" instead.',
19+
},
20+
type: "suggestion", // "problem",
21+
},
22+
create(context) {
23+
const sourceCode = context.getSourceCode()
24+
25+
type CharacterGroup = {
26+
min: Character
27+
max: Character
28+
nodes: (Character | CharacterClassRange)[]
29+
}
30+
31+
/**
32+
* Create visitor
33+
* @param node
34+
*/
35+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
36+
/** Get report location ranges */
37+
function getReportRanges(
38+
nodes: (Character | CharacterClassRange)[],
39+
): [number, number][] | null {
40+
const ranges: [number, number][] = []
41+
for (const reportNode of nodes) {
42+
const reportRange = getRegexpRange(
43+
sourceCode,
44+
node,
45+
reportNode,
46+
)
47+
if (!reportRange) {
48+
return null
49+
}
50+
const range = ranges.find(
51+
(r) => r[0] <= reportRange[1] && reportRange[0] <= r[1],
52+
)
53+
if (range) {
54+
range[0] = Math.min(range[0], reportRange[0])
55+
range[1] = Math.max(range[1], reportRange[1])
56+
} else {
57+
ranges.push([...reportRange])
58+
}
59+
}
60+
return ranges
61+
}
62+
63+
return {
64+
onCharacterClassEnter(ccNode) {
65+
const groups: CharacterGroup[] = []
66+
for (const element of ccNode.elements) {
67+
let data: { min: Character; max: Character }
68+
if (element.type === "Character") {
69+
data = { min: element, max: element }
70+
} else if (element.type === "CharacterClassRange") {
71+
data = {
72+
min: element.min,
73+
max: element.max,
74+
}
75+
} else {
76+
continue
77+
}
78+
const group = groups.find(
79+
(gp) =>
80+
gp.min.value - 1 <= data.max.value &&
81+
data.min.value <= gp.max.value + 1,
82+
)
83+
if (group) {
84+
if (data.min.value < group.min.value) {
85+
group.min = data.min
86+
}
87+
if (group.max.value < data.max.value) {
88+
group.max = data.max
89+
}
90+
group.nodes.push(element)
91+
} else {
92+
groups.push({
93+
...data,
94+
nodes: [element],
95+
})
96+
}
97+
}
98+
99+
for (const group of groups) {
100+
if (
101+
group.max.value - group.min.value > 1 &&
102+
group.nodes.length > 1
103+
) {
104+
const ranges = getReportRanges(group.nodes)
105+
const newText = `${group.min.raw}-${group.max.raw}`
106+
for (const range of ranges || [node.range!]) {
107+
context.report({
108+
node,
109+
loc: {
110+
start: sourceCode.getLocFromIndex(
111+
range[0],
112+
),
113+
end: sourceCode.getLocFromIndex(
114+
range[1],
115+
),
116+
},
117+
messageId: "unexpected",
118+
data: {
119+
range: newText,
120+
},
121+
fix: ranges
122+
? (fixer) => {
123+
return ranges.map((r, index) => {
124+
if (index === 0) {
125+
return fixer.replaceTextRange(
126+
r,
127+
newText,
128+
)
129+
}
130+
return fixer.removeRange(r)
131+
})
132+
}
133+
: undefined,
134+
})
135+
}
136+
}
137+
}
138+
},
139+
}
140+
}
141+
142+
return defineRegexpVisitor(context, {
143+
createVisitor,
144+
})
145+
},
146+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import preferD from "../rules/prefer-d"
1818
import preferPlusQuantifier from "../rules/prefer-plus-quantifier"
1919
import preferQuantifier from "../rules/prefer-quantifier"
2020
import preferQuestionQuantifier from "../rules/prefer-question-quantifier"
21+
import preferRange from "../rules/prefer-range"
2122
import preferRegexpExec from "../rules/prefer-regexp-exec"
2223
import preferRegexpTest from "../rules/prefer-regexp-test"
2324
import preferStarQuantifier from "../rules/prefer-star-quantifier"
@@ -45,6 +46,7 @@ export const rules = [
4546
preferPlusQuantifier,
4647
preferQuantifier,
4748
preferQuestionQuantifier,
49+
preferRange,
4850
preferRegexpExec,
4951
preferRegexpTest,
5052
preferStarQuantifier,

tests/lib/rules/prefer-range.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/prefer-range"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("prefer-range", rule as any, {
12+
valid: [`/[a]/`, `/[ab]/`, `/[a-c]/`, `/[a-b]/`],
13+
invalid: [
14+
{
15+
code: `/[abc]/`,
16+
output: `/[a-c]/`,
17+
errors: [
18+
{
19+
message:
20+
'Unexpected multiple adjacent characters. Use "a-c" instead.',
21+
line: 1,
22+
column: 3,
23+
endLine: 1,
24+
endColumn: 6,
25+
},
26+
],
27+
},
28+
{
29+
code: `/[abc-f]/`,
30+
output: `/[a-f]/`,
31+
errors: [
32+
{
33+
message:
34+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
35+
line: 1,
36+
column: 3,
37+
endLine: 1,
38+
endColumn: 8,
39+
},
40+
],
41+
},
42+
{
43+
code: `/[a-cd-f]/`,
44+
output: `/[a-f]/`,
45+
errors: [
46+
{
47+
message:
48+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
49+
line: 1,
50+
column: 3,
51+
endLine: 1,
52+
endColumn: 9,
53+
},
54+
],
55+
},
56+
{
57+
code: `/[abc_d-f]/`,
58+
output: `/[a-f_]/`,
59+
errors: [
60+
{
61+
message:
62+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
63+
line: 1,
64+
column: 3,
65+
endColumn: 6,
66+
},
67+
{
68+
message:
69+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
70+
line: 1,
71+
column: 7,
72+
endColumn: 10,
73+
},
74+
],
75+
},
76+
{
77+
code: `/[abc_d-f_h-j_k-m]/`,
78+
output: `/[a-f__h-m_]/`,
79+
errors: [
80+
{
81+
message:
82+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
83+
line: 1,
84+
column: 3,
85+
},
86+
{
87+
message:
88+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
89+
line: 1,
90+
column: 7,
91+
},
92+
{
93+
message:
94+
'Unexpected multiple adjacent characters. Use "h-m" instead.',
95+
line: 1,
96+
column: 11,
97+
},
98+
{
99+
message:
100+
'Unexpected multiple adjacent characters. Use "h-m" instead.',
101+
line: 1,
102+
column: 15,
103+
},
104+
],
105+
},
106+
{
107+
code: `/[a-d_d-f_h-k_j-m]/`,
108+
output: `/[a-f__h-m_]/`,
109+
errors: [
110+
{
111+
message:
112+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
113+
line: 1,
114+
column: 3,
115+
},
116+
{
117+
message:
118+
'Unexpected multiple adjacent characters. Use "a-f" instead.',
119+
line: 1,
120+
column: 7,
121+
},
122+
{
123+
message:
124+
'Unexpected multiple adjacent characters. Use "h-m" instead.',
125+
line: 1,
126+
column: 11,
127+
},
128+
{
129+
message:
130+
'Unexpected multiple adjacent characters. Use "h-m" instead.',
131+
line: 1,
132+
column: 15,
133+
},
134+
],
135+
},
136+
{
137+
code: String.raw`/[0-2\d3-4]/`,
138+
output: String.raw`/[0-4\d]/`,
139+
errors: [
140+
'Unexpected multiple adjacent characters. Use "0-4" instead.',
141+
'Unexpected multiple adjacent characters. Use "0-4" instead.',
142+
],
143+
},
144+
{
145+
code: `/[3-4560-2]/`,
146+
output: `/[0-6]/`,
147+
errors: [
148+
'Unexpected multiple adjacent characters. Use "0-6" instead.',
149+
],
150+
},
151+
{
152+
code: String.raw`const s = "[0-23-4\\d]"
153+
new RegExp(s)`,
154+
output: null,
155+
errors: [
156+
'Unexpected multiple adjacent characters. Use "0-4" instead.',
157+
],
158+
},
159+
],
160+
})

0 commit comments

Comments
 (0)