Skip to content

Commit 5301eef

Browse files
Add suggestions for regexp/no-useless-assertion (#666)
* Add suggestions for `regexp/no-useless-assertion` * Create unlucky-humans-fry.md * npm run update
1 parent 542c39d commit 5301eef

File tree

6 files changed

+327
-5
lines changed

6 files changed

+327
-5
lines changed

.changeset/unlucky-humans-fry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"eslint-plugin-regexp": minor
3+
---
4+
5+
Add suggestions for `regexp/no-useless-assertion`

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ The `plugin:regexp/all` config enables all rules. It's meant for testing, not fo
128128
| [no-potentially-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-potentially-useless-backreference.html) | disallow backreferences that reference a group that might not be matched | || | |
129129
| [no-super-linear-backtracking](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-super-linear-backtracking.html) | disallow exponential and polynomial backtracking || | 🔧 | |
130130
| [no-super-linear-move](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-super-linear-move.html) | disallow quantifiers that cause quadratic moves | | | | |
131-
| [no-useless-assertions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-assertions.html) | disallow assertions that are known to always accept (or reject) || | | |
131+
| [no-useless-assertions](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-assertions.html) | disallow assertions that are known to always accept (or reject) || | | 💡 |
132132
| [no-useless-backreference](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-backreference.html) | disallow useless backreferences in regular expressions || | | |
133133
| [no-useless-dollar-replacements](https://ota-meshi.github.io/eslint-plugin-regexp/rules/no-useless-dollar-replacements.html) | disallow useless `$` replacements in replacement string || | | |
134134
| [strict](https://ota-meshi.github.io/eslint-plugin-regexp/rules/strict.html) | disallow not strictly valid regular expressions || | 🔧 | 💡 |

docs/rules/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ sidebarDepth: 0
3434
| [no-potentially-useless-backreference](no-potentially-useless-backreference.md) | disallow backreferences that reference a group that might not be matched | || | |
3535
| [no-super-linear-backtracking](no-super-linear-backtracking.md) | disallow exponential and polynomial backtracking || | 🔧 | |
3636
| [no-super-linear-move](no-super-linear-move.md) | disallow quantifiers that cause quadratic moves | | | | |
37-
| [no-useless-assertions](no-useless-assertions.md) | disallow assertions that are known to always accept (or reject) || | | |
37+
| [no-useless-assertions](no-useless-assertions.md) | disallow assertions that are known to always accept (or reject) || | | 💡 |
3838
| [no-useless-backreference](no-useless-backreference.md) | disallow useless backreferences in regular expressions || | | |
3939
| [no-useless-dollar-replacements](no-useless-dollar-replacements.md) | disallow useless `$` replacements in replacement string || | | |
4040
| [strict](strict.md) | disallow not strictly valid regular expressions || | 🔧 | 💡 |

docs/rules/no-useless-assertions.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ since: "v0.9.0"
99

1010
💼 This rule is enabled in the ✅ `plugin:regexp/recommended` config.
1111

12+
💡 This rule is manually fixable by [editor suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
13+
1214
<!-- end auto-generated rule header -->
1315

1416
> disallow assertions that are known to always accept (or reject)
1517
1618
## :book: Rule Details
1719

18-
Some assertion are unnecessary because the rest of the pattern forces them to
20+
Some assertions are unnecessary because the rest of the pattern forces them to
1921
always be accept (or reject).
2022

2123
<eslint-code-block>

lib/rules/no-useless-assertions.ts

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import type { RegExpVisitor } from "@eslint-community/regexpp/visitor"
22
import type {
3+
Alternative,
34
Assertion,
45
EdgeAssertion,
56
Element,
67
LookaroundAssertion,
78
Node,
9+
Pattern,
810
WordBoundaryAssertion,
911
} from "@eslint-community/regexpp/ast"
1012
import type { RegExpContext } from "../utils"
@@ -203,6 +205,73 @@ function createReorderingGetFirstCharAfter(
203205
}
204206
}
205207

208+
function removeAlternative(
209+
alternative: Alternative,
210+
): [Element | Pattern, string] {
211+
const parent = alternative.parent
212+
if (parent.alternatives.length > 1) {
213+
// we can just remove the alternative
214+
let { start, end } = alternative
215+
if (parent.alternatives[0] === alternative) {
216+
end++
217+
} else {
218+
start--
219+
}
220+
const before = parent.raw.slice(0, start - parent.start)
221+
const after = parent.raw.slice(end - parent.start)
222+
return [parent, before + after]
223+
}
224+
225+
// we have to remove the parent as well
226+
227+
switch (parent.type) {
228+
case "Pattern":
229+
return [parent, "[]"]
230+
231+
case "Assertion": {
232+
// the inner part of the assertion always rejects
233+
const assertionParent = parent.parent
234+
if (parent.negate) {
235+
// the assertion always accepts
236+
return [
237+
assertionParent.type === "Quantifier"
238+
? assertionParent
239+
: parent,
240+
"",
241+
]
242+
}
243+
if (assertionParent.type === "Quantifier") {
244+
if (assertionParent.min === 0) {
245+
return [assertionParent, ""]
246+
}
247+
return removeAlternative(assertionParent.parent)
248+
}
249+
return removeAlternative(assertionParent)
250+
}
251+
252+
case "CapturingGroup": {
253+
// we don't remove capturing groups
254+
const before = parent.raw.slice(0, alternative.start - parent.start)
255+
const after = parent.raw.slice(alternative.end - parent.start)
256+
return [parent, `${before}[]${after}`]
257+
}
258+
259+
case "Group": {
260+
const groupParent = parent.parent
261+
if (groupParent.type === "Quantifier") {
262+
if (groupParent.min === 0) {
263+
return [groupParent, ""]
264+
}
265+
return removeAlternative(groupParent.parent)
266+
}
267+
return removeAlternative(groupParent)
268+
}
269+
270+
default:
271+
return assertNever(parent)
272+
}
273+
}
274+
206275
const messages = {
207276
alwaysRejectByChar:
208277
"{{assertion}} will always reject because it is {{followedOrPreceded}} by a character.",
@@ -226,6 +295,10 @@ const messages = {
226295
"The {{kind}} {{assertion}} will always {{acceptOrReject}}.",
227296
alwaysForNegativeLookaround:
228297
"The negative {{kind}} {{assertion}} will always {{acceptOrReject}}.",
298+
299+
acceptSuggestion: "Remove the assertion. (Replace with empty string.)",
300+
rejectSuggestion:
301+
"Remove branch of the assertion. (Replace with empty set.)",
229302
}
230303

231304
export default createRule("no-useless-assertions", {
@@ -236,6 +309,7 @@ export default createRule("no-useless-assertions", {
236309
category: "Possible Errors",
237310
recommended: true,
238311
},
312+
hasSuggestions: true,
239313
schema: [],
240314
messages,
241315
type: "problem",
@@ -245,15 +319,44 @@ export default createRule("no-useless-assertions", {
245319
node,
246320
flags,
247321
getRegexpLocation,
322+
fixReplaceNode,
248323
}: RegExpContext): RegExpVisitor.Handlers {
249324
const reported = new Set<Assertion>()
250325

326+
function replaceWithEmptyString(assertion: Assertion) {
327+
if (assertion.parent.type === "Quantifier") {
328+
// the assertion always accepts does not consume characters, we can remove the quantifier as well.
329+
return fixReplaceNode(assertion.parent, "")
330+
}
331+
return fixReplaceNode(assertion, "")
332+
}
333+
334+
function replaceWithEmptySet(assertion: Assertion) {
335+
if (assertion.parent.type === "Quantifier") {
336+
if (assertion.parent.min === 0) {
337+
// the assertion always rejects does not consume characters, we can remove the quantifier as well.
338+
return fixReplaceNode(assertion.parent, "")
339+
}
340+
const [element, replacement] = removeAlternative(
341+
assertion.parent.parent,
342+
)
343+
return fixReplaceNode(element, replacement)
344+
}
345+
const [element, replacement] = removeAlternative(
346+
assertion.parent,
347+
)
348+
return fixReplaceNode(element, replacement)
349+
}
350+
251351
function report(
252352
assertion: Assertion,
253353
messageId: keyof typeof messages,
254-
data: Record<string, string>,
354+
data: Record<string, string> & {
355+
acceptOrReject: "accept" | "reject"
356+
},
255357
) {
256358
reported.add(assertion)
359+
const { acceptOrReject } = data
257360

258361
context.report({
259362
node,
@@ -263,6 +366,15 @@ export default createRule("no-useless-assertions", {
263366
assertion: mention(assertion),
264367
...data,
265368
},
369+
suggest: [
370+
{
371+
messageId: `${acceptOrReject}Suggestion`,
372+
fix:
373+
acceptOrReject === "accept"
374+
? replaceWithEmptyString(assertion)
375+
: replaceWithEmptySet(assertion),
376+
},
377+
],
266378
})
267379
}
268380

@@ -295,6 +407,7 @@ export default createRule("no-useless-assertions", {
295407
if (next.char.isEmpty) {
296408
report(assertion, "alwaysAcceptByChar", {
297409
followedOrPreceded,
410+
acceptOrReject: "accept",
298411
})
299412
}
300413
} else {
@@ -306,6 +419,7 @@ export default createRule("no-useless-assertions", {
306419
{
307420
followedOrPreceded,
308421
startOrEnd: assertion.kind,
422+
acceptOrReject: "accept",
309423
},
310424
)
311425
}
@@ -317,6 +431,7 @@ export default createRule("no-useless-assertions", {
317431
// since the m flag isn't present any character will result in trivial rejection
318432
report(assertion, "alwaysRejectByChar", {
319433
followedOrPreceded,
434+
acceptOrReject: "reject",
320435
})
321436
} else {
322437
// only if the character is a sub set of /./, will the assertion trivially reject
@@ -325,11 +440,15 @@ export default createRule("no-useless-assertions", {
325440
report(
326441
assertion,
327442
"alwaysRejectByNonLineTerminator",
328-
{ followedOrPreceded },
443+
{
444+
followedOrPreceded,
445+
acceptOrReject: "reject",
446+
},
329447
)
330448
} else if (next.char.isSubsetOf(lineTerminator)) {
331449
report(assertion, "alwaysAcceptByLineTerminator", {
332450
followedOrPreceded,
451+
acceptOrReject: "accept",
333452
})
334453
}
335454
}

0 commit comments

Comments
 (0)