Skip to content

Commit 30edbf5

Browse files
authored
Add regexp/no-lazy-ends rule (#96)
* Add `regexp/no-lazy-ends` rule * update
1 parent 6ca58c6 commit 30edbf5

File tree

6 files changed

+276
-0
lines changed

6 files changed

+276
-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-empty-lookarounds-assertion](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-empty-lookarounds-assertion.html) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
9696
| [regexp/no-escape-backspace](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-escape-backspace.html) | disallow escape backspace (`[\b]`) | :star: |
9797
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
98+
| [regexp/no-lazy-ends](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-lazy-ends.html) | disallow lazy quantifiers at the end of an expression | |
9899
| [regexp/no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow legacy RegExp features | |
99100
| [regexp/no-octal](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-octal.html) | disallow octal escape sequence | :star: |
100101
| [regexp/no-unused-capturing-group](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-unused-capturing-group.html) | disallow unused capturing group | |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
2323
| [regexp/no-empty-lookarounds-assertion](./no-empty-lookarounds-assertion.md) | disallow empty lookahead assertion or empty lookbehind assertion | :star: |
2424
| [regexp/no-escape-backspace](./no-escape-backspace.md) | disallow escape backspace (`[\b]`) | :star: |
2525
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
26+
| [regexp/no-lazy-ends](./no-lazy-ends.md) | disallow lazy quantifiers at the end of an expression | |
2627
| [regexp/no-legacy-features](./no-legacy-features.md) | disallow legacy RegExp features | |
2728
| [regexp/no-octal](./no-octal.md) | disallow octal escape sequence | :star: |
2829
| [regexp/no-unused-capturing-group](./no-unused-capturing-group.md) | disallow unused capturing group | |

docs/rules/no-lazy-ends.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-lazy-ends"
5+
description: "disallow lazy quantifiers at the end of an expression"
6+
---
7+
# regexp/no-lazy-ends
8+
9+
> disallow lazy quantifiers at the end of an expression
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+
13+
## :book: Rule Details
14+
15+
If a lazily quantified element is the last element matched by an expression
16+
(e.g. the `a{2,3}?` in `b+a{2,3}?`), we know that the lazy quantifier will
17+
always only match the element the minimum number of times. The maximum is
18+
completely ignored because the expression can accept after the minimum was
19+
reached.
20+
21+
If the minimum of the lazy quantifier is 0, we can even remove the quantifier
22+
and the quantified element without changing the meaning of the pattern. E.g.
23+
`a+b*?` and `a+` behave the same.
24+
25+
If the minimum is 1, we can remove the quantifier. E.g. `a+b+?` and `a+b` behave
26+
the same.
27+
28+
If the minimum is greater than 1, we can replace the quantifier with a constant,
29+
greedy quantifier. E.g. `a+b{2,4}?` and `a+b{2}` behave the same.
30+
31+
<eslint-code-block>
32+
33+
```js
34+
/* eslint regexp/no-lazy-ends: "error" */
35+
36+
/* ✓ GOOD */
37+
var foo = /a+?b*/
38+
var foo = /a??(?:ba+?|c)*/
39+
var foo = /ba*?$/
40+
41+
/* ✗ BAD */
42+
var foo = /a??/
43+
var foo = /a+b+?/
44+
var foo = /a(?:c|ab+?)?/
45+
```
46+
47+
</eslint-code-block>
48+
49+
## :wrench: Options
50+
51+
Nothing.
52+
53+
## :heart: Compatibility
54+
55+
This rule was taken from [eslint-plugin-clean-regex].
56+
This rule is compatible with [clean-regex/no-lazy-ends] rule.
57+
58+
[eslint-plugin-clean-regex]: https://github.com/RunDevelopment/eslint-plugin-clean-regex
59+
[clean-regex/no-lazy-ends]: https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/docs/rules/no-lazy-ends.md
60+
61+
## :mag: Implementation
62+
63+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-lazy-ends.ts)
64+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-lazy-ends.ts)

lib/rules/no-lazy-ends.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { Alternative, Quantifier } from "regexpp/ast"
4+
import { createRule, defineRegexpVisitor, getRegexpLocation } from "../utils"
5+
6+
/**
7+
* Extract lazy end quantifiers
8+
*/
9+
function* extractLazyEndQuantifiers(
10+
alternatives: Alternative[],
11+
): IterableIterator<Quantifier> {
12+
for (const { elements } of alternatives) {
13+
if (elements.length > 0) {
14+
const last = elements[elements.length - 1]
15+
switch (last.type) {
16+
case "Quantifier":
17+
if (!last.greedy && last.min !== last.max) {
18+
yield last
19+
} else if (last.max === 1) {
20+
const element = last.element
21+
if (
22+
element.type === "Group" ||
23+
element.type === "CapturingGroup"
24+
) {
25+
yield* extractLazyEndQuantifiers(
26+
element.alternatives,
27+
)
28+
}
29+
}
30+
break
31+
32+
case "CapturingGroup":
33+
case "Group":
34+
yield* extractLazyEndQuantifiers(last.alternatives)
35+
break
36+
37+
default:
38+
break
39+
}
40+
}
41+
}
42+
}
43+
44+
export default createRule("no-lazy-ends", {
45+
meta: {
46+
docs: {
47+
description:
48+
"disallow lazy quantifiers at the end of an expression",
49+
// TODO Switch to recommended in the major version.
50+
// recommended: true,
51+
recommended: false,
52+
default: "warn",
53+
},
54+
schema: [],
55+
messages: {
56+
uselessElement:
57+
"The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.",
58+
uselessQuantifier:
59+
"The quantifier can be removed because the quantifier is lazy and has a minimum of 1.",
60+
uselessRange:
61+
"The quantifier can be replaced with '{{{min}}}' because the quantifier is lazy and has a minimum of {{min}}.",
62+
},
63+
type: "problem",
64+
},
65+
create(context) {
66+
const sourceCode = context.getSourceCode()
67+
68+
/**
69+
* Create visitor
70+
* @param node
71+
*/
72+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
73+
return {
74+
onPatternEnter(pNode) {
75+
for (const lazy of extractLazyEndQuantifiers(
76+
pNode.alternatives,
77+
)) {
78+
if (lazy.min === 0) {
79+
context.report({
80+
node,
81+
loc: getRegexpLocation(sourceCode, node, lazy),
82+
messageId: "uselessElement",
83+
})
84+
} else if (lazy.min === 1) {
85+
context.report({
86+
node,
87+
loc: getRegexpLocation(sourceCode, node, lazy),
88+
messageId: "uselessQuantifier",
89+
})
90+
} else {
91+
context.report({
92+
node,
93+
loc: getRegexpLocation(sourceCode, node, lazy),
94+
messageId: "uselessRange",
95+
data: {
96+
min: String(lazy.min),
97+
},
98+
})
99+
}
100+
}
101+
},
102+
}
103+
}
104+
105+
return defineRegexpVisitor(context, {
106+
createVisitor,
107+
})
108+
},
109+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import noEmptyGroup from "../rules/no-empty-group"
1111
import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
1212
import noEscapeBackspace from "../rules/no-escape-backspace"
1313
import noInvisibleCharacter from "../rules/no-invisible-character"
14+
import noLazyEnds from "../rules/no-lazy-ends"
1415
import noLegacyFeatures from "../rules/no-legacy-features"
1516
import noOctal from "../rules/no-octal"
1617
import noUnusedCapturingGroup from "../rules/no-unused-capturing-group"
@@ -51,6 +52,7 @@ export const rules = [
5152
noEmptyLookaroundsAssertion,
5253
noEscapeBackspace,
5354
noInvisibleCharacter,
55+
noLazyEnds,
5456
noLegacyFeatures,
5557
noOctal,
5658
noUnusedCapturingGroup,

tests/lib/rules/no-lazy-ends.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/no-lazy-ends"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("no-lazy-ends", rule as any, {
12+
valid: [
13+
`/a+?b*/`,
14+
`/a??(?:ba+?|c)*/`,
15+
`/ba*?$/`,
16+
17+
`/a{3}?/`, // uselessly lazy but that's not for this rule to correct
18+
],
19+
invalid: [
20+
{
21+
code: `/a??/`,
22+
errors: [
23+
{
24+
message:
25+
"The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.",
26+
line: 1,
27+
column: 2,
28+
},
29+
],
30+
},
31+
{
32+
code: `/a*?/`,
33+
errors: [
34+
{
35+
message:
36+
"The quantifier and the quantified element can be removed because the quantifier is lazy and has a minimum of 0.",
37+
line: 1,
38+
column: 2,
39+
},
40+
],
41+
},
42+
{
43+
code: `/a+?/`,
44+
errors: [
45+
{
46+
message:
47+
"The quantifier can be removed because the quantifier is lazy and has a minimum of 1.",
48+
line: 1,
49+
column: 2,
50+
},
51+
],
52+
},
53+
{
54+
code: `/a{3,7}?/`,
55+
errors: [
56+
{
57+
message:
58+
"The quantifier can be replaced with '{3}' because the quantifier is lazy and has a minimum of 3.",
59+
line: 1,
60+
column: 2,
61+
},
62+
],
63+
},
64+
{
65+
code: `/a{3,}?/`,
66+
errors: [
67+
{
68+
message:
69+
"The quantifier can be replaced with '{3}' because the quantifier is lazy and has a minimum of 3.",
70+
line: 1,
71+
column: 2,
72+
},
73+
],
74+
},
75+
76+
{
77+
code: `/(?:a|b(c+?))/`,
78+
errors: [
79+
{
80+
message:
81+
"The quantifier can be removed because the quantifier is lazy and has a minimum of 1.",
82+
line: 1,
83+
column: 9,
84+
},
85+
],
86+
},
87+
{
88+
code: `/a(?:c|ab+?)?/`,
89+
errors: [
90+
{
91+
message:
92+
"The quantifier can be removed because the quantifier is lazy and has a minimum of 1.",
93+
line: 1,
94+
column: 9,
95+
},
96+
],
97+
},
98+
],
99+
})

0 commit comments

Comments
 (0)