Skip to content

Commit 8b134e7

Browse files
authored
Fix false positives for backreference and invalid escape in regexp/no-useless-character-class rule (#42)
1 parent c0f9a06 commit 8b134e7

File tree

3 files changed

+138
-1
lines changed

3 files changed

+138
-1
lines changed

lib/rules/no-useless-character-class.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Expression } from "estree"
22
import type { RegExpVisitor } from "regexpp/visitor"
33
import {
4+
canUnwrapped,
45
createRule,
56
defineRegexpVisitor,
67
fixerApplyEscape,
@@ -85,6 +86,9 @@ export default createRule("no-useless-character-class", {
8586
) {
8687
return
8788
}
89+
if (!canUnwrapped(ccNode, element.raw)) {
90+
return
91+
}
8892
} else if (element.type === "CharacterClassRange") {
8993
if (element.min.value !== element.max.value) {
9094
return

lib/utils/index.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
import type * as ESTree from "estree"
22
import type { RuleListener, RuleModule, PartialRuleModule } from "../types"
33
import type { RegExpVisitor } from "regexpp/visitor"
4-
import type { Node as RegExpNode, Quantifier } from "regexpp/ast"
4+
import type {
5+
Alternative,
6+
AnyCharacterSet,
7+
Assertion,
8+
Backreference,
9+
CapturingGroup,
10+
CharacterClass,
11+
Group,
12+
Node as RegExpNode,
13+
Quantifier,
14+
} from "regexpp/ast"
515
import { RegExpParser, visitRegExpAST } from "regexpp"
616
import {
717
CALL,
@@ -345,3 +355,112 @@ export function getQuantifierOffsets(qNode: Quantifier): [number, number] {
345355
const endOffset = qNode.raw.length - (qNode.greedy ? 0 : 1)
346356
return [startOffset, endOffset]
347357
}
358+
359+
/* eslint-disable complexity -- X( */
360+
/**
361+
* Check the siblings to see if the regex doesn't change when unwrapped.
362+
*/
363+
export function canUnwrapped(
364+
/* eslint-enable complexity -- X( */
365+
node:
366+
| CharacterClass
367+
| Group
368+
| CapturingGroup
369+
| Assertion
370+
| Quantifier
371+
| AnyCharacterSet
372+
| Backreference,
373+
text: string,
374+
): boolean {
375+
const { alternative, index } = getAlternativeAndIndex()
376+
if (index === 0) {
377+
return true
378+
}
379+
if (/^\d+$/u.test(text)) {
380+
let prevIndex = index - 1
381+
let prev = alternative.elements[prevIndex]
382+
if (prev.type === "Backreference") {
383+
// e.g. /()\1[0]/ -> /()\10/
384+
return false
385+
}
386+
387+
while (
388+
prev.type === "Character" &&
389+
/^\d+$/u.test(prev.raw) &&
390+
prevIndex > 0
391+
) {
392+
prevIndex--
393+
prev = alternative.elements[prevIndex]
394+
}
395+
if (prev.type === "Character" && prev.raw === "{") {
396+
// e.g. /a{[0]}/ -> /a{0}/
397+
return false
398+
}
399+
}
400+
if (/^[0-7]+$/u.test(text)) {
401+
const prev = alternative.elements[index - 1]
402+
if (prev.type === "Character" && /^\\[0-7]+$/u.test(prev.raw)) {
403+
// e.g. /\0[1]/ -> /\01/
404+
return false
405+
}
406+
}
407+
if (/^[\da-f]+$/iu.test(text)) {
408+
let prevIndex = index - 1
409+
let prev = alternative.elements[prevIndex]
410+
while (
411+
prev.type === "Character" &&
412+
/^[\da-f]+$/iu.test(prev.raw) &&
413+
prevIndex > 0
414+
) {
415+
prevIndex--
416+
prev = alternative.elements[prevIndex]
417+
}
418+
if (
419+
prev.type === "Character" &&
420+
(prev.raw === "\\x" || prev.raw === "\\u")
421+
) {
422+
// e.g. /\xF[F]/ -> /\xFF/
423+
// e.g. /\uF[F]FF/ -> /\xFFFF/
424+
return false
425+
}
426+
}
427+
if (/^[a-z]+$/iu.test(text)) {
428+
if (index > 1) {
429+
const prev = alternative.elements[index - 1]
430+
if (prev.type === "Character" && prev.raw === "c") {
431+
const prev2 = alternative.elements[index - 2]
432+
if (prev2.type === "Character" && prev2.raw === "\\") {
433+
// e.g. /\c[M]/ -> /\cM/
434+
return false
435+
}
436+
}
437+
}
438+
}
439+
440+
return true
441+
442+
/** Get alternative and element index */
443+
function getAlternativeAndIndex() {
444+
const parent = node.parent
445+
let target:
446+
| CharacterClass
447+
| Group
448+
| CapturingGroup
449+
| Assertion
450+
| Quantifier
451+
| AnyCharacterSet
452+
| Backreference,
453+
alt: Alternative
454+
if (parent.type === "Quantifier") {
455+
alt = parent.parent
456+
target = parent
457+
} else {
458+
alt = parent
459+
target = node
460+
}
461+
return {
462+
alternative: alt,
463+
index: alt.elements.indexOf(target),
464+
}
465+
}
466+
}

tests/lib/rules/no-useless-character-class.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ tester.run("no-useless-character-class", rule as any, {
3333
String.raw`/(,)(,)(,)(,)(,) (,)(,)(,)(,)(,) (,)[\7]/ // back reference escape`,
3434
String.raw`/(,)(,)(,)(,)(,) (,)(,)(,)(,)(,) (,)[\11]/ // back reference escape`,
3535
String.raw`/\0/`,
36+
String.raw`/\1[0]/`,
37+
String.raw`/\0[1]/`,
38+
String.raw`/a{[0]}/`,
39+
String.raw`/a{123[0]}/`,
40+
`/\\xF[F]/`,
41+
`/\\xf[f]/`,
42+
`/\\x4[4]/`,
43+
`/\\uF[F]FF/`,
44+
`/\\uf[f]ff/`,
45+
`/\\u4[4]44/`,
46+
String.raw`/\c[M]/`,
47+
String.raw`/\c[A]/`,
48+
String.raw`/\c[Z]/`,
49+
String.raw`/\c[m]/`,
3650
],
3751
invalid: [
3852
{

0 commit comments

Comments
 (0)