Skip to content

Commit dbe5556

Browse files
Add no-useless-quantifier rule (#197)
1 parent 920678e commit dbe5556

File tree

6 files changed

+362
-0
lines changed

6 files changed

+362
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
132132
| [regexp/no-useless-lazy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-lazy.html) | disallow unnecessarily non-greedy quantifiers | :wrench: |
133133
| [regexp/no-useless-non-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-capturing-group.html) | disallow unnecessary Non-capturing group | :wrench: |
134134
| [regexp/no-useless-non-greedy](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-non-greedy.html) | disallow unnecessarily non-greedy quantifiers | :wrench: |
135+
| [regexp/no-useless-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-quantifier.html) | disallow quantifiers that can be removed | :wrench: |
135136
| [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: |
136137
| [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::wrench: |
137138
| [regexp/optimal-lookaround-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/optimal-lookaround-quantifier.html) | disallow the alternatives of lookarounds that end with a non-constant quantifier | |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
4646
| [regexp/no-useless-lazy](./no-useless-lazy.md) | disallow unnecessarily non-greedy quantifiers | :wrench: |
4747
| [regexp/no-useless-non-capturing-group](./no-useless-non-capturing-group.md) | disallow unnecessary Non-capturing group | :wrench: |
4848
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessarily non-greedy quantifiers | :wrench: |
49+
| [regexp/no-useless-quantifier](./no-useless-quantifier.md) | disallow quantifiers that can be removed | :wrench: |
4950
| [regexp/no-useless-range](./no-useless-range.md) | disallow unnecessary range of characters by using a hyphen | :wrench: |
5051
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star::wrench: |
5152
| [regexp/optimal-lookaround-quantifier](./optimal-lookaround-quantifier.md) | disallow the alternatives of lookarounds that end with a non-constant quantifier | |

docs/rules/no-useless-quantifier.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-useless-quantifier"
5+
description: "disallow quantifiers that can be removed"
6+
---
7+
# regexp/no-useless-quantifier
8+
9+
> disallow quantifiers that can be removed
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 reports quantifiers that can trivially be removed without affecting the pattern.
17+
18+
This rule only fixes constant one quantifiers (e.g. `a{1}`). All other reported useless quantifiers hint at programmer oversight or fundamental problems with the pattern.
19+
20+
Examples:
21+
22+
- `a{1}`
23+
24+
It's clear that the `{1}` quantifier can be removed.
25+
26+
- `(?:a+b*|c*)?`
27+
28+
It might not very obvious that the `?` quantifier can be removed. Without this quantifier, that pattern can still match the empty string by choosing 0 many `c`s in the `c*` alternative.
29+
30+
- `(?:\b)+`
31+
32+
The `+` quantifier can be removed because its quantified element doesn't consume characters.
33+
34+
<eslint-code-block fix>
35+
36+
```js
37+
/* eslint regexp/no-useless-quantifier: "error" */
38+
39+
/* ✓ GOOD */
40+
var foo = /a*/;
41+
var foo = /(?:a|b?)??/;
42+
var foo = /(?:\b|(?!a))*/;
43+
44+
/* ✗ BAD */
45+
var foo = /a{1}/;
46+
var foo = /(?:\b)+/;
47+
var foo = /(?:a+b*|c*)?/;
48+
```
49+
50+
</eslint-code-block>
51+
52+
## :wrench: Options
53+
54+
Nothing.
55+
56+
## :mag: Implementation
57+
58+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-useless-quantifier.ts)
59+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-useless-quantifier.ts)

lib/rules/no-useless-quantifier.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import type { Rule } from "eslint"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { Quantifier } from "regexpp/ast"
4+
import type { RegExpContext } from "../utils"
5+
import { canUnwrapped, createRule, defineRegexpVisitor } from "../utils"
6+
import { isEmpty, isPotentiallyEmpty, isZeroLength } from "regexp-ast-analysis"
7+
8+
export default createRule("no-useless-quantifier", {
9+
meta: {
10+
docs: {
11+
description: "disallow quantifiers that can be removed",
12+
// TODO Switch to recommended in the major version.
13+
// recommended: true,
14+
recommended: false,
15+
},
16+
fixable: "code",
17+
schema: [],
18+
messages: {
19+
constOne: "Unexpected useless quantifier.",
20+
empty:
21+
"Unexpected useless quantifier. The quantified element doesn't consume or assert characters.",
22+
emptyQuestionMark:
23+
"Unexpected useless quantifier. The quantified element can already accept the empty string, so this quantifier is redundant.",
24+
zeroLength:
25+
"Unexpected useless quantifier. The quantified element doesn't consume characters.",
26+
27+
// suggestions
28+
remove: "Remove the '{{quant}}' quantifier.",
29+
},
30+
type: "suggestion", // "problem",
31+
},
32+
create(context) {
33+
/**
34+
* Create visitor
35+
*/
36+
function createVisitor(
37+
regexpContext: RegExpContext,
38+
): RegExpVisitor.Handlers {
39+
const { node, getRegexpLocation, fixReplaceNode } = regexpContext
40+
41+
/**
42+
* Returns a fix that replaces the given quantifier with its
43+
* quantified element
44+
*/
45+
function fixRemoveQuant(qNode: Quantifier) {
46+
return fixReplaceNode(qNode, () => {
47+
const text = qNode.element.raw
48+
return canUnwrapped(qNode, text) ? text : null
49+
})
50+
}
51+
52+
/**
53+
* Returns a suggestion that replaces the given quantifier with its
54+
* quantified element
55+
*/
56+
function suggestRemoveQuant(
57+
qNode: Quantifier,
58+
): Rule.SuggestionReportDescriptor {
59+
const quant = qNode.raw.slice(qNode.element.end - qNode.start)
60+
61+
return {
62+
messageId: "remove",
63+
data: { quant },
64+
fix: fixReplaceNode(qNode, () => {
65+
const text = qNode.element.raw
66+
return canUnwrapped(qNode, text) ? text : null
67+
}),
68+
}
69+
}
70+
71+
return {
72+
onQuantifierEnter(qNode) {
73+
// trivial case
74+
// e.g. a{1}
75+
if (qNode.min === 1 && qNode.max === 1) {
76+
context.report({
77+
node,
78+
loc: getRegexpLocation(qNode),
79+
messageId: "constOne",
80+
fix: fixRemoveQuant(qNode),
81+
})
82+
return
83+
}
84+
85+
// the quantified element already accepts the empty string
86+
// e.g. (||)*
87+
if (isEmpty(qNode.element)) {
88+
context.report({
89+
node,
90+
loc: getRegexpLocation(qNode),
91+
messageId: "empty",
92+
suggest: [suggestRemoveQuant(qNode)],
93+
})
94+
return
95+
}
96+
97+
// the quantified element already accepts the empty string
98+
// e.g. (a?)?
99+
if (
100+
qNode.min === 0 &&
101+
qNode.max === 1 &&
102+
qNode.greedy &&
103+
isPotentiallyEmpty(qNode.element)
104+
) {
105+
context.report({
106+
node,
107+
loc: getRegexpLocation(qNode),
108+
messageId: "emptyQuestionMark",
109+
suggest: [suggestRemoveQuant(qNode)],
110+
})
111+
return
112+
}
113+
114+
// the quantified is zero length
115+
// e.g. (\b){5}
116+
if (qNode.min >= 1 && isZeroLength(qNode.element)) {
117+
context.report({
118+
node,
119+
loc: getRegexpLocation(qNode),
120+
messageId: "zeroLength",
121+
suggest: [suggestRemoveQuant(qNode)],
122+
})
123+
}
124+
},
125+
}
126+
}
127+
128+
return defineRegexpVisitor(context, {
129+
createVisitor,
130+
})
131+
},
132+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import noUselessFlag from "../rules/no-useless-flag"
3434
import noUselessLazy from "../rules/no-useless-lazy"
3535
import noUselessNonCapturingGroup from "../rules/no-useless-non-capturing-group"
3636
import noUselessNonGreedy from "../rules/no-useless-non-greedy"
37+
import noUselessQuantifier from "../rules/no-useless-quantifier"
3738
import noUselessRange from "../rules/no-useless-range"
3839
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
3940
import optimalLookaroundQuantifier from "../rules/optimal-lookaround-quantifier"
@@ -92,6 +93,7 @@ export const rules = [
9293
noUselessLazy,
9394
noUselessNonCapturingGroup,
9495
noUselessNonGreedy,
96+
noUselessQuantifier,
9597
noUselessRange,
9698
noUselessTwoNumsQuantifier,
9799
optimalLookaroundQuantifier,
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/no-useless-quantifier"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("no-useless-quantifier", rule as any, {
12+
valid: [
13+
String.raw`/a*/`,
14+
String.raw`/(?:a)?/`,
15+
String.raw`/(?:a|b?)??/`,
16+
String.raw`/(?:\b|a)?/`,
17+
String.raw`/(?:\b)*/`,
18+
String.raw`/(?:\b|(?!a))*/`,
19+
String.raw`/(?:\b|(?!))*/`,
20+
String.raw`/#[\da-z]+|#(?:-|([+/\\*~<>=@%|&?!])\1?)|#(?=\()/`,
21+
],
22+
invalid: [
23+
// trivial
24+
{
25+
code: String.raw`/a{1}/`,
26+
output: String.raw`/a/`,
27+
errors: ["Unexpected useless quantifier."],
28+
},
29+
{
30+
code: String.raw`/a{1,1}?/`,
31+
output: String.raw`/a/`,
32+
errors: ["Unexpected useless quantifier."],
33+
},
34+
35+
// empty quantified element
36+
{
37+
code: String.raw`/(?:)+/`,
38+
output: null,
39+
errors: [
40+
{
41+
messageId: "empty",
42+
suggestions: [
43+
{ messageId: "remove", output: String.raw`/(?:)/` },
44+
],
45+
},
46+
],
47+
},
48+
{
49+
code: String.raw`/(?:|(?:)){5,9}/`,
50+
output: null,
51+
errors: [
52+
{
53+
messageId: "empty",
54+
suggestions: [
55+
{
56+
messageId: "remove",
57+
output: String.raw`/(?:|(?:))/`,
58+
},
59+
],
60+
},
61+
],
62+
},
63+
{
64+
code: String.raw`/(?:|()()())*/`,
65+
output: null,
66+
errors: [
67+
{
68+
messageId: "empty",
69+
suggestions: [
70+
{
71+
messageId: "remove",
72+
output: String.raw`/(?:|()()())/`,
73+
},
74+
],
75+
},
76+
],
77+
},
78+
79+
// unnecessary optional quantifier (?) because the quantified element is potentially empty
80+
{
81+
code: String.raw`/(?:a+b*|c*)?/`,
82+
output: null,
83+
errors: [
84+
{
85+
messageId: "emptyQuestionMark",
86+
suggestions: [
87+
{
88+
messageId: "remove",
89+
output: String.raw`/(?:a+b*|c*)/`,
90+
},
91+
],
92+
},
93+
],
94+
},
95+
{
96+
code: String.raw`/(?:a|b?c?d?e?f?)?/`,
97+
output: null,
98+
errors: [
99+
{
100+
messageId: "emptyQuestionMark",
101+
suggestions: [
102+
{
103+
messageId: "remove",
104+
output: String.raw`/(?:a|b?c?d?e?f?)/`,
105+
},
106+
],
107+
},
108+
],
109+
},
110+
111+
// quantified elements which do not consume characters
112+
{
113+
code: String.raw`/(?:\b)+/`,
114+
output: null,
115+
errors: [
116+
{
117+
messageId: "zeroLength",
118+
suggestions: [
119+
{ messageId: "remove", output: String.raw`/(?:\b)/` },
120+
],
121+
},
122+
],
123+
},
124+
{
125+
code: String.raw`/(?:\b){5,100}/`,
126+
output: null,
127+
errors: [
128+
{
129+
messageId: "zeroLength",
130+
suggestions: [
131+
{ messageId: "remove", output: String.raw`/(?:\b)/` },
132+
],
133+
},
134+
],
135+
},
136+
{
137+
code: String.raw`/(?:\b|(?!a))+/`,
138+
output: null,
139+
errors: [
140+
{
141+
messageId: "zeroLength",
142+
suggestions: [
143+
{
144+
messageId: "remove",
145+
output: String.raw`/(?:\b|(?!a))/`,
146+
},
147+
],
148+
},
149+
],
150+
},
151+
{
152+
code: String.raw`/(?:\b|(?!)){6}/`,
153+
output: null,
154+
errors: [
155+
{
156+
messageId: "zeroLength",
157+
suggestions: [
158+
{
159+
messageId: "remove",
160+
output: String.raw`/(?:\b|(?!))/`,
161+
},
162+
],
163+
},
164+
],
165+
},
166+
],
167+
})

0 commit comments

Comments
 (0)