Skip to content

Commit ca8357d

Browse files
Updated no-useless-non-greedy rule (#143)
1 parent b3baa15 commit ca8357d

File tree

5 files changed

+138
-36
lines changed

5 files changed

+138
-36
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
110110
| [regexp/no-useless-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-escape.html) | disallow unnecessary escape characters in RegExp | |
111111
| [regexp/no-useless-exactly-quantifier](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-exactly-quantifier.html) | disallow unnecessary exactly quantifier | :star: |
112112
| [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: |
113-
| [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: |
113+
| [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: |
114114
| [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: |
115115
| [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: |
116116
| [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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
3838
| [regexp/no-useless-escape](./no-useless-escape.md) | disallow unnecessary escape characters in RegExp | |
3939
| [regexp/no-useless-exactly-quantifier](./no-useless-exactly-quantifier.md) | disallow unnecessary exactly quantifier | :star: |
4040
| [regexp/no-useless-non-capturing-group](./no-useless-non-capturing-group.md) | disallow unnecessary Non-capturing group | :wrench: |
41-
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |
41+
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessarily non-greedy quantifiers | :wrench: |
4242
| [regexp/no-useless-range](./no-useless-range.md) | disallow unnecessary range of characters by using a hyphen | :wrench: |
4343
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star::wrench: |
4444
| [regexp/optimal-lookaround-quantifier](./optimal-lookaround-quantifier.md) | disallow the alternatives of lookarounds that end with a non-constant quantifier | |

docs/rules/no-useless-non-greedy.md

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,30 @@
22
pageClass: "rule-details"
33
sidebarDepth: 0
44
title: "regexp/no-useless-non-greedy"
5-
description: "disallow unnecessary quantifier non-greedy (`?`)"
5+
description: "disallow unnecessarily non-greedy quantifiers"
66
since: "v0.3.0"
77
---
88
# regexp/no-useless-non-greedy
99

10-
> disallow unnecessary quantifier non-greedy (`?`)
10+
> disallow unnecessarily non-greedy quantifiers
1111
1212
- :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.
1313

1414
## :book: Rule Details
1515

16-
This rule reports unnecessary quantifier non-greedy (`?`).
16+
This rule reports lazy quantifiers that don't need to by lazy.
17+
18+
There are two reasons why a lazy quantifier doesn't have to lazy:
19+
20+
1. It's a constant quantifier (e.g. `a{3}?`).
21+
22+
2. The quantifier is effectively possessive (e.g. `a+?b`).
23+
24+
Whether a quantifier (let's call it _q_) is effectively possessive depends on the expression after it (let's call it _e_). _q_ is effectively possessive if _q_ cannot accept the character accepted by _e_ and _e_ cannot accept the characters accepted by _q_.
25+
26+
In the example above, the character `a` and the character `b` do not overlap. Therefore the quantifier `a+` is possessive.
27+
28+
Since an effectively possessive quantifier cannot give up characters to the expression after it, it doesn't matter whether the quantifier greedy or lazy. However, greedy quantifiers should be preferred because they require less characters to write and are easier to visually parse.
1729

1830
<eslint-code-block fix>
1931

@@ -25,11 +37,13 @@ var foo = /a*?/;
2537
var foo = /a+?/;
2638
var foo = /a{4,}?/;
2739
var foo = /a{2,4}?/;
40+
var foo = /a[\s\S]*?bar/;
2841

2942
/* ✗ BAD */
3043
var foo = /a{1}?/;
3144
var foo = /a{4}?/;
3245
var foo = /a{2,2}?/;
46+
var foo = /ab+?c/;
3347
```
3448

3549
</eslint-code-block>

lib/rules/no-useless-non-greedy.ts

Lines changed: 92 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,63 @@
1-
import type { Expression } from "estree"
1+
import type { Rule, SourceCode } from "eslint"
2+
import type { Expression, SourceLocation } from "estree"
3+
import {
4+
getMatchingDirection,
5+
getFirstConsumedChar,
6+
getFirstCharAfter,
7+
} from "regexp-ast-analysis"
8+
import type { Quantifier } from "regexpp/ast"
29
import type { RegExpVisitor } from "regexpp/visitor"
310
import {
411
createRule,
512
defineRegexpVisitor,
613
getRegexpLocation,
714
getRegexpRange,
15+
parseFlags,
816
} from "../utils"
917

18+
/**
19+
* Returns a fix that makes the given quantifier greedy.
20+
*/
21+
function makeGreedy(
22+
sourceCode: SourceCode,
23+
node: Expression,
24+
qNode: Quantifier,
25+
) {
26+
return (fixer: Rule.RuleFixer): Rule.Fix | null => {
27+
const range = getRegexpRange(sourceCode, node, qNode)
28+
if (range == null) {
29+
return null
30+
}
31+
return fixer.removeRange([range[1] - 1, range[1]])
32+
}
33+
}
34+
35+
/**
36+
* Returns the source location of the lazy modifier of the given quantifier.
37+
*/
38+
function getLazyLoc(
39+
sourceCode: SourceCode,
40+
node: Expression,
41+
qNode: Quantifier,
42+
): SourceLocation {
43+
const offset = qNode.raw.length - 1
44+
return getRegexpLocation(sourceCode, node, qNode, [offset, offset + 1])
45+
}
46+
1047
export default createRule("no-useless-non-greedy", {
1148
meta: {
1249
docs: {
13-
description: "disallow unnecessary quantifier non-greedy (`?`)",
50+
description: "disallow unnecessarily non-greedy quantifiers",
1451
// TODO In the major version
1552
// recommended: true,
1653
recommended: false,
1754
},
1855
fixable: "code",
1956
schema: [],
2057
messages: {
21-
unexpected: "Unexpected quantifier non-greedy.",
58+
constant: "Unexpected non-greedy constant quantifier.",
59+
possessive:
60+
"Unexpected non-greedy constant quantifier. The quantifier is effectively possessive, so it doesn't matter whether it is greedy or not.",
2261
},
2362
type: "suggestion", // "problem",
2463
},
@@ -29,33 +68,61 @@ export default createRule("no-useless-non-greedy", {
2968
* Create visitor
3069
* @param node
3170
*/
32-
function createVisitor(node: Expression): RegExpVisitor.Handlers {
71+
function createVisitor(
72+
node: Expression,
73+
_pattern: string,
74+
flagsStr: string,
75+
): RegExpVisitor.Handlers {
76+
const flags = parseFlags(flagsStr)
77+
3378
return {
3479
onQuantifierEnter(qNode) {
35-
if (qNode.greedy === false && qNode.min === qNode.max) {
36-
const offset = qNode.raw.length - 1
80+
if (qNode.greedy) {
81+
return
82+
}
83+
84+
if (qNode.min === qNode.max) {
85+
// a constant lazy quantifier (e.g. /a{2}?/)
86+
3787
context.report({
3888
node,
39-
loc: getRegexpLocation(sourceCode, node, qNode, [
40-
offset,
41-
offset + 1,
42-
]),
43-
messageId: "unexpected",
44-
fix(fixer) {
45-
const range = getRegexpRange(
46-
sourceCode,
47-
node,
48-
qNode,
49-
)
50-
if (range == null) {
51-
return null
52-
}
53-
return fixer.removeRange([
54-
range[1] - 1,
55-
range[1],
56-
])
57-
},
89+
loc: getLazyLoc(sourceCode, node, qNode),
90+
messageId: "constant",
91+
fix: makeGreedy(sourceCode, node, qNode),
5892
})
93+
return
94+
}
95+
96+
// This is more tricky.
97+
// The basic idea here is that if the first character of the
98+
// quantified element and the first character of whatever
99+
// comes after the quantifier are always different, then the
100+
// lazy modifier doesn't matter.
101+
// E.g. /a+?b+/ == /a+b+/
102+
103+
const matchingDir = getMatchingDirection(qNode)
104+
const firstChar = getFirstConsumedChar(
105+
qNode,
106+
matchingDir,
107+
flags,
108+
)
109+
if (!firstChar.empty) {
110+
const after = getFirstCharAfter(
111+
qNode,
112+
matchingDir,
113+
flags,
114+
)
115+
if (
116+
!after.edge &&
117+
firstChar.char.isDisjointWith(after.char)
118+
) {
119+
context.report({
120+
node,
121+
loc: getLazyLoc(sourceCode, node, qNode),
122+
messageId: "possessive",
123+
fix: makeGreedy(sourceCode, node, qNode),
124+
})
125+
}
59126
}
60127
},
61128
}

tests/lib/rules/no-useless-non-greedy.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,24 @@ const tester = new RuleTester({
99
})
1010

1111
tester.run("no-useless-non-greedy", rule as any, {
12-
valid: [`/a*?/`, `/a+?/`, `/a{4,}?/`, `/a{2,4}?/`, `/a{2,2}/`, `/a{3}/`],
12+
valid: [
13+
`/a*?/`,
14+
`/a+?/`,
15+
`/a{4,}?/`,
16+
`/a{2,4}?/`,
17+
`/a{2,2}/`,
18+
`/a{3}/`,
19+
`/a+?b*/`,
20+
`/[\\s\\S]+?bar/`,
21+
`/a??a?/`,
22+
],
1323
invalid: [
1424
{
1525
code: `/a{1}?/`,
1626
output: `/a{1}/`,
1727
errors: [
1828
{
19-
message: "Unexpected quantifier non-greedy.",
29+
messageId: "constant",
2030
line: 1,
2131
column: 6,
2232
endLine: 1,
@@ -29,7 +39,7 @@ tester.run("no-useless-non-greedy", rule as any, {
2939
output: `/a{4}/`,
3040
errors: [
3141
{
32-
message: "Unexpected quantifier non-greedy.",
42+
messageId: "constant",
3343
line: 1,
3444
column: 6,
3545
endLine: 1,
@@ -40,20 +50,31 @@ tester.run("no-useless-non-greedy", rule as any, {
4050
{
4151
code: `/a{2,2}?/`,
4252
output: `/a{2,2}/`,
43-
errors: ["Unexpected quantifier non-greedy."],
53+
errors: [{ messageId: "constant" }],
4454
},
4555
{
4656
code: String.raw`const s = "\\d{1}?"
4757
new RegExp(s)`,
4858
output: String.raw`const s = "\\d{1}"
4959
new RegExp(s)`,
50-
errors: ["Unexpected quantifier non-greedy."],
60+
errors: [{ messageId: "constant" }],
5161
},
5262
{
5363
code: String.raw`const s = "\\d"+"{1}?"
5464
new RegExp(s)`,
5565
output: null,
56-
errors: ["Unexpected quantifier non-greedy."],
66+
errors: [{ messageId: "constant" }],
67+
},
68+
69+
{
70+
code: `/a+?b+/`,
71+
output: `/a+b+/`,
72+
errors: [{ messageId: "possessive" }],
73+
},
74+
{
75+
code: `/(?:a|cd)+?(?:b+|zzz)/`,
76+
output: `/(?:a|cd)+(?:b+|zzz)/`,
77+
errors: [{ messageId: "possessive" }],
5778
},
5879
],
5980
})

0 commit comments

Comments
 (0)