-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
regexp/no-extra-lookaround-assertions
rule (#482)
- Loading branch information
Showing
6 changed files
with
313 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}, | ||
], | ||
}, | ||
], | ||
}) |