Skip to content

Commit feaf121

Browse files
Add regexp/optimal-lookaround-quantifier rule (#97)
* Add `regexp/optimal-lookaround-quantifier` rule * Refactored optimal lookahead quantifier (#101) Co-authored-by: Michael Schmidt <[email protected]>
1 parent 30edbf5 commit feaf121

File tree

6 files changed

+271
-0
lines changed

6 files changed

+271
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
108108
| [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: |
109109
| [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: |
110110
| [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: |
111+
| [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 | |
111112
| [regexp/order-in-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/order-in-character-class.html) | enforces elements order in character class | :wrench: |
112113
| [regexp/prefer-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-character-class.html) | enforce using character class | :wrench: |
113114
| [regexp/prefer-d](https://ota-meshi.github.io/eslint-plugin-regexp/rules/prefer-d.html) | enforce using `\d` | :star::wrench: |

docs/rules/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
3636
| [regexp/no-useless-non-greedy](./no-useless-non-greedy.md) | disallow unnecessary quantifier non-greedy (`?`) | :wrench: |
3737
| [regexp/no-useless-range](./no-useless-range.md) | disallow unnecessary range of characters by using a hyphen | :wrench: |
3838
| [regexp/no-useless-two-nums-quantifier](./no-useless-two-nums-quantifier.md) | disallow unnecessary `{n,m}` quantifier | :star: |
39+
| [regexp/optimal-lookaround-quantifier](./optimal-lookaround-quantifier.md) | disallow the alternatives of lookarounds that end with a non-constant quantifier | |
3940
| [regexp/order-in-character-class](./order-in-character-class.md) | enforces elements order in character class | :wrench: |
4041
| [regexp/prefer-character-class](./prefer-character-class.md) | enforce using character class | :wrench: |
4142
| [regexp/prefer-d](./prefer-d.md) | enforce using `\d` | :star::wrench: |
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/optimal-lookaround-quantifier"
5+
description: "disallow the alternatives of lookarounds that end with a non-constant quantifier"
6+
---
7+
# regexp/optimal-lookaround-quantifier
8+
9+
> disallow the alternatives of lookarounds that end with a non-constant 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+
13+
## :book: Rule Details
14+
15+
Non-constant quantifiers are quantifiers that describe a range (e.g. `?`, `*`,
16+
`+`, `{0,1}`, `{5,9}`, `{3,}`). They have to match some number of times (the
17+
minimum) after which further matches are optional until a certain maximum (may
18+
be infinite) is reached.
19+
20+
It's obvious that `/ba{2}/` and `/ba{2,6}/` will match differently because of
21+
the different quantifiers of `a` but that not the case if for lookarounds. Both
22+
`/b(?=a{2})/` and `/b(?=a{2,6})/` will match strings the same way. I.e. for the
23+
input string `"baaa"`, both will create the same match arrays. The two regular
24+
expression are actually equivalent, meaning that `(?=a{2})` is equivalent to
25+
`(?=a{2,6})`.
26+
27+
More generally, if a non-constant quantifier is an **end** of the expression
28+
tree of a **lookahead**, that quantifier can be replaced with a constant
29+
quantifier that matched the element minimum-if-the-non-constant-quantifier many
30+
times. For **lookbehinds**, the non-constant quantifier has to be at the
31+
**start** of the expression tree as lookbehinds are matched from right to left.
32+
33+
<eslint-code-block>
34+
35+
```js
36+
/* eslint regexp/optimal-lookaround-quantifier: "error" */
37+
38+
/* ✓ GOOD */
39+
// lookaheads
40+
var foo = /\w+(?=\s*:)/;
41+
42+
// lookbehinds
43+
var foo = /(?<=ab+)/;
44+
45+
/* ✗ BAD */
46+
// lookaheads
47+
var foo = /(?=ab+)/; // == /(?=ab)/
48+
var foo = /(?=ab*)/; // == /(?=a)/
49+
var foo = /(?!ab?)/; // == /(?!a)/
50+
var foo = /(?!ab{6,})/; // == /(?!ab{6})/
51+
52+
// lookbehinds
53+
var foo = /(?<=a+b)/; // == /(?<=ab)/
54+
var foo = /(?<!\w*\s*,)/; // == /(?<!,)/
55+
```
56+
57+
</eslint-code-block>
58+
59+
## :wrench: Options
60+
61+
Nothing.
62+
63+
## :heart: Compatibility
64+
65+
This rule was taken from [eslint-plugin-clean-regex].
66+
This rule is compatible with [clean-regex/optimal-lookaround-quantifier] rule.
67+
68+
[eslint-plugin-clean-regex]: https://github.com/RunDevelopment/eslint-plugin-clean-regex
69+
[clean-regex/optimal-lookaround-quantifier]: https://github.com/RunDevelopment/eslint-plugin-clean-regex/blob/master/docs/rules/optimal-lookaround-quantifier.md
70+
71+
## :mag: Implementation
72+
73+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/optimal-lookaround-quantifier.ts)
74+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/optimal-lookaround-quantifier.ts)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { Expression } from "estree"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { Alternative, LookaroundAssertion, Quantifier } from "regexpp/ast"
4+
import { createRule, defineRegexpVisitor, getRegexpLocation } from "../utils"
5+
6+
/**
7+
* Extract invalid quantifiers for lookarounds
8+
*/
9+
function* extractInvalidQuantifiers(
10+
alternatives: Alternative[],
11+
kind: LookaroundAssertion["kind"],
12+
): IterableIterator<Quantifier> {
13+
for (const { elements } of alternatives) {
14+
if (elements.length > 0) {
15+
const lastIndex = kind === "lookahead" ? elements.length - 1 : 0
16+
const last = elements[lastIndex]
17+
switch (last.type) {
18+
case "Quantifier":
19+
if (last.min !== last.max) {
20+
// TODO: last might contain a capturing group in which cause, we can't change the quantifier
21+
yield last
22+
}
23+
break
24+
25+
case "Group":
26+
yield* extractInvalidQuantifiers(last.alternatives, kind)
27+
break
28+
29+
// we ignore capturing groups on purpose.
30+
// Example: /(?=(a*))\w+\1/ (no ideal but it illustrates the point)
31+
32+
default:
33+
break
34+
}
35+
}
36+
}
37+
}
38+
39+
const END_START_PHRASE = {
40+
lookahead: "end",
41+
lookbehind: "start",
42+
}
43+
44+
export default createRule("optimal-lookaround-quantifier", {
45+
meta: {
46+
docs: {
47+
description:
48+
"disallow the alternatives of lookarounds that end with a non-constant quantifier",
49+
// TODO Switch to recommended in the major version.
50+
// recommended: true,
51+
recommended: false,
52+
default: "warn",
53+
},
54+
schema: [],
55+
messages: {
56+
remove:
57+
"The quantified expression {{expr}} at the {{endOrStart}} of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround.",
58+
replacedWith:
59+
"The quantified expression {{expr}} at the {{endOrStart}} of the expression tree should only be matched a constant number of times. The expression can be replaced with {{replacer}} without affecting the lookaround.",
60+
},
61+
type: "problem",
62+
},
63+
create(context) {
64+
const sourceCode = context.getSourceCode()
65+
66+
/**
67+
* Create visitor
68+
* @param node
69+
*/
70+
function createVisitor(node: Expression): RegExpVisitor.Handlers {
71+
return {
72+
onAssertionEnter(aNode) {
73+
if (
74+
aNode.kind === "lookahead" ||
75+
aNode.kind === "lookbehind"
76+
) {
77+
const endOrStart = END_START_PHRASE[aNode.kind]
78+
const quantifiers = extractInvalidQuantifiers(
79+
aNode.alternatives,
80+
aNode.kind,
81+
)
82+
83+
for (const q of quantifiers) {
84+
const replacer =
85+
q.min === 0
86+
? ""
87+
: q.min === 1
88+
? `${q.element.raw} (no quantifier)`
89+
: `${q.element.raw}{${q.min}}`
90+
91+
context.report({
92+
node,
93+
loc: getRegexpLocation(sourceCode, node, q),
94+
messageId:
95+
q.min === 0 ? "remove" : "replacedWith",
96+
data: {
97+
expr: q.raw,
98+
endOrStart,
99+
replacer,
100+
},
101+
})
102+
}
103+
}
104+
},
105+
}
106+
}
107+
108+
return defineRegexpVisitor(context, {
109+
createVisitor,
110+
})
111+
},
112+
})

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import noUselessNonCapturingGroup from "../rules/no-useless-non-capturing-group"
2424
import noUselessNonGreedy from "../rules/no-useless-non-greedy"
2525
import noUselessRange from "../rules/no-useless-range"
2626
import noUselessTwoNumsQuantifier from "../rules/no-useless-two-nums-quantifier"
27+
import optimalLookaroundQuantifier from "../rules/optimal-lookaround-quantifier"
2728
import orderInCharacterClass from "../rules/order-in-character-class"
2829
import preferCharacterClass from "../rules/prefer-character-class"
2930
import preferD from "../rules/prefer-d"
@@ -65,6 +66,7 @@ export const rules = [
6566
noUselessNonGreedy,
6667
noUselessRange,
6768
noUselessTwoNumsQuantifier,
69+
optimalLookaroundQuantifier,
6870
orderInCharacterClass,
6971
preferCharacterClass,
7072
preferD,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/optimal-lookaround-quantifier"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("optimal-lookaround-quantifier", rule as any, {
12+
valid: [String.raw`/(?=(a*))\w+\1/`, `/(?<=a{4})/`],
13+
invalid: [
14+
{
15+
code: `/(?=ba*)/`,
16+
errors: [
17+
{
18+
message:
19+
"The quantified expression a* at the end of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround.",
20+
line: 1,
21+
column: 6,
22+
},
23+
],
24+
},
25+
{
26+
code: `/(?=(?:a|b|abc*))/`,
27+
errors: [
28+
{
29+
message:
30+
"The quantified expression c* at the end of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround.",
31+
line: 1,
32+
column: 14,
33+
},
34+
],
35+
},
36+
{
37+
code: `/(?=(?:a|b|abc+))/`,
38+
errors: [
39+
{
40+
message:
41+
"The quantified expression c+ at the end of the expression tree should only be matched a constant number of times. The expression can be replaced with c (no quantifier) without affecting the lookaround.",
42+
line: 1,
43+
column: 14,
44+
},
45+
],
46+
},
47+
{
48+
code: `/(?=(?:a|b|abc{4,9}))/`,
49+
errors: [
50+
{
51+
message:
52+
"The quantified expression c{4,9} at the end of the expression tree should only be matched a constant number of times. The expression can be replaced with c{4} without affecting the lookaround.",
53+
line: 1,
54+
column: 14,
55+
},
56+
],
57+
},
58+
{
59+
code: `/(?<=[a-c]*)/`,
60+
errors: [
61+
{
62+
message:
63+
"The quantified expression [a-c]* at the start of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround.",
64+
line: 1,
65+
column: 6,
66+
},
67+
],
68+
},
69+
{
70+
code: `/(?<=(c)*ab)/`,
71+
errors: [
72+
{
73+
message:
74+
"The quantified expression (c)* at the start of the expression tree should only be matched a constant number of times. The expression can be removed without affecting the lookaround.",
75+
line: 1,
76+
column: 6,
77+
},
78+
],
79+
},
80+
],
81+
})

0 commit comments

Comments
 (0)