Skip to content

Commit

Permalink
Add regexp/no-extra-lookaround-assertions rule (#482)
Browse files Browse the repository at this point in the history
  • Loading branch information
ota-meshi committed Nov 21, 2022
1 parent c5a8550 commit af2209e
Show file tree
Hide file tree
Showing 6 changed files with 313 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [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: |
| [regexp/negation](https://ota-meshi.github.io/eslint-plugin-regexp/rules/negation.html) | enforce use of escapes on negation | :star::wrench: |
| [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: |
| [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: |
| [regexp/no-invisible-character](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-invisible-character.html) | disallow invisible raw character | :star::wrench: |
| [regexp/no-legacy-features](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-legacy-features.html) | disallow legacy RegExp features | :star: |
| [regexp/no-non-standard-flag](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-non-standard-flag.html) | disallow non-standard flags | :star: |
Expand Down
1 change: 1 addition & 0 deletions docs/rules/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ The rules with the following star :star: are included in the `plugin:regexp/reco
| [regexp/control-character-escape](./control-character-escape.md) | enforce consistent escaping of control characters | :star::wrench: |
| [regexp/negation](./negation.md) | enforce use of escapes on negation | :star::wrench: |
| [regexp/no-dupe-characters-character-class](./no-dupe-characters-character-class.md) | disallow duplicate characters in the RegExp character class | :star::wrench: |
| [regexp/no-extra-lookaround-assertions](./no-extra-lookaround-assertions.md) | disallow unnecessary nested lookaround assertions | :wrench: |
| [regexp/no-invisible-character](./no-invisible-character.md) | disallow invisible raw character | :star::wrench: |
| [regexp/no-legacy-features](./no-legacy-features.md) | disallow legacy RegExp features | :star: |
| [regexp/no-non-standard-flag](./no-non-standard-flag.md) | disallow non-standard flags | :star: |
Expand Down
61 changes: 61 additions & 0 deletions docs/rules/no-extra-lookaround-assertions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "regexp/no-extra-lookaround-assertions"
description: "disallow unnecessary nested lookaround assertions"
---
# regexp/no-extra-lookaround-assertions

> disallow unnecessary nested lookaround assertions
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
- :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.

## :book: Rule Details

The last positive lookahead assertion within a lookahead assertion is the same without lookahead assertions.
Also, The first positive lookbehind assertion within a lookbehind assertion is the same without lookbehind assertions.
They can be inlined or converted to group.

```js
/a(?=b(?=c))/u; /* -> */ /a(?=bc)/u;
/a(?=b(?=c|C))/u; /* -> */ /a(?=b(?:c|C))/u;

/(?<=(?<=a)b)c/u; /* -> */ /(?<=ab)c/u;
/(?<=(?<=a|A)b)c/u; /* -> */ /(?<=(?:a|A)b)c/u;
```
This rule aims to report and fix these unnecessary lookaround assertions.
<eslint-code-block fix>
```js
/* eslint regexp/no-extra-lookaround-assertions: "error" */

/* ✓ GOOD */
var ts = 'JavaScript'.replace(/Java(?=Script)/u, 'Type');
var java = 'JavaScript'.replace(/(?<=Java)Script/u, '');
var re1 = /a(?=bc)/u;
var re2 = /a(?=b(?:c|C))/u;
var re3 = /(?<=ab)c/u;
var re4 = /(?<=(?:a|A)b)c/u;

/* ✗ BAD */
var ts = 'JavaScript'.replace(/Java(?=Scrip(?=t))/u, 'Type');
var java = 'JavaScript'.replace(/(?<=(?<=J)ava)Script/u, '');
var re1 = /a(?=b(?=c))/u;
var re2 = /a(?=b(?=c|C))/u;
var re3 = /(?<=(?<=a)b)c/u;
var re4 = /(?<=(?<=a|A)b)c/u;
```
</eslint-code-block>
## :wrench: Options
Nothing.
## :mag: Implementation
- [Rule source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/lib/rules/no-extra-lookaround-assertions.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-regexp/blob/master/tests/lib/rules/no-extra-lookaround-assertions.ts)
119 changes: 119 additions & 0 deletions lib/rules/no-extra-lookaround-assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type { LookaroundAssertion } from "regexpp/ast"
import type { RegExpVisitor } from "regexpp/visitor"
import type { RegExpContext } from "../utils"
import { createRule, defineRegexpVisitor } from "../utils"

export default createRule("no-extra-lookaround-assertions", {
meta: {
docs: {
description: "disallow unnecessary nested lookaround assertions",
category: "Best Practices",
// TODO Switch to recommended in the major version.
// recommended: true,
recommended: false,
},
fixable: "code",
schema: [],
messages: {
canBeInlined:
"This {{kind}} assertion is useless and can be inlined.",
canBeConvertedIntoGroup:
"This {{kind}} assertion is useless and can be converted into a group.",
},
type: "suggestion",
},
create(context) {
/**
* Create visitor
*/
function createVisitor(
regexpContext: RegExpContext,
): RegExpVisitor.Handlers {
return {
onAssertionEnter(aNode) {
if (
aNode.kind === "lookahead" ||
aNode.kind === "lookbehind"
) {
verify(regexpContext, aNode)
}
},
}
}

/** Verify for lookaround assertion */
function verify(
regexpContext: RegExpContext,
assertion: LookaroundAssertion,
) {
for (const alternative of assertion.alternatives) {
const nested = at(
alternative.elements,
assertion.kind === "lookahead"
? // The last positive lookahead assertion within
// a lookahead assertion is the same without the assertion.
-1
: // The first positive lookbehind assertion within
// a lookbehind assertion is the same without the assertion.
0,
)
if (
nested?.type === "Assertion" &&
nested.kind === assertion.kind &&
!nested.negate
) {
reportLookaroundAssertion(regexpContext, nested)
}
}
}

/** Report */
function reportLookaroundAssertion(
{ node, getRegexpLocation, fixReplaceNode }: RegExpContext,
assertion: LookaroundAssertion,
) {
let messageId, replaceText
if (assertion.alternatives.length === 1) {
messageId = "canBeInlined"
// unwrap `(?=` and `)`, `(?<=` and `)`
replaceText = assertion.alternatives[0].raw
} else {
messageId = "canBeConvertedIntoGroup"
// replace `?=` with `?:`, or `?<=` with `?:`
replaceText = `(?:${assertion.alternatives
.map((alt) => alt.raw)
.join("|")})`
}

context.report({
node,
loc: getRegexpLocation(assertion),
messageId,
data: {
kind: assertion.kind,
},
fix: fixReplaceNode(assertion, replaceText),
})
}

return defineRegexpVisitor(context, {
createVisitor,
})
},
})

// TODO After dropping support for Node < v16.6.0 we can use native `.at()`.
/**
* `.at()` polyfill
* see https://github.com/tc39/proposal-relative-indexing-method#polyfill
*/
function at<T>(array: T[], n: number) {
// ToInteger() abstract op
let num = Math.trunc(n) || 0
// Allow negative indexing from the end
if (num < 0) num += array.length
// OOB access is guaranteed to return undefined
if (num < 0 || num >= array.length) return undefined
// Otherwise, this is just normal property access
return array[num]
}
2 changes: 2 additions & 0 deletions lib/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import noEmptyCharacterClass from "../rules/no-empty-character-class"
import noEmptyGroup from "../rules/no-empty-group"
import noEmptyLookaroundsAssertion from "../rules/no-empty-lookarounds-assertion"
import noEscapeBackspace from "../rules/no-escape-backspace"
import noExtraLookaroundAssertions from "../rules/no-extra-lookaround-assertions"
import noInvalidRegexp from "../rules/no-invalid-regexp"
import noInvisibleCharacter from "../rules/no-invisible-character"
import noLazyEnds from "../rules/no-lazy-ends"
Expand Down Expand Up @@ -95,6 +96,7 @@ export const rules = [
noEmptyGroup,
noEmptyLookaroundsAssertion,
noEscapeBackspace,
noExtraLookaroundAssertions,
noInvalidRegexp,
noInvisibleCharacter,
noLazyEnds,
Expand Down
129 changes: 129 additions & 0 deletions tests/lib/rules/no-extra-lookaround-assertions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { RuleTester } from "eslint"
import rule from "../../../lib/rules/no-extra-lookaround-assertions"

const tester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: "module",
},
})

tester.run("no-extra-lookaround-assertions", rule as any, {
valid: [
`console.log('JavaScript'.replace(/Java(?=Script)/u, 'Type'))`,
`console.log('JavaScript'.replace(/(?<=Java)Script/u, ''))`,
],
invalid: [
{
code: `console.log('JavaScript'.replace(/Java(?=Scrip(?=t))/u, 'Type'))`,
output: `console.log('JavaScript'.replace(/Java(?=Script)/u, 'Type'))`,
errors: [
{
message:
"This lookahead assertion is useless and can be inlined.",
column: 47,
},
],
},
{
code: `console.log('JavaScript'.replace(/(?<=(?<=J)ava)Script/u, ''))`,
output: `console.log('JavaScript'.replace(/(?<=Java)Script/u, ''))`,
errors: [
{
message:
"This lookbehind assertion is useless and can be inlined.",
column: 39,
},
],
},
// Within negate
{
code: `console.log('JavaScript Java JavaRuntime'.replace(/Java(?!Scrip(?=t))/gu, 'Python'))`,
output: `console.log('JavaScript Java JavaRuntime'.replace(/Java(?!Script)/gu, 'Python'))`,
errors: [
{
message:
"This lookahead assertion is useless and can be inlined.",
column: 64,
},
],
},
{
code: `console.log('JavaScript TypeScript ActionScript'.replace(/(?<!(?<=J)ava)Script/gu, 'ScriptCompiler'))`,
output: `console.log('JavaScript TypeScript ActionScript'.replace(/(?<!Java)Script/gu, 'ScriptCompiler'))`,
errors: [
{
message:
"This lookbehind assertion is useless and can be inlined.",
column: 63,
},
],
},
// Multiple alternatives
{
code: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?=Checker|Linter))/gu, 'Type'))`,
output: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?:Checker|Linter))/gu, 'Type'))`,
errors: [
{
message:
"This lookahead assertion is useless and can be converted into a group.",
column: 72,
},
],
},
{
code: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?=(?:Check|Lint)er))/gu, 'Type'))`,
output: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?:Check|Lint)er)/gu, 'Type'))`,
errors: [
{
message:
"This lookahead assertion is useless and can be inlined.",
column: 72,
},
],
},
{
code: `console.log('ESLint JSLint TSLint'.replace(/(?<=(?<=J|T)S)Lint/gu, '-Runtime'))`,
output: `console.log('ESLint JSLint TSLint'.replace(/(?<=(?:J|T)S)Lint/gu, '-Runtime'))`,
errors: [
{
message:
"This lookbehind assertion is useless and can be converted into a group.",
column: 49,
},
],
},
{
code: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=Script(?=Checker)|Script(?=Linter))/gu, 'Type'))`,
output: `console.log('JavaScriptChecker JavaScriptLinter'.replace(/Java(?=ScriptChecker|ScriptLinter)/gu, 'Type'))`,
errors: [
{
message:
"This lookahead assertion is useless and can be inlined.",
column: 72,
},
{
message:
"This lookahead assertion is useless and can be inlined.",
column: 90,
},
],
},
{
code: `console.log('ESLint JSLint TSLint'.replace(/(?<=(?<=J)S|(?<=T)S)Lint/gu, '-Runtime'))`,
output: `console.log('ESLint JSLint TSLint'.replace(/(?<=JS|TS)Lint/gu, '-Runtime'))`,
errors: [
{
message:
"This lookbehind assertion is useless and can be inlined.",
column: 49,
},
{
message:
"This lookbehind assertion is useless and can be inlined.",
column: 57,
},
],
},
],
})

0 comments on commit af2209e

Please sign in to comment.