Skip to content

Commit fd683c5

Browse files
authored
Add support for v flag to regexp/no-dupe-characters-character-class rule (#608)
* Add support for v flag to `regexp/no-dupe-characters-character-class` rule * Create friendly-walls-reply.md
1 parent 17239ab commit fd683c5

File tree

3 files changed

+90
-46
lines changed

3 files changed

+90
-46
lines changed

.changeset/friendly-walls-reply.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 support for v flag to `regexp/no-dupe-characters-character-class` rule

lib/rules/no-dupe-characters-character-class.ts

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import type {
88
CharacterClassRange,
99
CharacterSet,
1010
AnyCharacterSet,
11+
UnicodeSetsCharacterClass,
12+
ClassStringDisjunction,
13+
ExpressionCharacterClass,
1114
} from "@eslint-community/regexpp/ast"
1215
import type { RegExpContext } from "../utils"
1316
import {
@@ -20,8 +23,9 @@ import {
2023
import type { CharRange, CharSet } from "refa"
2124
import { JS } from "refa"
2225
import type { ReadonlyFlags } from "regexp-ast-analysis"
23-
import { toCharSet } from "regexp-ast-analysis"
26+
import { toCharSet, toUnicodeSet } from "regexp-ast-analysis"
2427
import { mentionChar } from "../utils/mention"
28+
import { assertNever } from "../utils/util"
2529

2630
interface Grouping {
2731
duplicates: {
@@ -30,7 +34,13 @@ interface Grouping {
3034
}[]
3135
characters: Character[]
3236
characterRanges: CharacterClassRange[]
33-
characterSets: (EscapeCharacterSet | UnicodePropertyCharacterSet)[]
37+
characterSetAndClasses: (
38+
| EscapeCharacterSet
39+
| UnicodePropertyCharacterSet
40+
| UnicodeSetsCharacterClass
41+
| ClassStringDisjunction
42+
| ExpressionCharacterClass
43+
)[]
3444
}
3545

3646
/**
@@ -44,9 +54,13 @@ function groupElements(
4454
const duplicates: Grouping["duplicates"] = []
4555
const characters = new Map<number, Character>()
4656
const characterRanges = new Map<string, CharacterClassRange>()
47-
const characterSets = new Map<
57+
const characterSetAndClasses = new Map<
4858
string,
49-
EscapeCharacterSet | UnicodePropertyCharacterSet
59+
| EscapeCharacterSet
60+
| UnicodePropertyCharacterSet
61+
| UnicodeSetsCharacterClass
62+
| ClassStringDisjunction
63+
| ExpressionCharacterClass
5064
>()
5165

5266
/**
@@ -68,27 +82,32 @@ function groupElements(
6882
}
6983

7084
for (const e of elements) {
71-
// FIXME: TS Error
72-
// @ts-expect-error -- FIXME
73-
const charSet = toCharSet(e, flags)
74-
7585
if (e.type === "Character") {
86+
const charSet = toCharSet(e, flags)
7687
const key = charSet.ranges[0].min
7788
addToGroup(characters, key, e)
7889
} else if (e.type === "CharacterClassRange") {
90+
const charSet = toCharSet(e, flags)
7991
const key = buildRangeKey(charSet)
8092
addToGroup(characterRanges, key, e)
81-
} else if (e.type === "CharacterSet") {
93+
} else if (
94+
e.type === "CharacterSet" ||
95+
e.type === "CharacterClass" ||
96+
e.type === "ClassStringDisjunction" ||
97+
e.type === "ExpressionCharacterClass"
98+
) {
8299
const key = e.raw
83-
addToGroup(characterSets, key, e)
100+
addToGroup(characterSetAndClasses, key, e)
101+
} else {
102+
assertNever(e)
84103
}
85104
}
86105

87106
return {
88107
duplicates,
89108
characters: [...characters.values()],
90109
characterRanges: [...characterRanges.values()],
91-
characterSets: [...characterSets.values()],
110+
characterSetAndClasses: [...characterSetAndClasses.values()],
92111
}
93112

94113
function buildRangeKey(rangeCharSet: CharSet) {
@@ -197,7 +216,10 @@ export default createRule("no-dupe-characters-character-class", {
197216
subsetElement: CharacterClassElement,
198217
element:
199218
| Exclude<CharacterSet, AnyCharacterSet>
200-
| CharacterClassRange,
219+
| CharacterClassRange
220+
| UnicodeSetsCharacterClass
221+
| ClassStringDisjunction
222+
| ExpressionCharacterClass,
201223
) {
202224
const { node, getRegexpLocation } = regexpContext
203225

@@ -254,9 +276,12 @@ export default createRule("no-dupe-characters-character-class", {
254276
duplicates,
255277
characters,
256278
characterRanges,
257-
characterSets,
279+
characterSetAndClasses,
258280
} = groupElements(ccNode.elements, flags)
259-
const rangesAndSets = [...characterRanges, ...characterSets]
281+
const elementsOtherThanCharacter = [
282+
...characterRanges,
283+
...characterSetAndClasses,
284+
]
260285

261286
// keep track of all reported subset elements
262287
const subsets = new Set<CharacterClassElement>()
@@ -269,10 +294,10 @@ export default createRule("no-dupe-characters-character-class", {
269294

270295
// report characters that are already matched by some range or set
271296
for (const char of characters) {
272-
for (const other of rangesAndSets) {
273-
// FIXME: TS Error
274-
// @ts-expect-error -- FIXME
275-
if (toCharSet(other, flags).has(char.value)) {
297+
for (const other of elementsOtherThanCharacter) {
298+
if (
299+
toUnicodeSet(other, flags).chars.has(char.value)
300+
) {
276301
reportSubset(regexpContext, char, other)
277302
subsets.add(char)
278303
break
@@ -281,19 +306,15 @@ export default createRule("no-dupe-characters-character-class", {
281306
}
282307

283308
// report character ranges and sets that are already matched by some range or set
284-
for (const element of rangesAndSets) {
285-
for (const other of rangesAndSets) {
309+
for (const element of elementsOtherThanCharacter) {
310+
for (const other of elementsOtherThanCharacter) {
286311
if (element === other || subsets.has(other)) {
287312
continue
288313
}
289314

290315
if (
291-
// FIXME: TS Error
292-
// @ts-expect-error -- FIXME
293-
toCharSet(element, flags).isSubsetOf(
294-
// FIXME: TS Error
295-
// @ts-expect-error -- FIXME
296-
toCharSet(other, flags),
316+
toUnicodeSet(element, flags).isSubsetOf(
317+
toUnicodeSet(other, flags),
297318
)
298319
) {
299320
reportSubset(regexpContext, element, other)
@@ -305,34 +326,28 @@ export default createRule("no-dupe-characters-character-class", {
305326

306327
// character ranges and sets might be a subset of a combination of other elements
307328
// e.g. `b-d` is a subset of `a-cd-f`
308-
const characterTotal = toCharSet(
329+
const characterTotal = toUnicodeSet(
309330
characters.filter((c) => !subsets.has(c)),
310331
flags,
311332
)
312-
for (const element of rangesAndSets) {
333+
for (const element of elementsOtherThanCharacter) {
313334
if (subsets.has(element)) {
314335
continue
315336
}
316337

317338
const totalOthers = characterTotal.union(
318-
...rangesAndSets
339+
...elementsOtherThanCharacter
319340
.filter((e) => !subsets.has(e) && e !== element)
320-
// FIXME: TS Error
321-
// @ts-expect-error -- FIXME
322-
.map((e) => toCharSet(e, flags)),
341+
.map((e) => toUnicodeSet(e, flags)),
323342
)
324343

325-
// FIXME: TS Error
326-
// @ts-expect-error -- FIXME
327-
const elementCharSet = toCharSet(element, flags)
344+
const elementCharSet = toUnicodeSet(element, flags)
328345
if (elementCharSet.isSubsetOf(totalOthers)) {
329346
const superSetElements = ccNode.elements
330347
.filter((e) => !subsets.has(e) && e !== element)
331348
.filter(
332349
(e) =>
333-
// FIXME: TS Error
334-
// @ts-expect-error -- FIXME
335-
!toCharSet(e, flags).isDisjointWith(
350+
!toUnicodeSet(e, flags).isDisjointWith(
336351
elementCharSet,
337352
),
338353
)
@@ -354,18 +369,20 @@ export default createRule("no-dupe-characters-character-class", {
354369
continue
355370
}
356371

357-
for (let j = i + 1; j < rangesAndSets.length; j++) {
358-
const other = rangesAndSets[j]
372+
for (
373+
let j = i + 1;
374+
j < elementsOtherThanCharacter.length;
375+
j++
376+
) {
377+
const other = elementsOtherThanCharacter[j]
359378
if (range === other || subsets.has(other)) {
360379
continue
361380
}
362381

363-
const intersection = toCharSet(
382+
const intersection = toUnicodeSet(
364383
range,
365384
flags,
366-
// FIXME: TS Error
367-
// @ts-expect-error -- FIXME
368-
).intersect(toCharSet(other, flags))
385+
).intersect(toUnicodeSet(other, flags))
369386
if (intersection.isEmpty) {
370387
continue
371388
}
@@ -375,7 +392,8 @@ export default createRule("no-dupe-characters-character-class", {
375392
// character range.
376393
// there is no point in reporting overlaps that can't be fixed.
377394
const interestingRanges =
378-
intersection.ranges.filter(
395+
// Range and string never intersect, so we can only check `chars`.
396+
intersection.chars.ranges.filter(
379397
(r) =>
380398
inRange(r, range.min.value) ||
381399
inRange(r, range.max.value),

tests/lib/rules/no-dupe-characters-character-class.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import rule from "../../../lib/rules/no-dupe-characters-character-class"
33

44
const tester = new RuleTester({
55
parserOptions: {
6-
ecmaVersion: 2020,
6+
ecmaVersion: "latest",
77
sourceType: "module",
88
},
99
})
@@ -21,6 +21,7 @@ tester.run("no-dupe-characters-character-class", rule as any, {
2121
"/[\\w\\p{L}]/u",
2222
"/\\p{ASCII}abc/u",
2323
String.raw`/[\u1fff-\u2020\s]/`,
24+
String.raw`/[\q{a}\q{ab}\q{abc}[\w--[ab]][\w&&b]]/v`,
2425
// error
2526
"var r = new RegExp('[\\\\wA-Za-z0-9_][invalid');",
2627
],
@@ -685,5 +686,25 @@ tester.run("no-dupe-characters-character-class", rule as any, {
685686
output: null,
686687
errors: 1,
687688
},
689+
// v flags
690+
{
691+
code: String.raw`/[\q{a}aa-c[\w--b][\w&&a]]/v`,
692+
output: String.raw`/[aa-c[\w--b]]/v`,
693+
errors: [
694+
"'\\q{a}' is already included in 'a-c' (U+0061 - U+0063).",
695+
"'a' (U+0061) is already included in 'a-c' (U+0061 - U+0063).",
696+
"Unexpected overlap of 'a-c' (U+0061 - U+0063) and '[\\w--b]' was found '[ac]'.",
697+
"'[\\w&&a]' is already included in 'a-c' (U+0061 - U+0063).",
698+
],
699+
},
700+
{
701+
code: String.raw`/[\q{abc}\q{abc|ab}[\q{abc}--b][\q{abc}&&\q{abc|ab}]]/v`,
702+
output: String.raw`/[\q{abc|ab}[\q{abc}&&\q{abc|ab}]]/v`,
703+
errors: [
704+
"'\\q{abc}' is already included in '\\q{abc|ab}'.",
705+
"'[\\q{abc}--b]' is already included in '\\q{abc|ab}'.",
706+
"'[\\q{abc}&&\\q{abc|ab}]' is already included in '\\q{abc|ab}'.",
707+
],
708+
},
688709
],
689710
})

0 commit comments

Comments
 (0)