Skip to content

Commit cbf162e

Browse files
authored
Fix false positives for disallowNeverMatch option in regexp/no-dupe-disjunctions rule (#84)
1 parent 67a900d commit cbf162e

File tree

2 files changed

+85
-52
lines changed

2 files changed

+85
-52
lines changed

lib/utils/regexp-ast/is-covered.ts

Lines changed: 70 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -603,11 +603,18 @@ class NormalizedOptional implements NormalizedNodeBase {
603603
)
604604
}
605605

606-
public decrementMax() {
606+
public decrementMax(dec = 1): NormalizedOptional | null {
607+
if (this.max <= dec) {
608+
return null
609+
}
607610
if (this.max === Infinity) {
608611
return this
609612
}
610-
const opt = new NormalizedOptional(this.node, this.flags, this.max - 1)
613+
const opt = new NormalizedOptional(
614+
this.node,
615+
this.flags,
616+
this.max - dec,
617+
)
611618
opt.normalizedElement = this.normalizedElement
612619
return opt
613620
}
@@ -776,82 +783,93 @@ function isCoveredAnyNode(
776783
/** Check whether the right nodes is covered by the left nodes. */
777784
function isCoveredAltNodes(
778785
/* eslint-enable complexity -- X( */
779-
left: NormalizedNode[],
780-
right: NormalizedNode[],
786+
leftNodes: NormalizedNode[],
787+
rightNodes: NormalizedNode[],
781788
options: Options,
782-
) {
783-
let rightLength = right.length
784-
if (options.canOmitRight) {
785-
while (right[rightLength - 1]) {
786-
const re = right[rightLength - 1]
787-
if (re.type === "NormalizedOptional") {
788-
rightLength--
789-
} else {
790-
break
791-
}
792-
}
793-
}
794-
let leftIndex = 0
795-
let rightIndex = 0
796-
while (leftIndex < left.length && rightIndex < rightLength) {
797-
const le = left[leftIndex]
798-
const re = right[rightIndex]
789+
): boolean {
790+
const left = options.canOmitRight ? omitEnds(leftNodes) : leftNodes
791+
const right = options.canOmitRight ? omitEnds(rightNodes) : rightNodes
792+
while (left.length && right.length) {
793+
const le = left.shift()!
794+
const re = right.shift()!
799795

800796
if (re.type === "NormalizedOptional") {
801-
let leftElement
802797
if (le.type === "NormalizedOptional") {
803-
leftElement = le.element
798+
// Check for elements
799+
if (
800+
!isCoveredForNormalizedNode(le.element, re.element, options)
801+
) {
802+
return false
803+
}
804+
// Check for next
805+
const decrementLe = le.decrementMax(re.max)
806+
if (decrementLe) {
807+
return isCoveredAltNodes(
808+
[decrementLe, ...left],
809+
right,
810+
options,
811+
)
812+
}
813+
const decrementRe = re.decrementMax(le.max)
814+
if (decrementRe) {
815+
return isCoveredAltNodes(
816+
left,
817+
[decrementRe, ...right],
818+
options,
819+
)
820+
}
804821
} else {
805-
leftElement = le
806-
}
807-
if (!isCoveredForNormalizedNode(leftElement, re.element, options)) {
808-
return false
822+
// Check for elements
823+
if (!isCoveredForNormalizedNode(le, re.element, options)) {
824+
return false
825+
}
826+
const decrementRe = re.decrementMax()
827+
if (decrementRe) {
828+
// Check for multiple iterations.
829+
return isCoveredAltNodes(
830+
left,
831+
[decrementRe, ...right],
832+
options,
833+
)
834+
}
809835
}
810836
} else if (le.type === "NormalizedOptional") {
811837
// Checks if skipped.
812-
const skippedLeftItems = left.slice(leftIndex + 1)
813-
if (
814-
isCoveredAltNodes(
815-
skippedLeftItems,
816-
right.slice(rightIndex),
817-
options,
818-
)
819-
) {
838+
if (isCoveredAltNodes(left, [re, ...right], options)) {
820839
return true
821840
}
822841
if (!isCoveredForNormalizedNode(le.element, re, options)) {
823842
// I know it won't match if I skip it.
824843
return false
825844
}
826-
if (le.max >= 2) {
845+
const decrementLe = le.decrementMax()
846+
if (decrementLe) {
827847
// Check for multiple iterations.
828-
if (
829-
isCoveredAltNodes(
830-
[le.decrementMax(), ...skippedLeftItems],
831-
right.slice(rightIndex + 1),
832-
options,
833-
)
834-
) {
848+
if (isCoveredAltNodes([decrementLe, ...left], right, options)) {
835849
return true
836850
}
837851
}
838852
} else if (!isCoveredForNormalizedNode(le, re, options)) {
839853
return false
840854
}
841-
leftIndex++
842-
rightIndex++
843855
}
844856
if (!options.canOmitRight) {
845-
if (rightIndex < right.length) {
857+
if (right.length) {
846858
return false
847859
}
848860
}
849-
while (leftIndex < left.length) {
850-
const le = left[leftIndex]
851-
if (le.type !== "NormalizedOptional") {
852-
return false
861+
return !left.length
862+
}
863+
864+
/**
865+
* Exclude the end optionals.
866+
*/
867+
function omitEnds(nodes: NormalizedNode[]): NormalizedNode[] {
868+
for (let index = nodes.length - 1; index >= 0; index--) {
869+
const node = nodes[index]
870+
if (node.type !== "NormalizedOptional") {
871+
return nodes.slice(0, index + 1)
853872
}
854-
leftIndex++
855873
}
856-
return leftIndex >= left.length
874+
return []
857875
}

tests/lib/utils/regexp-ast.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,11 @@ describe("regexp-ast isEqualNodes", () => {
235235
})
236236

237237
const TESTCASES_FOR_COVERED_NODE: TestCase[] = [
238+
{
239+
a: /a?b?cd?e?f?/,
240+
b: /a*b?c_/,
241+
result: false,
242+
},
238243
{
239244
a: /a+a+/,
240245
b: /aa/,
@@ -495,6 +500,16 @@ const TESTCASES_FOR_COVERED_NODE: TestCase[] = [
495500
b: /gooooood/,
496501
result: true,
497502
},
503+
{
504+
a: /a?b?cd?e?f?/,
505+
b: /a?b?c_/,
506+
result: true,
507+
},
508+
{
509+
a: /a?b?cd?e?f?/,
510+
b: /a*b?c_/,
511+
result: false,
512+
},
498513
]
499514
describe("regexp-ast isCoveredNode", () => {
500515
for (const testCase of [

0 commit comments

Comments
 (0)