Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add regexp/no-extra-lookaround-assertions rule #482

Merged
merged 8 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
},
],
},
],
})