Skip to content

Commit a7c73b9

Browse files
committed
Tests
1 parent 36023a8 commit a7c73b9

File tree

2 files changed

+75
-25
lines changed

2 files changed

+75
-25
lines changed

spec/recursion-spec.js

Lines changed: 64 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,70 @@ import {regex} from 'regex';
22
import {recursion} from '../src/index.js';
33

44
describe('recursion', () => {
5+
it('should throw for invalid and unsupported recursion depths', () => {
6+
const values = ['-2', '0', '1', '+2', '2.5', '101', 'a', null];
7+
for (const value of values) {
8+
expect(() => regex({plugins: [recursion]})({raw: [`a(?R=${value})?b`]})).toThrow();
9+
expect(() => regex({plugins: [recursion]})({raw: [`(?<r>a\\g<r&R=${value}>?b)`]})).toThrow();
10+
}
11+
});
12+
13+
it('should allow recursion depths 2-100', () => {
14+
const values = ['2', '100'];
15+
for (const value of values) {
16+
expect(() => regex({plugins: [recursion]})({raw: [`a(?R=${value})?b`]})).not.toThrow();
17+
expect(() => regex({plugins: [recursion]})({raw: [`(?<r>a\\g<r&R=${value}>?b)`]})).not.toThrow();
18+
}
19+
});
20+
21+
it('should match global recursion', () => {
22+
expect(regex({plugins: [recursion]})`a(?R=2)?b`.exec('aabb')?.[0]).toBe('aabb');
23+
});
24+
25+
it('should match direct recursion', () => {
26+
expect('aabb').toMatch(regex({plugins: [recursion]})`^(?<r>a\g<r&R=2>?b)$`);
27+
expect('aab').not.toMatch(regex({plugins: [recursion]})`^(?<r>a\g<r&R=2>?b)$`);
28+
});
29+
30+
it('should throw for multiple direct, overlapping recursions', () => {
31+
expect(() => regex({plugins: [recursion]})`a(?R=2)?(?<r>a\g<r&R=2>?)`).toThrow();
32+
expect(() => regex({plugins: [recursion]})`(?<r>a\g<r&R=2>?\g<r&R=2>?)`).toThrow();
33+
});
34+
35+
it('should throw for multiple direct, nonoverlapping recursions', () => {
36+
// TODO: Has a bug and lets invalid JS syntax through
37+
expect(() => regex({plugins: [recursion]})`(?<r1>a\g<r1&R=2>?)(?<r2>a\g<r2&R=2>?)`).toThrow();
38+
});
39+
40+
it('should throw for indirect recursion', () => {
41+
expect(() => regex({plugins: [recursion]})`(?<a>(?<b>a\g<a&R=2>?)\g<b&R=2>)`).toThrow();
42+
expect(() => regex({plugins: [recursion]})`(?<a>\g<b&R=2>(?<b>a\g<a&R=2>?))`).toThrow();
43+
expect(() => regex({plugins: [recursion]})`(?<a>\g<b&R=2>)(?<b>a\g<a&R=2>?)`).toThrow();
44+
expect(() => regex({plugins: [recursion]})`\g<a&R=2>(?<a>\g<b&R=2>)(?<b>a\g<a&R=2>?)`).toThrow();
45+
expect(() => regex({plugins: [recursion]})`(?<a>\g<b&R=2>)(?<b>\g<c&R=2>)(?<c>a\g<a&R=2>?)`).toThrow();
46+
});
47+
48+
it('should not adjust named backreferences referring outside of the recursed expression', () => {
49+
expect('aababbabcc').toMatch(regex({plugins: [recursion]})`^(?<a>a)\k<a>(?<r>(?<b>b)\k<a>\k<b>\k<c>\g<r&R=2>?)(?<c>c)\k<c>$`);
50+
});
51+
52+
// Just documenting current behavior; this could be supported in the future
53+
it('should throw for numbered backreferences in interpolated regexes when using recursion', () => {
54+
expect(() => regex({plugins: [recursion]})`a(?R=2)?b${/()\1/}`).toThrow();
55+
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>${/()\1/})`).toThrow();
56+
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>)${/()\1/}`).toThrow();
57+
expect(() => regex({plugins: [recursion]})`${/()\1/}a(?R=2)?b`).toThrow();
58+
expect(() => regex({plugins: [recursion]})`(?<n>${/()\1/}a|\g<n&R=2>)`).toThrow();
59+
expect(() => regex({plugins: [recursion]})`${/()\1/}(?<n>a|\g<n&R=2>)`).toThrow();
60+
});
61+
62+
it('should throw for subroutine definition groups when using recursion', () => {
63+
expect(() => regex({plugins: [recursion]})`a(?R=2)?b(?(DEFINE))`).toThrow();
64+
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>)(?(DEFINE))`).toThrow();
65+
});
66+
});
67+
68+
describe('readme examples', () => {
569
it('should match an equal number of two different subpatterns', () => {
670
expect(regex({plugins: [recursion]})`a(?R=50)?b`.exec('test aaaaaabbb')[0]).toBe('aaabbb');
771
expect('aAbb').toMatch(regex({flags: 'i', plugins: [recursion]})`a(?R=2)?b`);
@@ -50,23 +114,4 @@ describe('recursion', () => {
50114
\b`;
51115
expect('Racecar, ABBA, and redivided'.match(palindromeWords)).toEqual(['Racecar', 'ABBA']);
52116
});
53-
54-
it('should not adjust named backreferences referring outside of the recursed expression', () => {
55-
expect('aababbabcc').toMatch(regex({plugins: [recursion]})`^(?<a>a)\k<a>(?<r>(?<b>b)\k<a>\k<b>\k<c>\g<r&R=2>?)(?<c>c)\k<c>$`);
56-
});
57-
58-
// Just documenting current behavior; this could be supported in the future
59-
it('should not allow numbered backreferences in interpolated regexes when using recursion', () => {
60-
expect(() => regex({plugins: [recursion]})`a(?R=2)?b${/()\1/}`).toThrow();
61-
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>${/()\1/})`).toThrow();
62-
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>)${/()\1/}`).toThrow();
63-
expect(() => regex({plugins: [recursion]})`${/()\1/}a(?R=2)?b`).toThrow();
64-
expect(() => regex({plugins: [recursion]})`(?<n>${/()\1/}a|\g<n&R=2>)`).toThrow();
65-
expect(() => regex({plugins: [recursion]})`${/()\1/}(?<n>a|\g<n&R=2>)`).toThrow();
66-
});
67-
68-
it('should not allow subroutine definition groups when using recursion', () => {
69-
expect(() => regex({plugins: [recursion]})`a(?R=2)?b(?(DEFINE))`).toThrow();
70-
expect(() => regex({plugins: [recursion]})`(?<n>a|\g<n&R=2>)(?(DEFINE))`).toThrow();
71-
});
72117
});

src/index.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Context, forEachUnescaped, getGroupContents, hasUnescaped, replaceUnescaped} from 'regex-utilities';
22

3-
const gRToken = String.raw`\\g<(?<gRName>[^>&]+)&R=(?<gRDepth>\d+)>`;
4-
const recursiveToken = String.raw`\(\?R=(?<rDepth>\d+)\)|${gRToken}`;
3+
const gRToken = String.raw`\\g<(?<gRName>[^>&]+)&R=(?<gRDepth>[^>]+)>`;
4+
const recursiveToken = String.raw`\(\?R=(?<rDepth>[^\)]+)\)|${gRToken}`;
55
const namedCapturingDelim = String.raw`\(\?<(?![=!])(?<captureName>[^>]+)>`;
66
const token = new RegExp(String.raw`${namedCapturingDelim}|${recursiveToken}|\\?.`, 'gsu');
77

@@ -45,16 +45,16 @@ export function recursion(expression) {
4545
groupContentsStartPos.set(captureName, token.lastIndex);
4646
// (?R=N)
4747
} else if (rDepth) {
48+
assertMaxInBounds(rDepth);
4849
const maxDepth = +rDepth;
49-
assertMaxInBounds(maxDepth);
5050
const pre = expression.slice(0, match.index);
5151
const post = expression.slice(token.lastIndex);
5252
assertNoFollowingRecursion(post);
5353
return makeRecursive(pre, post, maxDepth, false);
5454
// \g<name&R=N>
5555
} else if (gRName) {
56+
assertMaxInBounds(gRDepth);
5657
const maxDepth = +gRDepth;
57-
assertMaxInBounds(maxDepth);
5858
const outsideOwnGroupMsg = `Recursion via \\g<${gRName}&R=${gRDepth}> must be used within the referenced group`;
5959
// Appears before/outside the referenced group
6060
if (!groupContentsStartPos.has(gRName)) {
@@ -82,11 +82,16 @@ export function recursion(expression) {
8282
}
8383

8484
/**
85-
@param {number} max
85+
@param {string} max
8686
*/
8787
function assertMaxInBounds(max) {
88+
const errMsg = `Max depth must be integer between 2 and 100; used ${max}`;
89+
if (!/^[1-9]\d*$/.test(max)) {
90+
throw new Error(errMsg);
91+
}
92+
max = +max;
8893
if (max < 2 || max > 100) {
89-
throw new Error(`Max depth must be between 2 and 100; used ${max}`);
94+
throw new Error(errMsg);
9095
}
9196
}
9297

0 commit comments

Comments
 (0)