Skip to content

Commit af2209e

Browse files
authored
Add regexp/no-extra-lookaround-assertions rule (#482)
1 parent c5a8550 commit af2209e

File tree

6 files changed

+313
-0
lines changed

6 files changed

+313
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
138138
| [regexp/control-character-escape](https://ota-meshi.github.io/eslint-plugin-regexp/rules/control-character-escape.html) | enforce consistent escaping of control characters | :star::wrench: |
139139
| [regexp/negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/negation.html) | enforce use of escapes on negation | :star::wrench: |
140140
| [regexp/no-dupe-characters-character-class](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-dupe-characters-character-class.html) | disallow duplicate characters in the RegExp character class | :star::wrench: |
141+
| [regexp/no-extra-lookaround-assertions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-extra-lookaround-assertions.html) | disallow unnecessary nested lookaround assertions | :wrench: |
141142
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
142143
| [regexp/no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow legacy RegExp features | :star: |
143144
| [regexp/no-non-standard-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-non-standard-flag.html) | disallow non-standard flags | :star: |

docs/rules/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
4343
| [regexp/control-character-escape](./control-character-escape.md) | enforce consistent escaping of control characters | :star::wrench: |
4444
| [regexp/negation](./negation.md) | enforce use of escapes on negation | :star::wrench: |
4545
| [regexp/no-dupe-characters-character-class](./no-dupe-characters-character-class.md) | disallow duplicate characters in the RegExp character class | :star::wrench: |
46+
| [regexp/no-extra-lookaround-assertions](./no-extra-lookaround-assertions.md) | disallow unnecessary nested lookaround assertions | :wrench: |
4647
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
4748
| [regexp/no-legacy-features](./no-legacy-features.md) | disallow legacy RegExp features | :star: |
4849
| [regexp/no-non-standard-flag](./no-non-standard-flag.md) | disallow non-standard flags | :star: |
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "regexp/no-extra-lookaround-assertions"
5+
description: "disallow unnecessary nested lookaround assertions"
6+
---
7+
# regexp/no-extra-lookaround-assertions
8+
9+
> disallow unnecessary nested lookaround assertions
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+
The last positive lookahead assertion within a lookahead assertion is the same without lookahead assertions.
17+
Also, The first positive lookbehind assertion within a lookbehind assertion is the same without lookbehind assertions.
18+
They can be inlined or converted to group.
19+
20+
```js
21+
/a(?=b(?=c))/u; /* -> */ /a(?=bc)/u;
22+
/a(?=b(?=c|C))/u; /* -> */ /a(?=b(?:c|C))/u;
23+
24+
/(?<=(?<=a)b)c/u; /* -> */ /(?<=ab)c/u;
25+
/(?<=(?<=a|A)b)c/u; /* -> */ /(?<=(?:a|A)b)c/u;
26+
```
27+
28+
This rule aims to report and fix these unnecessary lookaround assertions.
29+
30+
<eslint-code-block fix>
31+
32+
```js
33+
/* eslint regexp/no-extra-lookaround-assertions: "error" */
34+
35+
/* ✓ GOOD */
36+
var ts = 'JavaScript'.replace(/Java(?=Script)/u, 'Type');
37+
var java = 'JavaScript'.replace(/(?<=Java)Script/u, '');
38+
var re1 = /a(?=bc)/u;
39+
var re2 = /a(?=b(?:c|C))/u;
40+
var re3 = /(?<=ab)c/u;
41+
var re4 = /(?<=(?:a|A)b)c/u;
42+
43+
/* ✗ BAD */
44+
var ts = 'JavaScript'.replace(/Java(?=Scrip(?=t))/u, 'Type');
45+
var java = 'JavaScript'.replace(/(?<=(?<=J)ava)Script/u, '');
46+
var re1 = /a(?=b(?=c))/u;
47+
var re2 = /a(?=b(?=c|C))/u;
48+
var re3 = /(?<=(?<=a)b)c/u;
49+
var re4 = /(?<=(?<=a|A)b)c/u;
50+
```
51+
52+
</eslint-code-block>
53+
54+
## :wrench: Options
55+
56+
Nothing.
57+
58+
## :mag: Implementation
59+
60+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-extra-lookaround-assertions.ts)
61+
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-extra-lookaround-assertions.ts)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import type { LookaroundAssertion } from "regexpp/ast"
2+
import type { RegExpVisitor } from "regexpp/visitor"
3+
import type { RegExpContext } from "../utils"
4+
import { createRule, defineRegexpVisitor } from "../utils"
5+
6+
export default createRule("no-extra-lookaround-assertions", {
7+
meta: {
8+
docs: {
9+
description: "disallow unnecessary nested lookaround assertions",
10+
category: "Best Practices",
11+
// TODO Switch to recommended in the major version.
12+
// recommended: true,
13+
recommended: false,
14+
},
15+
fixable: "code",
16+
schema: [],
17+
messages: {
18+
canBeInlined:
19+
"This {{kind}} assertion is useless and can be inlined.",
20+
canBeConvertedIntoGroup:
21+
"This {{kind}} assertion is useless and can be converted into a group.",
22+
},
23+
type: "suggestion",
24+
},
25+
create(context) {
26+
/**
27+
* Create visitor
28+
*/
29+
function createVisitor(
30+
regexpContext: RegExpContext,
31+
): RegExpVisitor.Handlers {
32+
return {
33+
onAssertionEnter(aNode) {
34+
if (
35+
aNode.kind === "lookahead" ||
36+
aNode.kind === "lookbehind"
37+
) {
38+
verify(regexpContext, aNode)
39+
}
40+
},
41+
}
42+
}
43+
44+
/** Verify for lookaround assertion */
45+
function verify(
46+
regexpContext: RegExpContext,
47+
assertion: LookaroundAssertion,
48+
) {
49+
for (const alternative of assertion.alternatives) {
50+
const nested = at(
51+
alternative.elements,
52+
assertion.kind === "lookahead"
53+
? // The last positive lookahead assertion within
54+
// a lookahead assertion is the same without the assertion.
55+
-1
56+
: // The first positive lookbehind assertion within
57+
// a lookbehind assertion is the same without the assertion.
58+
0,
59+
)
60+
if (
61+
nested?.type === "Assertion" &&
62+
nested.kind === assertion.kind &&
63+
!nested.negate
64+
) {
65+
reportLookaroundAssertion(regexpContext, nested)
66+
}
67+
}
68+
}
69+
70+
/** Report */
71+
function reportLookaroundAssertion(
72+
{ node, getRegexpLocation, fixReplaceNode }: RegExpContext,
73+
assertion: LookaroundAssertion,
74+
) {
75+
let messageId, replaceText
76+
if (assertion.alternatives.length === 1) {
77+
messageId = "canBeInlined"
78+
// unwrap `(?=` and `)`, `(?<=` and `)`
79+
replaceText = assertion.alternatives[0].raw
80+
} else {
81+
messageId = "canBeConvertedIntoGroup"
82+
// replace `?=` with `?:`, or `?<=` with `?:`
83+
replaceText = `(?:${assertion.alternatives
84+
.map((alt) => alt.raw)
85+
.join("|")})`
86+
}
87+
88+
context.report({
89+
node,
90+
loc: getRegexpLocation(assertion),
91+
messageId,
92+
data: {
93+
kind: assertion.kind,
94+
},
95+
fix: fixReplaceNode(assertion, replaceText),
96+
})
97+
}
98+
99+
return defineRegexpVisitor(context, {
100+
createVisitor,
101+
})
102+
},
103+
})
104+
105+
// TODO After dropping support for Node < v16.6.0 we can use native `.at()`.
106+
/**
107+
* `.at()` polyfill
108+
* see https://github.com/tc39/proposal-relative-indexing-method#polyfill
109+
*/
110+
function at<T>(array: T[], n: number) {
111+
// ToInteger() abstract op
112+
let num = Math.trunc(n) || 0
113+
// Allow negative indexing from the end
114+
if (num < 0) num += array.length
115+
// OOB access is guaranteed to return undefined
116+
if (num < 0 || num >= array.length) return undefined
117+
// Otherwise, this is just normal property access
118+
return array[num]
119+
}

lib/utils/rules.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import noEmptyCharacterClass from "../rules/no-empty-character-class"
1616
import noEmptyGroup from "../rules/no-empty-group"
1717
import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
1818
import noEscapeBackspace from "../rules/no-escape-backspace"
19+
import noExtraLookaroundAssertions from "../rules/no-extra-lookaround-assertions"
1920
import noInvalidRegexp from "../rules/no-invalid-regexp"
2021
import noInvisibleCharacter from "../rules/no-invisible-character"
2122
import noLazyEnds from "../rules/no-lazy-ends"
@@ -95,6 +96,7 @@ export const rules = [
9596
noEmptyGroup,
9697
noEmptyLookaroundsAssertion,
9798
noEscapeBackspace,
99+
noExtraLookaroundAssertions,
98100
noInvalidRegexp,
99101
noInvisibleCharacter,
100102
noLazyEnds,
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../lib/rules/no-extra-lookaround-assertions"
3+
4+
const tester = new RuleTester({
5+
parserOptions: {
6+
ecmaVersion: 2020,
7+
sourceType: "module",
8+
},
9+
})
10+
11+
tester.run("no-extra-lookaround-assertions", rule as any, {
12+
valid: [
13+
`console.log('JavaScript'.replace(/Java(?=Script)/u, 'Type'))`,
14+
`console.log('JavaScript'.replace(/(?<=Java)Script/u, ''))`,
15+
],
16+
invalid: [
17+
{
18+
code: `console.log('JavaScript'.replace(/Java(?=Scrip(?=t))/u, 'Type'))`,
19+
output: `console.log('JavaScript'.replace(/Java(?=Script)/u, 'Type'))`,
20+
errors: [
21+
{
22+
message:
23+
"This lookahead assertion is useless and can be inlined.",
24+
column: 47,
25+
},
26+
],
27+
},
28+
{
29+
code: `console.log('JavaScript'.replace(/(?<=(?<=J)ava)Script/u, ''))`,
30+
output: `console.log('JavaScript'.replace(/(?<=Java)Script/u, ''))`,
31+
errors: [
32+
{
33+
message:
34+
"This lookbehind assertion is useless and can be inlined.",
35+
column: 39,
36+
},
37+
],
38+
},
39+
// Within negate
40+
{
41+
code: `console.log('JavaScript Java JavaRuntime'.replace(/Java(?!Scrip(?=t))/gu, 'Python'))`,
42+
output: `console.log('JavaScript Java JavaRuntime'.replace(/Java(?!Script)/gu, 'Python'))`,
43+
errors: [
44+
{
45+
message:
46+
"This lookahead assertion is useless and can be inlined.",
47+
column: 64,
48+
},
49+
],
50+
},
51+
{
52+
code: `console.log('JavaScript TypeScript ActionScript'.replace(/(?<!(?<=J)ava)Script/gu, 'ScriptCompiler'))`,
53+
output: `console.log('JavaScript TypeScript ActionScript'.replace(/(?<!Java)Script/gu, 'ScriptCompiler'))`,
54+
errors: [
55+
{
56+
message:
57+
"This lookbehind assertion is useless and can be inlined.",
58+
column: 63,
59+
},
60+
],
61+
},
62+
// Multiple alternatives
63+
{
64+
code: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?=Checker|Linter))/gu, 'Type'))`,
65+
output: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?:Checker|Linter))/gu, 'Type'))`,
66+
errors: [
67+
{
68+
message:
69+
"This lookahead assertion is useless and can be converted into a group.",
70+
column: 72,
71+
},
72+
],
73+
},
74+
{
75+
code: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?=(?:Check|Lint)er))/gu, 'Type'))`,
76+
output: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?:Check|Lint)er)/gu, 'Type'))`,
77+
errors: [
78+
{
79+
message:
80+
"This lookahead assertion is useless and can be inlined.",
81+
column: 72,
82+
},
83+
],
84+
},
85+
{
86+
code: `console.log('ESLint JSLint TSLint'.replace(/(?<=(?<=J|T)S)Lint/gu, '-Runtime'))`,
87+
output: `console.log('ESLint JSLint TSLint'.replace(/(?<=(?:J|T)S)Lint/gu, '-Runtime'))`,
88+
errors: [
89+
{
90+
message:
91+
"This lookbehind assertion is useless and can be converted into a group.",
92+
column: 49,
93+
},
94+
],
95+
},
96+
{
97+
code: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?=Checker)|Script(?=Linter))/gu, 'Type'))`,
98+
output: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=ScriptChecker|ScriptLinter)/gu, 'Type'))`,
99+
errors: [
100+
{
101+
message:
102+
"This lookahead assertion is useless and can be inlined.",
103+
column: 72,
104+
},
105+
{
106+
message:
107+
"This lookahead assertion is useless and can be inlined.",
108+
column: 90,
109+
},
110+
],
111+
},
112+
{
113+
code: `console.log('ESLint JSLint TSLint'.replace(/(?<=(?<=J)S|(?<=T)S)Lint/gu, '-Runtime'))`,
114+
output: `console.log('ESLint JSLint TSLint'.replace(/(?<=JS|TS)Lint/gu, '-Runtime'))`,
115+
errors: [
116+
{
117+
message:
118+
"This lookbehind assertion is useless and can be inlined.",
119+
column: 49,
120+
},
121+
{
122+
message:
123+
"This lookbehind assertion is useless and can be inlined.",
124+
column: 57,
125+
},
126+
],
127+
},
128+
],
129+
})

0 commit comments

Comments
 (0)