Skip to content

Commit 938a180

Browse files
authored
Fix false negatives for escape char class in range in regexp/no-dupe-characters-character-class rule (#90)
1 parent feaf121 commit 938a180

File tree

2 files changed

+120
-29
lines changed

2 files changed

+120
-29
lines changed

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

Lines changed: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {
77
EscapeCharacterSet,
88
UnicodePropertyCharacterSet,
99
CharacterClassRange,
10+
CharacterSet,
1011
} from "regexpp/ast"
1112
import {
1213
createRule,
@@ -15,14 +16,16 @@ import {
1516
CP_LOW_LINE,
1617
CP_RANGE_DIGIT,
1718
CP_RANGE_SPACES,
18-
CPS_SINGLE_SPACES,
19+
CPS_SINGLE_SPACES as CPS_SINGLE_SPACES_SET,
1920
CP_RANGES_WORDS,
2021
isDigit,
2122
isSpace,
2223
isWord,
2324
invisibleEscape,
2425
} from "../utils"
2526

27+
const CPS_SINGLE_SPACES = [...CPS_SINGLE_SPACES_SET]
28+
2629
/**
2730
* Checks if the given character is within the character class range.
2831
* @param char The character to check.
@@ -102,10 +105,13 @@ function getCharacterClassRangesIntersection(
102105
function getCharacterClassRangeAndCharacterSetIntersections(
103106
range: CharacterClassRange,
104107
set: EscapeCharacterSet | UnicodePropertyCharacterSet,
105-
) {
108+
): {
109+
intersections: (number | [number, number])[]
110+
set?: EscapeCharacterSet | UnicodePropertyCharacterSet
111+
} {
106112
if (set.negate) {
107113
// It does not check negate character set.
108-
return []
114+
return { intersections: [] }
109115
}
110116
const codePointRange = [range.min.value, range.max.value] as const
111117

@@ -120,6 +126,22 @@ function getCharacterClassRangeAndCharacterSetIntersections(
120126
)
121127
}
122128

129+
/**
130+
* Checks if the given code point is in code point range.
131+
*/
132+
function isCodePointInRange(codePoint: number) {
133+
return codePointRange[0] <= codePoint && codePoint <= codePointRange[1]
134+
}
135+
136+
/**
137+
* Checks if the given code point range is in code point range.
138+
*/
139+
function isRangeInRange(cpRange: readonly [number, number]) {
140+
return (
141+
codePointRange[0] <= cpRange[0] && cpRange[1] <= codePointRange[1]
142+
)
143+
}
144+
123145
/**
124146
* Gets the intersections that do not separate the range.
125147
* @param otherRange the range to check
@@ -146,33 +168,59 @@ function getCharacterClassRangeAndCharacterSetIntersections(
146168
codePointRange,
147169
CP_RANGE_DIGIT,
148170
)
149-
return intersection ? [intersection] : []
171+
return { intersections: intersection ? [intersection] : [] }
150172
}
151173
if (set.kind === "space") {
174+
if (
175+
isRangeInRange(CP_RANGE_SPACES) &&
176+
CPS_SINGLE_SPACES.every((codePoint) =>
177+
isCodePointInRange(codePoint),
178+
)
179+
) {
180+
// EscapeCharacterSet in range
181+
return {
182+
intersections: [],
183+
set,
184+
}
185+
}
152186
const result: number[] = []
153187
for (const codePoint of CPS_SINGLE_SPACES) {
154188
if (isCodePointIsRangeEdge(codePoint)) {
155189
result.push(codePoint)
156190
}
157191
}
158192
const intersection = getIntersectionAndNotSeparate(CP_RANGE_SPACES)
159-
return intersection ? [...result, intersection] : result
193+
return {
194+
intersections: intersection ? [...result, intersection] : result,
195+
}
160196
}
161197
if (set.kind === "word") {
198+
if (
199+
isCodePointInRange(CP_LOW_LINE) &&
200+
CP_RANGES_WORDS.every((r) => isRangeInRange(r))
201+
) {
202+
// EscapeCharacterSet in range
203+
return {
204+
intersections: [],
205+
set,
206+
}
207+
}
162208
const intersections: [number, number][] = []
163209
for (const wordRange of CP_RANGES_WORDS) {
164210
const intersection = getIntersectionAndNotSeparate(wordRange)
165211
if (intersection) {
166212
intersections.push(intersection)
167213
}
168214
}
169-
return isCodePointIsRangeEdge(CP_LOW_LINE)
170-
? [...intersections, CP_LOW_LINE]
171-
: intersections
215+
return {
216+
intersections: isCodePointIsRangeEdge(CP_LOW_LINE)
217+
? [...intersections, CP_LOW_LINE]
218+
: intersections,
219+
}
172220
}
173221

174222
// It does not check Unicode properties.
175-
return []
223+
return { intersections: [] }
176224
}
177225

178226
/**
@@ -216,9 +264,9 @@ function groupingElements(elements: CharacterClassElement[]) {
216264
}
217265

218266
return {
219-
characters,
220-
characterClassRanges,
221-
characterSets,
267+
characters: [...characters.values()],
268+
characterClassRanges: [...characterClassRanges.values()],
269+
characterSets: [...characterSets.values()],
222270
}
223271

224272
/**
@@ -259,6 +307,7 @@ export default createRule("no-dupe-characters-character-class", {
259307
messages: {
260308
duplicates: "Unexpected element '{{element}}' duplication.",
261309
charIsIncluded: "The '{{char}}' is included in '{{element}}'.",
310+
charSetIsInRange: "The '{{charSet}}' is included in '{{range}}'.",
262311
intersect:
263312
"Unexpected intersection of '{{elementA}}' and '{{elementB}}' was found '{{intersection}}'.",
264313
},
@@ -346,6 +395,25 @@ export default createRule("no-dupe-characters-character-class", {
346395
}
347396
}
348397

398+
/**
399+
* Report the character set included in the range.
400+
*/
401+
function reportCharSetInRange(
402+
node: Expression,
403+
set: CharacterSet,
404+
range: CharacterClassRange,
405+
) {
406+
context.report({
407+
node,
408+
loc: getRegexpLocation(sourceCode, node, set),
409+
messageId: "charSetIsInRange",
410+
data: {
411+
charSet: set.raw,
412+
range: range.raw,
413+
},
414+
})
415+
}
416+
349417
/**
350418
* Create visitor
351419
* @param node
@@ -359,12 +427,12 @@ export default createRule("no-dupe-characters-character-class", {
359427
characterSets,
360428
} = groupingElements(ccNode.elements)
361429

362-
for (const [char, ...dupeChars] of characters.values()) {
430+
for (const [char, ...dupeChars] of characters) {
363431
if (dupeChars.length) {
364432
reportDuplicates(node, [char, ...dupeChars])
365433
}
366434

367-
for (const [range] of characterClassRanges.values()) {
435+
for (const [range] of characterClassRanges) {
368436
if (isCharacterInCharacterClassRange(char, range)) {
369437
reportCharIncluded(
370438
node,
@@ -373,7 +441,7 @@ export default createRule("no-dupe-characters-character-class", {
373441
)
374442
}
375443
}
376-
for (const [set] of characterSets.values()) {
444+
for (const [set] of characterSets) {
377445
if (isCharacterInCharacterSet(char, set)) {
378446
reportCharIncluded(
379447
node,
@@ -384,21 +452,14 @@ export default createRule("no-dupe-characters-character-class", {
384452
}
385453
}
386454

387-
for (const [
388-
key,
389-
[range, ...dupeRanges],
390-
] of characterClassRanges) {
455+
for (const [range, ...dupeRanges] of characterClassRanges) {
391456
if (dupeRanges.length) {
392457
reportDuplicates(node, [range, ...dupeRanges])
393458
}
394459

395-
for (const [
396-
keyOther,
397-
[rangeOther],
398-
] of characterClassRanges) {
399-
if (keyOther === key) {
400-
continue
401-
}
460+
for (const [rangeOther] of characterClassRanges.filter(
461+
([ccr]) => ccr !== range,
462+
)) {
402463
const intersection = getCharacterClassRangesIntersection(
403464
range,
404465
rangeOther,
@@ -413,11 +474,17 @@ export default createRule("no-dupe-characters-character-class", {
413474
}
414475
}
415476

416-
for (const [set] of characterSets.values()) {
417-
const intersections = getCharacterClassRangeAndCharacterSetIntersections(
477+
for (const [set] of characterSets) {
478+
const {
479+
set: reportSet,
480+
intersections,
481+
} = getCharacterClassRangeAndCharacterSetIntersections(
418482
range,
419483
set,
420484
)
485+
if (reportSet) {
486+
reportCharSetInRange(node, reportSet, range)
487+
}
421488
for (const intersection of intersections) {
422489
reportIntersect(
423490
node,
@@ -428,7 +495,7 @@ export default createRule("no-dupe-characters-character-class", {
428495
}
429496
}
430497
}
431-
for (const [set, ...dupeSets] of characterSets.values()) {
498+
for (const [set, ...dupeSets] of characterSets) {
432499
if (dupeSets.length) {
433500
reportDuplicates(node, [set, ...dupeSets])
434501
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,5 +374,29 @@ tester.run("no-dupe-characters-character-class", rule as any, {
374374
},
375375
],
376376
},
377+
{
378+
code: String.raw`/[\w0-z]/`,
379+
errors: [
380+
{
381+
message: "The '\\w' is included in '0-z'.",
382+
line: 1,
383+
column: 3,
384+
endLine: 1,
385+
endColumn: 5,
386+
},
387+
],
388+
},
389+
{
390+
code: String.raw`/[\t-\uFFFF\s]/`,
391+
errors: [
392+
{
393+
message: "The '\\s' is included in '\\t-\\uFFFF'.",
394+
line: 1,
395+
column: 12,
396+
endLine: 1,
397+
endColumn: 14,
398+
},
399+
],
400+
},
377401
],
378402
})

0 commit comments

Comments
 (0)