Skip to content

Commit 98adf16

Browse files
authored
Add regexp/prefer-character-class rule (#40)
1 parent f9b8f5c commit 98adf16

File tree

6 files changed

+290
-0
lines changed

6 files changed

+290
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
9595
| [regexp/no-useless-non-greedy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-greedy.html) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |
9696
| [regexp/no-useless-range](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-range.html) | disallow unnecessary range of characters by using a hyphen | :wrench: |
9797
| [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: |
98+
| [regexp/prefer-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-character-class.html) | enforce using character class | :wrench: |
9899
| [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |
99100
| [regexp/prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | :star::wrench: |
100101
| [regexp/prefer-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-quantifier.html) | enforce using quantifier | :wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2727
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |
2828
| [regexp/no-useless-range](./no-useless-range.md) | disallow unnecessary range of characters by using a hyphen | :wrench: |
2929
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star: |
30+
| [regexp/prefer-character-class](./prefer-character-class.md) | enforce using character class | :wrench: |
3031
| [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
3132
| [regexp/prefer-plus-quantifier](./prefer-plus-quantifier.md) | enforce using `+` quantifier | :star::wrench: |
3233
| [regexp/prefer-quantifier](./prefer-quantifier.md) | enforce using quantifier | :wrench: |

docs/rules/prefer-character-class.md

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

lib/rules/prefer-character-class.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type {
4+
CapturingGroup,
5+
Character,
6+
CharacterClass,
7+
CharacterSet,
8+
Group,
9+
LookaroundAssertion,
10+
Pattern,
11+
} from "regexpp/ast"
12+
import {
13+
createRule,
14+
defineRegexpVisitor,
15+
fixerApplyEscape,
16+
getRegexpLocation,
17+
getRegexpRange,
18+
} from "../utils"
19+
20+
export default createRule("prefer-character-class", {
21+
meta: {
22+
docs: {
23+
description: "enforce using character class",
24+
// TODO In the major version
25+
// recommended: true,
26+
recommended: false,
27+
},
28+
fixable: "code",
29+
schema: [],
30+
messages: {
31+
unexpected:
32+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
33+
},
34+
type: "suggestion", // "problem",
35+
},
36+
create(context) {
37+
const sourceCode = context.getSourceCode()
38+
39+
/**
40+
* Create visitor
41+
* @param node
42+
*/
43+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
44+
/** Verify alternatives */
45+
function verify(
46+
regexpNode:
47+
| Group
48+
| CapturingGroup
49+
| Pattern
50+
| LookaroundAssertion,
51+
) {
52+
if (regexpNode.alternatives.length <= 1) {
53+
return
54+
}
55+
const elements: (
56+
| Character
57+
| CharacterSet
58+
| CharacterClass
59+
)[] = []
60+
for (const alt of regexpNode.alternatives) {
61+
if (alt.elements.length !== 1) {
62+
return
63+
}
64+
const element = alt.elements[0]
65+
if (element.type === "CharacterSet") {
66+
if (element.kind === "any") {
67+
return
68+
}
69+
} else if (
70+
element.type !== "Character" &&
71+
element.type !== "CharacterClass"
72+
) {
73+
return
74+
}
75+
elements.push(element)
76+
}
77+
78+
context.report({
79+
node,
80+
loc: getRegexpLocation(sourceCode, node, regexpNode),
81+
messageId: "unexpected",
82+
fix(fixer) {
83+
let replaceRange: [number, number] | null = null
84+
let newText = ""
85+
for (const element of elements) {
86+
const range = getRegexpRange(
87+
sourceCode,
88+
node,
89+
element,
90+
)
91+
if (!range) {
92+
return null
93+
}
94+
if (!replaceRange) {
95+
replaceRange = [...range]
96+
} else {
97+
replaceRange[1] = range[1]
98+
}
99+
const text =
100+
element.type === "CharacterClass"
101+
? element.raw.slice(1, -1)
102+
: element.raw
103+
if (text.startsWith("-")) {
104+
newText += fixerApplyEscape("\\", node)
105+
}
106+
newText += text
107+
}
108+
return fixer.replaceTextRange(
109+
replaceRange!,
110+
`[${newText}]`,
111+
)
112+
},
113+
})
114+
}
115+
116+
return {
117+
onPatternEnter: verify,
118+
onGroupEnter: verify,
119+
onCapturingGroupEnter: verify,
120+
onAssertionEnter(aNode) {
121+
if (
122+
aNode.kind === "lookahead" ||
123+
aNode.kind === "lookbehind"
124+
) {
125+
verify(aNode)
126+
}
127+
},
128+
}
129+
}
130+
131+
return defineRegexpVisitor(context, {
132+
createVisitor,
133+
})
134+
},
135+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import noUselessExactlyQuantifier from "../rules/no-useless-exactly-quantifier"
1515
import noUselessNonGreedy from "../rules/no-useless-non-greedy"
1616
import noUselessRange from "../rules/no-useless-range"
1717
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
18+
import preferCharacterClass from "../rules/prefer-character-class"
1819
import preferD from "../rules/prefer-d"
1920
import preferPlusQuantifier from "../rules/prefer-plus-quantifier"
2021
import preferQuantifier from "../rules/prefer-quantifier"
@@ -44,6 +45,7 @@ export const rules = [
4445
noUselessNonGreedy,
4546
noUselessRange,
4647
noUselessTwoNumsQuantifier,
48+
preferCharacterClass,
4749
preferD,
4850
preferPlusQuantifier,
4951
preferQuantifier,
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/prefer-character-class"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("prefer-character-class", rule as any, {
12+
valid: [
13+
`/regexp/`,
14+
`/[regexp]/`,
15+
`/reg|exp/`,
16+
String.raw`/a|b|c|\d|(d)/`,
17+
String.raw`/a|.|c|\d|c|[-d-f]/`,
18+
],
19+
invalid: [
20+
{
21+
code: String.raw`/a|b|c|\d/`,
22+
output: String.raw`/[abc\d]/`,
23+
errors: [
24+
{
25+
message:
26+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
27+
line: 1,
28+
column: 2,
29+
endLine: 1,
30+
endColumn: 10,
31+
},
32+
],
33+
},
34+
{
35+
code: String.raw`/(a|b|c|\d)/`,
36+
output: String.raw`/([abc\d])/`,
37+
errors: [
38+
{
39+
message:
40+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
41+
line: 1,
42+
column: 2,
43+
endLine: 1,
44+
endColumn: 12,
45+
},
46+
],
47+
},
48+
{
49+
code: String.raw`/(?:a|b|c|\d)/`,
50+
output: String.raw`/(?:[abc\d])/`,
51+
errors: [
52+
{
53+
message:
54+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
55+
line: 1,
56+
column: 2,
57+
endLine: 1,
58+
endColumn: 14,
59+
},
60+
],
61+
},
62+
{
63+
code: String.raw`/(?=a|b|c|\d)/`,
64+
output: String.raw`/(?=[abc\d])/`,
65+
errors: [
66+
{
67+
message:
68+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
69+
line: 1,
70+
column: 2,
71+
endLine: 1,
72+
endColumn: 14,
73+
},
74+
],
75+
},
76+
{
77+
code: String.raw`/(?<=a|b|c|\d)/`,
78+
output: String.raw`/(?<=[abc\d])/`,
79+
errors: [
80+
{
81+
message:
82+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
83+
line: 1,
84+
column: 2,
85+
endLine: 1,
86+
endColumn: 15,
87+
},
88+
],
89+
},
90+
{
91+
code: String.raw`/a|b|c|\d|[d-f]/`,
92+
output: String.raw`/[abc\dd-f]/`,
93+
errors: [
94+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
95+
],
96+
},
97+
{
98+
code: String.raw`/a|-|c|\d|c|[-d-f]/`,
99+
output: String.raw`/[a\-c\dc\-d-f]/`,
100+
errors: [
101+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
102+
],
103+
},
104+
{
105+
code: String.raw`/a|[.]|c|\d|c|[-d-f]/`,
106+
output: String.raw`/[a.c\dc\-d-f]/`,
107+
errors: [
108+
'Unexpected the disjunction of single element alternatives. Use character class "[...]" instead.',
109+
],
110+
},
111+
],
112+
})

0 commit comments

Comments
 (0)