Skip to content

Commit 9cf4beb

Browse files
authored
Add regexp/prefer-quantifier rule (#24)
* Add `regexp/prefer-quantifier` rule * Add testcases * Add testcases
1 parent 4c12325 commit 9cf4beb

File tree

9 files changed

+437
-0
lines changed

9 files changed

+437
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
9292
| [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: |
9393
| [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |
9494
| [regexp/prefer-plus-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-plus-quantifier.html) | enforce using `+` quantifier | :star::wrench: |
95+
| [regexp/prefer-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-quantifier.html) | enforce using quantifier | :wrench: |
9596
| [regexp/prefer-question-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-question-quantifier.html) | enforce using `?` quantifier | :star::wrench: |
9697
| [regexp/prefer-star-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-star-quantifier.html) | enforce using `*` quantifier | :star::wrench: |
9798
| [regexp/prefer-t](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-t.html) | enforce using `\t` | :star::wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2424
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star: |
2525
| [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
2626
| [regexp/prefer-plus-quantifier](./prefer-plus-quantifier.md) | enforce using `+` quantifier | :star::wrench: |
27+
| [regexp/prefer-quantifier](./prefer-quantifier.md) | enforce using quantifier | :wrench: |
2728
| [regexp/prefer-question-quantifier](./prefer-question-quantifier.md) | enforce using `?` quantifier | :star::wrench: |
2829
| [regexp/prefer-star-quantifier](./prefer-star-quantifier.md) | enforce using `*` quantifier | :star::wrench: |
2930
| [regexp/prefer-t](./prefer-t.md) | enforce using `\t` | :star::wrench: |

docs/rules/prefer-quantifier.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-quantifier"
5+
description: "enforce using quantifier"
6+
---
7+
# regexp/prefer-quantifier
8+
9+
> enforce using quantifier
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 quantifiers instead of consecutive characters in regular expressions.
17+
18+
<eslint-code-block fix>
19+
20+
```js
21+
/* eslint regexp/prefer-quantifier: "error" */
22+
23+
/* ✓ GOOD */
24+
var foo = /\d{4}-\d{2}-\d{2}/;
25+
26+
/* ✗ BAD */
27+
var foo = /\d\d\d\d-\d\d-\d\d/;
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-quantifier.ts)
39+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/prefer-quantifier.ts)

lib/rules/match-any.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export default createRule("match-any", {
6565
minItems: 1,
6666
},
6767
},
68+
additionalProperties: false,
6869
},
6970
],
7071
messages: {

lib/rules/prefer-quantifier.ts

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { Character, CharacterSet, Quantifier } from "regexpp/ast"
4+
import {
5+
createRule,
6+
defineRegexpVisitor,
7+
getRegexpRange,
8+
isDigit,
9+
isLetter,
10+
} from "../utils"
11+
12+
class CharBuffer {
13+
public target: CharacterSet | Character
14+
15+
public min: number
16+
17+
public max: number
18+
19+
public elements: (CharacterSet | Character | Quantifier)[]
20+
21+
public equalChar: (element: CharacterSet | Character) => boolean
22+
23+
public constructor(
24+
element: CharacterSet | Character | Quantifier,
25+
target: CharacterSet | Character,
26+
) {
27+
this.target = target
28+
this.elements = [element]
29+
30+
if (element.type === "Quantifier") {
31+
this.min = element.min
32+
this.max = element.max
33+
} else {
34+
this.min = 1
35+
this.max = 1
36+
}
37+
if (target.type === "CharacterSet") {
38+
if (target.kind === "any") {
39+
this.equalChar = (e) =>
40+
e.type === "CharacterSet" && e.kind === "any"
41+
} else if (target.kind === "property") {
42+
this.equalChar = (e) =>
43+
e.type === "CharacterSet" &&
44+
e.kind === "property" &&
45+
e.key === target.key &&
46+
e.value === target.value &&
47+
e.negate === target.negate
48+
} else {
49+
// Escape
50+
this.equalChar = (e) =>
51+
e.type === "CharacterSet" &&
52+
e.kind === target.kind &&
53+
e.negate === target.negate
54+
}
55+
} else {
56+
this.equalChar = (e) =>
57+
e.type === "Character" && e.value === target.value
58+
}
59+
}
60+
61+
public addElement(element: CharacterSet | Character | Quantifier) {
62+
this.elements.push(element)
63+
if (element.type === "Quantifier") {
64+
this.min += element.min
65+
this.max += element.max
66+
} else {
67+
this.min += 1
68+
this.max += 1
69+
}
70+
}
71+
72+
public isValid(): boolean {
73+
if (this.elements.length < 2) {
74+
return true
75+
}
76+
let charKind: "digit" | "letter" | null = null
77+
for (const element of this.elements) {
78+
if (element.type === "Character") {
79+
if (charKind == null) {
80+
if (isDigit(element.value)) {
81+
charKind = "digit"
82+
} else if (isLetter(element.value)) {
83+
charKind = "letter"
84+
} else {
85+
return false
86+
}
87+
}
88+
} else {
89+
return false
90+
}
91+
}
92+
if (
93+
// It is valid when the same numbers are consecutive.
94+
charKind === "digit" ||
95+
// It is valid when the same letter character continues twice.
96+
(charKind === "letter" && this.elements.length <= 2)
97+
) {
98+
return true
99+
}
100+
return false
101+
}
102+
103+
public getQuantifier(): string {
104+
if (this.min === 0 && this.max === Number.POSITIVE_INFINITY) {
105+
return "*"
106+
} else if (this.min === 1 && this.max === Number.POSITIVE_INFINITY) {
107+
return "+"
108+
} else if (this.min === 0 && this.max === 1) {
109+
return "?"
110+
} else if (this.min === this.max) {
111+
return `{${this.min}}`
112+
} else if (this.max === Number.POSITIVE_INFINITY) {
113+
return `{${this.min},}`
114+
}
115+
return `{${this.min},${this.max}}`
116+
}
117+
}
118+
119+
export default createRule("prefer-quantifier", {
120+
meta: {
121+
docs: {
122+
description: "enforce using quantifier",
123+
// TODO In the major version, it will be changed to "recommended".
124+
recommended: false,
125+
},
126+
fixable: "code",
127+
schema: [],
128+
messages: {
129+
unexpected:
130+
'Unexpected consecutive same {{type}}. Use "{{quantifier}}" instead.',
131+
},
132+
type: "suggestion", // "problem",
133+
},
134+
create(context) {
135+
const sourceCode = context.getSourceCode()
136+
137+
/**
138+
* Create visitor
139+
* @param node
140+
*/
141+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
142+
return {
143+
onAlternativeEnter(aNode) {
144+
let charBuffer: CharBuffer | null = null
145+
for (const element of aNode.elements) {
146+
let target: CharacterSet | Character
147+
if (
148+
element.type === "CharacterSet" ||
149+
element.type === "Character"
150+
) {
151+
target = element
152+
} else if (element.type === "Quantifier") {
153+
if (
154+
element.element.type === "CharacterSet" ||
155+
element.element.type === "Character"
156+
) {
157+
target = element.element
158+
} else {
159+
if (charBuffer) {
160+
validateBuffer(charBuffer)
161+
charBuffer = null
162+
}
163+
continue
164+
}
165+
} else {
166+
if (charBuffer) {
167+
validateBuffer(charBuffer)
168+
charBuffer = null
169+
}
170+
continue
171+
}
172+
if (charBuffer) {
173+
if (charBuffer.equalChar(target)) {
174+
charBuffer.addElement(element)
175+
continue
176+
}
177+
validateBuffer(charBuffer)
178+
}
179+
charBuffer = new CharBuffer(element, target)
180+
}
181+
if (charBuffer) {
182+
validateBuffer(charBuffer)
183+
charBuffer = null
184+
}
185+
186+
/**
187+
* Validate
188+
*/
189+
function validateBuffer(buffer: CharBuffer) {
190+
if (buffer.isValid()) {
191+
return
192+
}
193+
const firstRange = getRegexpRange(
194+
sourceCode,
195+
node,
196+
buffer.elements[0],
197+
)
198+
const lastRange = getRegexpRange(
199+
sourceCode,
200+
node,
201+
buffer.elements[buffer.elements.length - 1],
202+
)
203+
let range: [number, number] | null = null
204+
if (firstRange && lastRange) {
205+
range = [firstRange[0], lastRange[1]]
206+
}
207+
context.report({
208+
node,
209+
loc: range
210+
? {
211+
start: sourceCode.getLocFromIndex(
212+
range[0],
213+
),
214+
end: sourceCode.getLocFromIndex(range[1]),
215+
}
216+
: undefined,
217+
messageId: "unexpected",
218+
data: {
219+
type:
220+
buffer.target.type === "Character"
221+
? "characters"
222+
: "character sets",
223+
quantifier: buffer.getQuantifier(),
224+
},
225+
fix(fixer) {
226+
if (range == null) {
227+
return null
228+
}
229+
return fixer.replaceTextRange(
230+
range,
231+
buffer.target.raw + buffer.getQuantifier(),
232+
)
233+
},
234+
})
235+
}
236+
},
237+
}
238+
}
239+
240+
return defineRegexpVisitor(context, {
241+
createVisitor,
242+
})
243+
},
244+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import noUselessExactlyQuantifier from "../rules/no-useless-exactly-quantifier"
1212
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
1313
import preferD from "../rules/prefer-d"
1414
import preferPlusQuantifier from "../rules/prefer-plus-quantifier"
15+
import preferQuantifier from "../rules/prefer-quantifier"
1516
import preferQuestionQuantifier from "../rules/prefer-question-quantifier"
1617
import preferStarQuantifier from "../rules/prefer-star-quantifier"
1718
import preferT from "../rules/prefer-t"
@@ -31,6 +32,7 @@ export const rules = [
3132
noUselessTwoNumsQuantifier,
3233
preferD,
3334
preferPlusQuantifier,
35+
preferQuantifier,
3436
preferQuestionQuantifier,
3537
preferStarQuantifier,
3638
preferT,

lib/utils/unicode.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,17 @@ function isCodePointInRange(
8080
export function isDigit(codePoint: number): boolean {
8181
return isCodePointInRange(codePoint, CP_RANGE_DIGIT)
8282
}
83+
/**
84+
* Checks if the given code point is letter.
85+
* @param codePoint The code point to check
86+
* @returns {boolean} `true` if the given code point is letter.
87+
*/
88+
export function isLetter(codePoint: number): boolean {
89+
return (
90+
isCodePointInRange(codePoint, CP_RANGE_SMALL_LETTER) ||
91+
isCodePointInRange(codePoint, CP_RANGE_CAPITAL_LETTER)
92+
)
93+
}
8394

8495
/**
8596
* Checks if the given code point is space.

0 commit comments

Comments
 (0)