Skip to content

Commit 061e510

Browse files
committed
fix(sort-regexp): skip sorting shadowing alternatives
1 parent ed08b95 commit 061e510

File tree

2 files changed

+124
-122
lines changed

2 files changed

+124
-122
lines changed

rules/sort-regexp.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,14 @@ export default createEslintRule<Options, MessageId>({
188188
return
189189
}
190190

191+
if (
192+
hasShadowingAlternatives({
193+
alternatives: alternative.parent.alternatives,
194+
})
195+
) {
196+
return
197+
}
198+
191199
let nodes = alternative.parent.alternatives.map(currentAlternative =>
192200
createSortingNode({
193201
alternative: currentAlternative,
@@ -564,6 +572,28 @@ function getCharacterClassElementValue(
564572
return rawValue
565573
}
566574

575+
function hasShadowingAlternatives({
576+
alternatives,
577+
}: {
578+
alternatives: Alternative[]
579+
}): boolean {
580+
let rawAlternatives = alternatives.map(alternative => alternative.raw)
581+
582+
for (let index = 0; index < rawAlternatives.length; index++) {
583+
let current = rawAlternatives[index]!
584+
585+
for (let offset = index + 1; offset < rawAlternatives.length; offset++) {
586+
let other = rawAlternatives[offset]!
587+
588+
if (doesAlternativeShadowOther(current, other)) {
589+
return true
590+
}
591+
}
592+
}
593+
594+
return false
595+
}
596+
567597
function createFlagSortingNodes({
568598
eslintDisabledLines,
569599
literalNode,
@@ -617,6 +647,22 @@ function getAlternativeAlias(alternative: Alternative): string | null {
617647
return null
618648
}
619649

650+
function doesAlternativeShadowOther(first: string, second: string): boolean {
651+
if (first.length === 0 || second.length === 0) {
652+
return true
653+
}
654+
655+
if (first.length === second.length) {
656+
return first === second
657+
}
658+
659+
if (first.length < second.length) {
660+
return second.startsWith(first)
661+
}
662+
663+
return first.startsWith(second)
664+
}
665+
620666
function getCharacterClassElementSortKey(
621667
element: CharacterClass['elements'][number],
622668
): CharacterClassElementSortKey {

test/rules/sort-regexp.test.ts

Lines changed: 78 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -558,17 +558,8 @@ describe('sort-regexp', () => {
558558
})
559559
})
560560

561-
it('sorts alternatives with numbers', async () => {
562-
await invalid({
563-
errors: [
564-
{
565-
messageId: 'unexpectedRegExpOrder',
566-
data: { right: '1', left: '20' },
567-
},
568-
],
569-
output: dedent`
570-
/(1|10|2|20|3)/
571-
`,
561+
it('skips sorting alternatives with shadowed numbers', async () => {
562+
await valid({
572563
code: dedent`
573564
/(20|1|10|2|3)/
574565
`,
@@ -598,17 +589,8 @@ describe('sort-regexp', () => {
598589
})
599590
})
600591

601-
it('handles empty alternatives correctly', async () => {
602-
await invalid({
603-
errors: [
604-
{
605-
messageId: 'unexpectedRegExpOrder',
606-
data: { right: '', left: 'b' },
607-
},
608-
],
609-
output: dedent`
610-
/(|a|b)/
611-
`,
592+
it('skips sorting when empty alternative can shadow others', async () => {
593+
await valid({
612594
code: dedent`
613595
/(b||a)/
614596
`,
@@ -701,32 +683,6 @@ describe('sort-regexp', () => {
701683
})
702684
})
703685

704-
it('sorts alternatives with different lengths', async () => {
705-
await invalid({
706-
errors: [
707-
{
708-
messageId: 'unexpectedRegExpOrder',
709-
data: { left: 'zzz', right: 'aa' },
710-
},
711-
{
712-
messageId: 'unexpectedRegExpOrder',
713-
data: { right: 'aaa', left: 'z' },
714-
},
715-
{
716-
messageId: 'unexpectedRegExpOrder',
717-
data: { left: 'aaa', right: 'a' },
718-
},
719-
],
720-
output: dedent`
721-
/(a|aa|aaa|z|zz|zzz)/
722-
`,
723-
code: dedent`
724-
/(zzz|aa|z|aaa|a|zz)/
725-
`,
726-
options: [options],
727-
})
728-
})
729-
730686
it('sorts alternatives with lookahead assertions', async () => {
731687
await invalid({
732688
errors: [
@@ -1203,6 +1159,28 @@ describe('sort-regexp', () => {
12031159
`,
12041160
})
12051161
})
1162+
1163+
it('does not sort alternatives when one is a prefix of another', async () => {
1164+
await valid({
1165+
code: dedent`
1166+
/(ab|a)/
1167+
`,
1168+
options: [options],
1169+
})
1170+
})
1171+
1172+
it('sorts alternatives with common prefix but no prefix relationship', async () => {
1173+
await invalid({
1174+
errors: [{ messageId: 'unexpectedRegExpOrder' }],
1175+
output: dedent`
1176+
/(abc|abd)/
1177+
`,
1178+
code: dedent`
1179+
/(abd|abc)/
1180+
`,
1181+
options: [options],
1182+
})
1183+
})
12061184
})
12071185

12081186
describe('natural', () => {
@@ -1769,21 +1747,8 @@ describe('sort-regexp', () => {
17691747
})
17701748
})
17711749

1772-
it('sorts alternatives with numbers', async () => {
1773-
await invalid({
1774-
errors: [
1775-
{
1776-
messageId: 'unexpectedRegExpOrder',
1777-
data: { right: '1', left: '20' },
1778-
},
1779-
{
1780-
messageId: 'unexpectedRegExpOrder',
1781-
data: { right: '2', left: '10' },
1782-
},
1783-
],
1784-
output: dedent`
1785-
/(1|2|3|10|20)/
1786-
`,
1750+
it('skips sorting alternatives with shadowed numbers', async () => {
1751+
await valid({
17871752
code: dedent`
17881753
/(20|1|10|2|3)/
17891754
`,
@@ -1809,17 +1774,8 @@ describe('sort-regexp', () => {
18091774
})
18101775
})
18111776

1812-
it('handles empty alternatives correctly', async () => {
1813-
await invalid({
1814-
errors: [
1815-
{
1816-
messageId: 'unexpectedRegExpOrder',
1817-
data: { right: '', left: 'b' },
1818-
},
1819-
],
1820-
output: dedent`
1821-
/(|a|b)/
1822-
`,
1777+
it('skips sorting when empty alternative can shadow others', async () => {
1778+
await valid({
18231779
code: dedent`
18241780
/(b||a)/
18251781
`,
@@ -1908,32 +1864,6 @@ describe('sort-regexp', () => {
19081864
})
19091865
})
19101866

1911-
it('sorts alternatives with different lengths', async () => {
1912-
await invalid({
1913-
errors: [
1914-
{
1915-
messageId: 'unexpectedRegExpOrder',
1916-
data: { left: 'zzz', right: 'aa' },
1917-
},
1918-
{
1919-
messageId: 'unexpectedRegExpOrder',
1920-
data: { right: 'aaa', left: 'z' },
1921-
},
1922-
{
1923-
messageId: 'unexpectedRegExpOrder',
1924-
data: { left: 'aaa', right: 'a' },
1925-
},
1926-
],
1927-
output: dedent`
1928-
/(a|aa|aaa|z|zz|zzz)/
1929-
`,
1930-
code: dedent`
1931-
/(zzz|aa|z|aaa|a|zz)/
1932-
`,
1933-
options: [options],
1934-
})
1935-
})
1936-
19371867
it('sorts alternatives with lookahead assertions', async () => {
19381868
await invalid({
19391869
errors: [
@@ -2406,6 +2336,28 @@ describe('sort-regexp', () => {
24062336
`,
24072337
})
24082338
})
2339+
2340+
it('does not sort alternatives when one is a prefix of another', async () => {
2341+
await valid({
2342+
code: dedent`
2343+
/(ab|a)/
2344+
`,
2345+
options: [options],
2346+
})
2347+
})
2348+
2349+
it('sorts alternatives with common prefix but no prefix relationship', async () => {
2350+
await invalid({
2351+
errors: [{ messageId: 'unexpectedRegExpOrder' }],
2352+
output: dedent`
2353+
/(abc|abd)/
2354+
`,
2355+
code: dedent`
2356+
/(abd|abc)/
2357+
`,
2358+
options: [options],
2359+
})
2360+
})
24092361
})
24102362

24112363
describe('line-length', () => {
@@ -2569,17 +2521,8 @@ describe('sort-regexp', () => {
25692521
})
25702522
})
25712523

2572-
it('sorts alternatives with numbers', async () => {
2573-
await invalid({
2574-
errors: [
2575-
{
2576-
messageId: 'unexpectedRegExpOrder',
2577-
data: { right: '10', left: '1' },
2578-
},
2579-
],
2580-
output: dedent`
2581-
/(20|10|1|2|3)/
2582-
`,
2524+
it('skips sorting alternatives with shadowed numbers', async () => {
2525+
await valid({
25832526
code: dedent`
25842527
/(20|1|10|2|3)/
25852528
`,
@@ -2605,17 +2548,8 @@ describe('sort-regexp', () => {
26052548
})
26062549
})
26072550

2608-
it('handles empty alternatives correctly', async () => {
2609-
await invalid({
2610-
errors: [
2611-
{
2612-
messageId: 'unexpectedRegExpOrder',
2613-
data: { right: 'a', left: '' },
2614-
},
2615-
],
2616-
output: dedent`
2617-
/(b|a|)/
2618-
`,
2551+
it('skips sorting when empty alternative can shadow others', async () => {
2552+
await valid({
26192553
code: dedent`
26202554
/(b||a)/
26212555
`,
@@ -2749,6 +2683,28 @@ describe('sort-regexp', () => {
27492683
`,
27502684
})
27512685
})
2686+
2687+
it('does not sort alternatives when one is a prefix of another', async () => {
2688+
await valid({
2689+
code: dedent`
2690+
/(a|ab)/
2691+
`,
2692+
options: [options],
2693+
})
2694+
})
2695+
2696+
it('sorts alternatives with common prefix but no prefix relationship', async () => {
2697+
await invalid({
2698+
errors: [{ messageId: 'unexpectedRegExpOrder' }],
2699+
output: dedent`
2700+
/(abcd|abd)/
2701+
`,
2702+
code: dedent`
2703+
/(abd|abcd)/
2704+
`,
2705+
options: [options],
2706+
})
2707+
})
27522708
})
27532709

27542710
describe('custom', () => {

0 commit comments

Comments
 (0)