diff --git a/package-lock.json b/package-lock.json index 06dd964..875388c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.7.1", "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.8.0", + "@eslint-community/regexpp": "^4.11.0", "refa": "^0.12.1" }, "devDependencies": { @@ -590,9 +590,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -6471,9 +6471,9 @@ } }, "@eslint-community/regexpp": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.0.tgz", - "integrity": "sha512-JylOEEzDiOryeUnFbQz+oViCXS0KsvR1mvHkoMiu5+UiBvy+RYX7tzlIIIEstF/gVa2tj9AQXk3dgnxv6KxhFg==" + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==" }, "@eslint/eslintrc": { "version": "2.0.2", diff --git a/package.json b/package.json index e900342..facb039 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "typescript": "5.0" }, "dependencies": { - "@eslint-community/regexpp": "^4.8.0", + "@eslint-community/regexpp": "^4.11.0", "refa": "^0.12.1" }, "files": [ diff --git a/src/basic.ts b/src/basic.ts index 9da8d87..abeecd1 100644 --- a/src/basic.ts +++ b/src/basic.ts @@ -392,11 +392,15 @@ function backreferenceIsPotentiallyEmpty( ): boolean { if (isEmptyBackreference(back, flags)) { return true; - } else if (hasSomeAncestor(back.resolved, a => a === root)) { - return !isStrictBackreference(back) || isPotentiallyZeroLengthImpl(back.resolved, root, flags); - } else { - return false; } + const groups = getReferencedGroupsFromBackreference(back); + if (groups.length === 0) return true; + for (const group of groups.filter(group => hasSomeAncestor(group, a => a === root))) { + if (!isStrictBackreference(back) || isPotentiallyZeroLengthImpl(group, root, flags)) { + return true; + } + } + return false; } /** @@ -749,19 +753,8 @@ export function getMatchingDirectionFromAssertionKind( * - The backreference might be before the capturing group. E.g. `/\1(a)/`, `/(?:\1(a))+/`, `/(?<=(a)\1)b/` */ export function isEmptyBackreference(backreference: Backreference, flags: ReadonlyFlags): boolean { - const group = backreference.resolved; - - const closestAncestor = getClosestAncestor(backreference, group); - - if (closestAncestor === group) { - // if the backreference is element of the referenced group - return true; - } - - if (closestAncestor.type !== "Alternative") { - // if the closest common ancestor isn't an alternative => they're disjunctive. - return true; - } + const groups = getReferencedGroupsFromBackreference(backreference); + if (groups.length === 0) return true; const backRefAncestors = new Set(); for (let a: Node | null = backreference; a; a = a.parent) { @@ -812,7 +805,7 @@ export function isEmptyBackreference(backreference: Backreference, flags: Readon } } - return !findBackreference(group) || isZeroLength(group, flags); + return groups.every(group => !findBackreference(group) || isZeroLength(group, flags)); } /** @@ -840,19 +833,8 @@ export function isEmptyBackreference(backreference: Backreference, flags: Readon * - `/(?!(a)).\1/` */ export function isStrictBackreference(backreference: Backreference): boolean { - const group = backreference.resolved; - - const closestAncestor = getClosestAncestor(backreference, group); - - if (closestAncestor === group) { - // if the backreference is element of the referenced group - return false; - } - - if (closestAncestor.type !== "Alternative") { - // if the closest common ancestor isn't an alternative => they're disjunctive. - return false; - } + const groups = getReferencedGroupsFromBackreference(backreference); + if (groups.length === 0) return false; const backRefAncestors = new Set(); for (let a: Node | null = backreference; a; a = a.parent) { @@ -890,7 +872,15 @@ export function isStrictBackreference(backreference: Backreference): boolean { // The captured text of a capturing group will be reset after leaving a negated lookaround return false; } else { - if (parentParent.alternatives.length > 1) { + if ( + parentParent.alternatives.length > 1 && + parentParent.alternatives.some( + alternative => + !hasSomeDescendant(alternative, node => { + return node.type === "CapturingGroup" && groups.includes(node); + }) + ) + ) { // e.g.: (?:a|(a))+b\1 return false; } @@ -907,7 +897,7 @@ export function isStrictBackreference(backreference: Backreference): boolean { } } - return findBackreference(group); + return groups.every(findBackreference); } /** @@ -1086,15 +1076,17 @@ function getLengthRangeElementImpl( return getLengthRangeAlternativesImpl(element.alternatives, flags); case "Backreference": { - if (isEmptyBackreference(element, flags)) { + const groups = getReferencedGroupsFromBackreference(element); + if (groups.length === 0) { + return ZERO_LENGTH_RANGE; + } else if (isEmptyBackreference(element, flags)) { return ZERO_LENGTH_RANGE; } else { - const resolvedRange = getLengthRangeElementImpl(element.resolved, flags); - if (resolvedRange.min > 0 && !isStrictBackreference(element)) { - return { min: 0, max: resolvedRange.max }; - } else { - return resolvedRange; - } + const resolvedRanges = groups.map(group => getLengthRangeElementImpl(group, flags)); + return { + min: isStrictBackreference(element) ? Math.min(...resolvedRanges.map(r => r.min)) : 0, + max: Math.max(...resolvedRanges.map(r => r.max)), + }; } } @@ -1189,11 +1181,11 @@ function isLengthRangeMinZeroElementImpl( return isLengthRangeMinZeroAlternativesImpl(element.alternatives, flags); case "Backreference": { - return ( - isEmptyBackreference(element, flags) || - !isStrictBackreference(element) || - isLengthRangeMinZeroElementImpl(element.resolved, flags) - ); + if (isEmptyBackreference(element, flags) || !isStrictBackreference(element)) { + return true; + } + const groups = getReferencedGroupsFromBackreference(element); + return groups.every(group => isLengthRangeMinZeroElementImpl(group, flags)); } default: @@ -1315,3 +1307,30 @@ export function getEffectiveMaximumRepetition(element: Node): number { } return max; } + +/** + * Returns the actually referenced capturing group from the given backreference. + * + * Actual referenced capturing group of a backreference is a capturing group that exists in the same alternative + * as the backreference and that does not have a backreference within it capturing group. + * + * ## Examples + * + * - `/(a)\1/`: This will return (a) + * - `/(a)(?:\1)/`: This will return (a) + * - `/(a)|\1/`: This will return empty + * - `/(a\1)/`: This will return empty + * - `/(?:(?a)|(?b))\k/`: This will return (?a) and (?b) + * - `/(?:(?a)|(?b)\k)/`: This will return (?b) + */ +export function getReferencedGroupsFromBackreference(back: Backreference): CapturingGroup[] { + return (back.ambiguous ? back.resolved : [back.resolved]).filter(group => { + const closestAncestor = getClosestAncestor(back, group); + return ( + // A backreference cannot refer to the referenced group if it is element of the referenced group. + closestAncestor !== group && + // If the closest common ancestor is an alternative, then they're not disjunctive. + closestAncestor.type === "Alternative" + ); + }); +} diff --git a/src/consumed-chars.ts b/src/consumed-chars.ts index 9def343..2392631 100644 --- a/src/consumed-chars.ts +++ b/src/consumed-chars.ts @@ -1,7 +1,7 @@ import { Alternative, Element, Pattern } from "@eslint-community/regexpp/ast"; import { CharSet } from "refa"; import { ReadonlyFlags } from "./flags"; -import { hasSomeDescendant, isEmptyBackreference } from "./basic"; +import { getReferencedGroupsFromBackreference, hasSomeDescendant, isEmptyBackreference } from "./basic"; import { Chars } from "./chars"; import { toUnicodeSet } from "./to-char-set"; @@ -49,9 +49,11 @@ export function getConsumedChars(element: Element | Pattern | Alternative, flags exact = exact && !c.isEmpty; } else if (d.type === "Backreference" && !isEmptyBackreference(d, flags)) { - const c = getConsumedChars(d.resolved, flags); - sets.push(c.chars); - exact = exact && c.exact && c.chars.size < 2; + for (const resolved of getReferencedGroupsFromBackreference(d)) { + const c = getConsumedChars(resolved, flags); + sets.push(c.chars); + exact = exact && c.exact && c.chars.size < 2; + } } // always continue to the next element diff --git a/src/equal.ts b/src/equal.ts index 0d85ea8..5455627 100644 --- a/src/equal.ts +++ b/src/equal.ts @@ -63,10 +63,22 @@ export function structurallyEqual(x: Node | null, y: Node | null): boolean { case "Backreference": { const other = y as Backreference; - return ( - structurallyEqual(x.resolved, other.resolved) && - isStrictBackreference(x) == isStrictBackreference(other) - ); + const groupsX = x.ambiguous ? x.resolved : [x.resolved]; + const groupsY = other.ambiguous ? other.resolved : [other.resolved]; + /** + * Keep any groups of `y` that did not match anything. + * If there are any groups remaining after searching the groups of `x`, they do not match. + */ + const unusedGroupsY = new Set(groupsY); + for (const groupX of groupsX) { + const matches = groupsY.filter(groupY => structurallyEqual(groupX, groupY)); + if (matches.length === 0) return false; + for (const groupY of matches) { + unusedGroupsY.delete(groupY); + } + } + if (unusedGroupsY.size > 0) return false; + return isStrictBackreference(x) == isStrictBackreference(other); } case "Character": { diff --git a/src/follow.ts b/src/follow.ts index b83c847..657908c 100644 --- a/src/follow.ts +++ b/src/follow.ts @@ -339,7 +339,6 @@ export function followPaths( parent.type === "CharacterClassRange" || parent.type === "ClassIntersection" || parent.type === "ClassSubtraction" || - parent.type === "ExpressionCharacterClass" || parent.type === "StringAlternative" ) { throw new Error("The given element cannot be part of a character class."); diff --git a/src/longest-prefix.ts b/src/longest-prefix.ts index 8a36475..a1f19d8 100644 --- a/src/longest-prefix.ts +++ b/src/longest-prefix.ts @@ -1,6 +1,7 @@ import { CharSet } from "refa"; import { Alternative, CapturingGroup, Element, Group, Quantifier } from "@eslint-community/regexpp/ast"; import { + getReferencedGroupsFromBackreference, isEmptyBackreference, isLengthRangeMinZero, isStrictBackreference, @@ -219,8 +220,12 @@ function getElementPrefix( return EMPTY_COMPLETE; } if (isStrictBackreference(element)) { - const inner = getElementPrefix(element.resolved, direction, { ...options, includeAfter: false }, flags); - return inner; + const groups = getReferencedGroupsFromBackreference(element); + const prefixes = groups.map(resolved => + getElementPrefix(resolved, direction, { ...options, includeAfter: false }, flags) + ); + + return getAlternationPrefix(element, prefixes, direction, options, flags); } if (!mayLookAhead(element, options, direction, flags)) { @@ -459,7 +464,6 @@ function isNextCharacterInsideAfter( parent.type === "CharacterClassRange" || parent.type === "ClassIntersection" || parent.type === "ClassSubtraction" || - parent.type === "ExpressionCharacterClass" || parent.type === "StringAlternative" ) { throw new Error("Expected an element outside a character class."); diff --git a/src/next-char.ts b/src/next-char.ts index 18a1f65..3082362 100644 --- a/src/next-char.ts +++ b/src/next-char.ts @@ -8,6 +8,7 @@ import { isEmptyBackreference, MatchingDirection, invertMatchingDirection, + getReferencedGroupsFromBackreference, } from "./basic"; import { toUnicodeSet } from "./to-char-set"; import { followPaths } from "./follow"; @@ -718,21 +719,26 @@ function getFirstConsumedCharUncachedImpl( if (isEmptyBackreference(element, flags)) { return FirstConsumedChars.emptyConcat(flags); } - let resolvedChar = getFirstConsumedCharImpl(element.resolved, direction, flags, options); + const groups = getReferencedGroupsFromBackreference(element); - // the resolved character is only exact if it is only a single character. - // i.e. /(\w)\1/ here the (\w) will capture exactly any word character, but the \1 can only match - // one word character and that is the only (\w) matched. - if (resolvedChar.exact && resolvedChar.char.size > 1) { - resolvedChar = { ...resolvedChar, exact: false }; - } + const resolvedChars = groups.map(group => { + let resolvedChar = getFirstConsumedCharImpl(group, direction, flags, options); - if (isStrictBackreference(element)) { + // the resolved character is only exact if it is only a single character. + // i.e. /(\w)\1/ here the (\w) will capture exactly any word character, but the \1 can only match + // one word character and that is the only (\w) matched. + if (resolvedChar.exact && resolvedChar.char.size > 1) { + resolvedChar = { ...resolvedChar, exact: false }; + } return resolvedChar; + }); + + if (isStrictBackreference(element)) { + return FirstConsumedChars.union(resolvedChars, flags); } else { // there is at least one path through which the backreference will (possibly) be replaced with the // empty string - return FirstConsumedChars.makeOptional(resolvedChar); + return FirstConsumedChars.makeOptional(FirstConsumedChars.union(resolvedChars, flags)); } } diff --git a/tests/__snapshots__/consumed-chars.ts.snap b/tests/__snapshots__/consumed-chars.ts.snap index b883432..d81af77 100644 --- a/tests/__snapshots__/consumed-chars.ts.snap +++ b/tests/__snapshots__/consumed-chars.ts.snap @@ -1,5 +1,19 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`getConsumedChars /(?:(?a)|(?b))\\k/ 1`] = ` +Object { + "chars": CharSet (65535) [61..62], + "exact": true, +} +`; + +exports[`getConsumedChars /(?:(?abc)|(?x))\\k/ 1`] = ` +Object { + "chars": CharSet (65535) [61..63, 78], + "exact": false, +} +`; + exports[`getConsumedChars /(?:\\d*\\.\\d+|\\d+\\.\\d*)_/ 1`] = ` Object { "chars": CharSet (65535) [2e, 30..39, 5f], diff --git a/tests/__snapshots__/next-char.ts.snap b/tests/__snapshots__/next-char.ts.snap index 2299640..1ad51af 100644 --- a/tests/__snapshots__/next-char.ts.snap +++ b/tests/__snapshots__/next-char.ts.snap @@ -147,6 +147,14 @@ Object { } `; +exports[`getFirstConsumedChar /(?:(?a)|(?b))\\k/ (ltr) 1`] = ` +Object { + "char": CharSet (65535) [61..62], + "empty": false, + "exact": true, +} +`; + exports[`getFirstConsumedChar /(?:(a)|b)\\1/ (rtl) 1`] = ` Object { "char": CharSet (65535) [61..62], diff --git a/tests/__snapshots__/reorder.ts.snap b/tests/__snapshots__/reorder.ts.snap index cc993c2..e1ff30d 100644 --- a/tests/__snapshots__/reorder.ts.snap +++ b/tests/__snapshots__/reorder.ts.snap @@ -1,5 +1,35 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`canReorder /(?:(?a)|(?b))\\k/ 1`] = ` +Object { + "(?:(?a)|(?b))": Object { + "(?a)|(?b)": Object { + "dir:ltr ignoreCG:false": false, + "dir:ltr ignoreCG:true ": true, + "dir:rtl ignoreCG:false": false, + "dir:rtl ignoreCG:true ": true, + "dir:unknown ignoreCG:false": false, + "dir:unknown ignoreCG:true ": true, + }, + }, +} +`; + +exports[`canReorder /(?:(?abc)|(?x))\\k/ 1`] = ` +Object { + "(?:(?abc)|(?x))": Object { + "(?abc)|(?x)": Object { + "dir:ltr ignoreCG:false": false, + "dir:ltr ignoreCG:true ": true, + "dir:rtl ignoreCG:false": false, + "dir:rtl ignoreCG:true ": true, + "dir:unknown ignoreCG:false": false, + "dir:unknown ignoreCG:true ": true, + }, + }, +} +`; + exports[`canReorder /(?:\\d*\\.\\d+|\\d+\\.\\d*)_/ 1`] = ` Object { "(?:\\\\d*\\\\.\\\\d+|\\\\d+\\\\.\\\\d*)": Object { diff --git a/tests/basic.ts b/tests/basic.ts index 9d954cc..2657c08 100644 --- a/tests/basic.ts +++ b/tests/basic.ts @@ -6,7 +6,7 @@ import * as RAA from "../src"; import { assert } from "chai"; describe(RAA.isStrictBackreference.name, function () { - function test(expected: boolean, regexps: RegExp[]): void { + function test(expected: boolean, regexps: (RegExp | string)[]): void { describe(`${expected}`, function () { regexps .map(r => new RegExpParser().parseLiteral(r.toString())) @@ -21,7 +21,16 @@ describe(RAA.isStrictBackreference.name, function () { }); } - test(true, [/(a)\1/, /(a)(?:b|\1)/, /(a)\1?/, /(?<=\1(a))b/, /(?!(a)\1)/]); + test(true, [ + /(a)\1/, + /(a)(?:b|\1)/, + /(a)\1?/, + /(?<=\1(a))b/, + /(?!(a)\1)/, + /(?:(?a))\k/, + String.raw`/(?:(?a)|(?b))\k/`, + String.raw`/(?:(?a)|(?b)\k)/`, + ]); test(false, [ /(a)|\1/, /(a\1)/, @@ -31,11 +40,13 @@ describe(RAA.isStrictBackreference.name, function () { /(?=\1(a))/, /(?!(a))\w\1/, /(?!(?!(a)))\w\1/, + String.raw`/(?:(?a)||(?b))\k/`, + String.raw`/(?:(?a)?|(?b))\k/`, ]); }); describe(RAA.isEmptyBackreference.name, function () { - function test(expected: boolean, regexps: RegExp[]): void { + function test(expected: boolean, regexps: (RegExp | string)[]): void { describe(`${expected}`, function () { regexps .map(r => new RegExpParser().parseLiteral(r.toString())) @@ -60,8 +71,21 @@ describe(RAA.isEmptyBackreference.name, function () { /(?!(a))\w\1/, /(?!(?!(a)))\w\1/, /(\3)(\1)(\2)/, + /(?:(?))\k/, + String.raw`/(?:(?)|(?))\k/`, + String.raw`/(?:(?)\k|(?b))/`, + ]); + test(false, [ + /(?:(a)|b)\1/, + /(a)?\1/, + /(a)\1/, + /(?=(a))\w\1/, + /(?!(a)\1)/, + /(?:(?a))\k/, + String.raw`/(?:(?a)|(?))\k/`, + String.raw`/(?:(?)|(?b))\k/`, + String.raw`/(?:(?)|(?b)\k)/`, ]); - test(false, [/(?:(a)|b)\1/, /(a)?\1/, /(a)\1/, /(?=(a))\w\1/, /(?!(a)\1)/]); }); describe(RAA.getCapturingGroupNumber.name, function () { @@ -244,3 +268,33 @@ describe("hasSomeAncestor and hasSomeDescendant condition", function () { return nodes; } }); + +describe(RAA.getReferencedGroupsFromBackreference.name, function () { + interface TestCase { + regexp: RegExp | string; + expected?: string[]; + } + + test([ + { regexp: /(a)\1/, expected: ["(a)"] }, + { regexp: /(a)(?:\1)/, expected: ["(a)"] }, + { regexp: /(a)|\1/, expected: [] }, + { regexp: /(a\1)/, expected: [] }, + { regexp: String.raw`/(?:(?a)|(?b))\k/`, expected: ["(?a)", "(?b)"] }, + { regexp: String.raw`/(?:(?a)|(?b)\k)/`, expected: ["(?b)"] }, + ]); + + function test(cases: TestCase[]): void { + for (const { regexp, expected } of cases) { + it(`${regexp}`, function () { + const { pattern } = new RegExpParser().parseLiteral(regexp.toString()); + const [ref] = select(pattern, (e): e is Backreference => e.type === "Backreference"); + const actual = RAA.getReferencedGroupsFromBackreference(ref); + assert.deepEqual( + actual.map(a => a.raw), + expected + ); + }); + } + } +}); diff --git a/tests/equal.ts b/tests/equal.ts index b1a8f54..ff18c6c 100644 --- a/tests/equal.ts +++ b/tests/equal.ts @@ -4,8 +4,8 @@ import { assert } from "chai"; describe(RAA.structurallyEqual.name, function () { interface TestCase { - a: RegExp; - b: RegExp; + a: RegExp | string; + b: RegExp | string; expected?: boolean; } @@ -47,6 +47,32 @@ describe(RAA.structurallyEqual.name, function () { { a: /(?=a)/, b: /(?=a)/ }, { a: /(?=a)/, b: /(?!a)/, expected: false }, { a: /(?=a)/, b: /(?<=a)/, expected: false }, + + { + a: String.raw`/(?:(?a)|(?b))\k/`, + b: String.raw`/(?:(?a)|(?b))\k/`, + expected: true, + }, + { + a: String.raw`/(?:(?a)|(?b))\k/`, + b: String.raw`/(?:(?b)|(?a))\k/`, + expected: false, + }, + { + a: String.raw`/(?:(?a)|(?b))\k/`, + b: String.raw`/(?:(?a)|(?b))\1/`, + expected: false, + }, + { + a: String.raw`/(?:(?a)|(?a))\k/`, + b: String.raw`/(?:(?a)|(a))\1/`, + expected: false, + }, + { + a: String.raw`/(?:(?a)|(?a))\k/`, + b: String.raw`/(?:(?a)|(?a))\1/`, + expected: false, + }, ]); function test(cases: TestCase[]): void { diff --git a/tests/helper/data.ts b/tests/helper/data.ts index 469e312..0d1aac1 100644 --- a/tests/helper/data.ts +++ b/tests/helper/data.ts @@ -51,4 +51,6 @@ export const TEST_REGEXES: readonly (RegExp | string)[] = [ String.raw`/foo|[\q{bar}]/v`, String.raw`/abc|\p{Basic_Emoji}/v`, /(abc)(?:foo|bar|\1)/, + String.raw`/(?:(?a)|(?b))\k/`, + String.raw`/(?:(?abc)|(?x))\k/`, ]; diff --git a/tests/length.ts b/tests/length.ts index 946eb21..42d0d4c 100644 --- a/tests/length.ts +++ b/tests/length.ts @@ -298,6 +298,9 @@ describe(RAA.getLengthRange.name, function () { { regexp: /(?:(a)|b)\1/, expected: { min: 1, max: 2 } }, { regexp: /(a*)(?\1)/, expected: { min: 0, max: Infinity }, selectNamed: true }, + { regexp: String.raw`/(?:(?x)|(?abc))\k/`, expected: { min: 2, max: 6 } }, + { regexp: String.raw`/(?:(?x)|(?abc)\k)/`, expected: { min: 1, max: 6 } }, + // Limitations: // "All characters classes/sets are assumed to consume at least one characters and all assertions are assumed // to have some accepting path." diff --git a/tests/next-char.ts b/tests/next-char.ts index e626800..2016a24 100644 --- a/tests/next-char.ts +++ b/tests/next-char.ts @@ -17,7 +17,7 @@ function* iter(array: T | T[]): IterableIterator { describe(RAA.getFirstConsumedChar.name, function () { interface TestCase { - regexp: RegExp | RegExp[]; + regexp: RegExp | RegExp[] | string; direction?: MatchingDirection; } @@ -45,6 +45,8 @@ describe(RAA.getFirstConsumedChar.name, function () { { regexp: /\1(a)/ }, { regexp: /\1a|a(b)/ }, + { regexp: String.raw`/(?:(?a)|(?b))\k/` }, + // assertions { regexp: /^/ },