From b03049a5fe5310ccc56bb8211f435ebee3fcc9ca Mon Sep 17 00:00:00 2001 From: Balaji Rao Date: Mon, 24 Feb 2025 18:05:39 +0100 Subject: [PATCH] NativeRegExp: Implement lookbehind assertions --- .../javascript/regexp/NativeRegExp.java | 359 +++++++++++++++--- .../javascript/tests/NativeRegExpTest.java | 97 +++++ .../javascript/tests/Test262SuiteTest.java | 1 - tests/testsrc/test262.properties | 3 +- 4 files changed, 400 insertions(+), 60 deletions(-) diff --git a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java index 1bc8399f25..de7c36892c 100644 --- a/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java +++ b/rhino/src/main/java/org/mozilla/javascript/regexp/NativeRegExp.java @@ -112,7 +112,14 @@ public class NativeRegExp extends IdScriptableObject { private static final byte REOP_ALTPREREQi = 54; /* case-independent REOP_ALTPREREQ */ private static final byte REOP_ALTPREREQ2 = 55; /* prerequisite for ALT, a char or a class */ // private static final byte REOP_ENDALT = 56; /* end of final alternate */ - private static final byte REOP_END = 57; + + private static final byte REOP_ASSERTBACK = 57; /* zero width positive lookbehind assertion */ + private static final byte REOP_ASSERTBACK_NOT = + 58; /* zero width negative lookbehind assertion */ + private static final byte REOP_ASSERTBACKTEST = 59; /* sentinel at end of assertion child */ + private static final byte REOP_ASSERTBACKNOTTEST = 60; /* sentinel at end of !assertion child */ + + private static final byte REOP_END = 61; private static final int ANCHOR_BOL = -2; @@ -395,7 +402,6 @@ private static void prettyPrintRE(RECompiled regexp) { case REOP_MINIMALQUANT: case REOP_QUANT: { - String quantType; boolean greedy; int min, max; @@ -472,12 +478,28 @@ private static void prettyPrintRE(RECompiled regexp) { System.out.println("ASSERT_NOT: " + assertNotNextPc); pc += INDEX_LEN; break; + case REOP_ASSERTBACK: + int assertBackNextPc = pc + getIndex(regexp.program, pc); + System.out.println("ASSERTBACK: " + assertBackNextPc); + pc += INDEX_LEN; + break; + case REOP_ASSERTBACK_NOT: + int assertBackNotNextPc = pc + getIndex(regexp.program, pc); + System.out.println("ASSERTBACK_NOT: " + assertBackNotNextPc); + pc += INDEX_LEN; + break; case REOP_ASSERTTEST: System.out.println("ASSERTTEST"); break; case REOP_ASSERTNOTTEST: System.out.println("ASSERTNOTTEST"); break; + case REOP_ASSERTBACKTEST: + System.out.println("ASSERTBACKTEST"); + break; + case REOP_ASSERTBACKNOTTEST: + System.out.println("ASSERTBACKNOTTEST"); + break; case REOP_ENDCHILD: System.out.println("ENDCHILD"); break; @@ -1007,6 +1029,26 @@ private static int getDecimalValue(char c, CompilerState state, String overflowM return value; } + private static RENode reverseNodeList(RENode head) { + RENode prev = null; + RENode node = head; + while (node != null) { + /* Don't reverse lookahead assertions. Lookbehind assertions should already have been reversed */ + if (node.kid != null + && node.op != REOP_ASSERT + && node.op != REOP_ASSERT_NOT + && node.op != REOP_ASSERTBACK + && node.op != REOP_ASSERTBACK_NOT) { + node.kid = reverseNodeList(node.kid); + } + RENode next = node.next; + node.next = prev; + prev = node; + node = next; + } + return prev; + } + private static boolean parseTerm(CompilerState state) { char[] src = state.cpbegin; char c = src[state.cp++]; @@ -1219,6 +1261,20 @@ private static boolean parseTerm(CompilerState state) { /* ASSERTNOT, , ... ASSERTNOTTEST */ state.progLength += 4; } + } else if (state.cp + 2 < state.cpend + && src[state.cp] == '?' + && src[state.cp + 1] == '<' + && ((c = src[state.cp + 2]) == '=' || c == '!')) { + state.cp += 3; + if (c == '=') { + result = new RENode(REOP_ASSERTBACK); + /* ASSERT, , ... ASSERTBACKTEST */ + state.progLength += 4; + } else { // c == '!' + result = new RENode(REOP_ASSERTBACK_NOT); + /* ASSERTNOT, , ... ASSERTBACKNOTTEST */ + state.progLength += 4; + } } else { result = new RENode(REOP_LPAREN); /* LPAREN, , ... RPAREN, */ @@ -1234,6 +1290,10 @@ private static boolean parseTerm(CompilerState state) { ++state.cp; --state.parenNesting; if (result != null) { + /* if we have a lookbehind then we reverse state.result linked list */ + if (result.op == REOP_ASSERTBACK || result.op == REOP_ASSERTBACK_NOT) { + state.result = reverseNodeList(state.result); + } result.kid = state.result; state.result = result; } @@ -1372,6 +1432,11 @@ private static boolean parseTerm(CompilerState state) { } if (!hasQ) return true; + if (term.op == REOP_ASSERTBACK || term.op == REOP_ASSERTBACK_NOT) { + reportError("msg.bad.quant", ""); + return false; + } + ++state.cp; state.result.kid = term; state.result.parenIndex = parenBaseCount; @@ -1483,17 +1548,20 @@ private static int emitREBytecode(CompilerState state, RECompiled re, int pc, RE pc = addIndex(program, pc, t.parenIndex); break; case REOP_ASSERT: + case REOP_ASSERTBACK: nextTermFixup = pc; pc += INDEX_LEN; pc = emitREBytecode(state, re, pc, t.kid); - program[pc++] = REOP_ASSERTTEST; + program[pc++] = t.op == REOP_ASSERT ? REOP_ASSERTTEST : REOP_ASSERTBACKTEST; resolveForwardJump(program, nextTermFixup, pc); break; case REOP_ASSERT_NOT: + case REOP_ASSERTBACK_NOT: nextTermFixup = pc; pc += INDEX_LEN; pc = emitREBytecode(state, re, pc, t.kid); - program[pc++] = REOP_ASSERTNOTTEST; + program[pc++] = + t.op == REOP_ASSERT_NOT ? REOP_ASSERTNOTTEST : REOP_ASSERTBACKNOTTEST; resolveForwardJump(program, nextTermFixup, pc); break; case REOP_QUANT: @@ -1538,6 +1606,7 @@ private static void pushProgState( int min, int max, int cp, + boolean matchBackward, REBackTrackData backTrackLastToSave, int continuationOp, int continuationPc) { @@ -1548,6 +1617,7 @@ private static void pushProgState( max, cp, backTrackLastToSave, + matchBackward, continuationOp, continuationPc); } @@ -1586,6 +1656,22 @@ private static boolean flatNMatcher( return true; } + private static boolean flatNMatcherBackward( + REGlobalData gData, int matchChars, int length, String input) { + if ((gData.cp - length) < 0) return false; + + // in the input, start from cp - 1 and go back length chars + // in the regex source, do it the other way + for (int i = 1; i <= length; i++) { + if (gData.regexp.source[matchChars + length - i] != input.charAt(gData.cp - i)) { + return false; + } + } + + gData.cp -= length; + return true; + } + private static boolean flatNIMatcher( REGlobalData gData, int matchChars, int length, String input, int end) { if ((gData.cp + length) > end) return false; @@ -1601,6 +1687,24 @@ private static boolean flatNIMatcher( return true; } + private static boolean flatNIMatcherBackward( + REGlobalData gData, int matchChars, int length, String input) { + if ((gData.cp - length) < 0) return false; + + // in the input, start from cp - 1 and go back length chars + // in the regex source, do it the other way + for (int i = 1; i <= length; i++) { + char c1 = gData.regexp.source[matchChars + length - i]; + char c2 = input.charAt(gData.cp - i); + if (c1 != c2 && upcase(c1) != upcase(c2)) { + return false; + } + } + + gData.cp -= length; + return true; + } + /* 1. Evaluate DecimalEscape to obtain an EscapeValue E. 2. If E is not a character then go to step 6. @@ -1625,7 +1729,7 @@ such that Canonicalize(s[i]) is not the same character as 10. Call c(y) and return its result. */ private static boolean backrefMatcher( - REGlobalData gData, int parenIndex, String input, int end) { + REGlobalData gData, int parenIndex, String input, int end, boolean matchBackward) { int len; int i; if (gData.parens == null || parenIndex >= gData.parens.length) return false; @@ -1633,18 +1737,37 @@ private static boolean backrefMatcher( if (parenContent == -1) return true; len = gData.parensLength(parenIndex); - if ((gData.cp + len) > end) return false; - if ((gData.regexp.flags & JSREG_FOLD) != 0) { - for (i = 0; i < len; i++) { - char c1 = input.charAt(parenContent + i); - char c2 = input.charAt(gData.cp + i); - if (c1 != c2 && upcase(c1) != upcase(c2)) return false; + // The capture is always "forward", i.e., in + // the input order + if (matchBackward) { + if ((gData.cp - len) < 0) return false; + + if ((gData.regexp.flags & JSREG_FOLD) != 0) { + // start from (cp - len) on the left and go to cp - 1 on the right + for (i = 0; i < len; i++) { + char c1 = input.charAt(parenContent + i); + char c2 = input.charAt(gData.cp + i - len); + if (c1 != c2 && upcase(c1) != upcase(c2)) return false; + } + } else if (!input.regionMatches(parenContent, input, gData.cp - len, len)) { + return false; + } + gData.cp -= len; + } else { + if ((gData.cp + len) > end) return false; + + if ((gData.regexp.flags & JSREG_FOLD) != 0) { + for (i = 0; i < len; i++) { + char c1 = input.charAt(parenContent + i); + char c2 = input.charAt(gData.cp + i); + if (c1 != c2 && upcase(c1) != upcase(c2)) return false; + } + } else if (!input.regionMatches(parenContent, input, gData.cp, len)) { + return false; } - } else if (!input.regionMatches(parenContent, input, gData.cp, len)) { - return false; + gData.cp += len; } - gData.cp += len; return true; } @@ -1917,17 +2040,25 @@ private static int simpleMatch( byte[] program, int pc, int end, - boolean updatecp) { + boolean updatecp, + boolean matchBackward) { boolean result = false; char matchCh; int parenIndex; int offset, length, index; int startcp = gData.cp; + int cpDelta = matchBackward ? -1 : 1; + + final int cpToMatch = gData.cp + (matchBackward ? -1 : 0); + final boolean cpInBounds = cpToMatch >= 0 && cpToMatch < end; switch (op) { case REOP_EMPTY: result = true; break; + + // We just use gData.cp and not cpToMatch in the BOL, EOL, WBDRY, WNONBDRY cases + // since their behaviour is identical in both forward and backward matching case REOP_BOL: if (gData.cp != 0) { if (!gData.multiline || !isLineTerm(input.charAt(gData.cp - 1))) { @@ -1955,54 +2086,54 @@ private static int simpleMatch( ^ ((gData.cp < end) && isWord(input.charAt(gData.cp)))); break; case REOP_DOT: - if (gData.cp != end + if (cpInBounds && ((gData.regexp.flags & JSREG_DOTALL) != 0 - || !isLineTerm(input.charAt(gData.cp)))) { + || !isLineTerm(input.charAt(cpToMatch)))) { result = true; - gData.cp++; + gData.cp += cpDelta; } break; case REOP_DIGIT: - if (gData.cp != end && isDigit(input.charAt(gData.cp))) { + if (cpInBounds && isDigit(input.charAt(cpToMatch))) { result = true; - gData.cp++; + gData.cp += cpDelta; } break; case REOP_NONDIGIT: - if (gData.cp != end && !isDigit(input.charAt(gData.cp))) { + if (cpInBounds && !isDigit(input.charAt(cpToMatch))) { result = true; - gData.cp++; + gData.cp += cpDelta; } break; case REOP_ALNUM: - if (gData.cp != end && isWord(input.charAt(gData.cp))) { + if (cpInBounds && isWord(input.charAt(cpToMatch))) { result = true; - gData.cp++; + gData.cp += cpDelta; } break; case REOP_NONALNUM: - if (gData.cp != end && !isWord(input.charAt(gData.cp))) { + if (cpInBounds && !isWord(input.charAt(cpToMatch))) { result = true; - gData.cp++; + gData.cp += cpDelta; } break; case REOP_SPACE: - if (gData.cp != end && isREWhiteSpace(input.charAt(gData.cp))) { + if (cpInBounds && isREWhiteSpace(input.charAt(cpToMatch))) { result = true; - gData.cp++; + gData.cp += cpDelta; } break; case REOP_NONSPACE: - if (gData.cp != end && !isREWhiteSpace(input.charAt(gData.cp))) { + if (cpInBounds && !isREWhiteSpace(input.charAt(cpToMatch))) { result = true; - gData.cp++; + gData.cp += cpDelta; } break; case REOP_BACKREF: { parenIndex = getIndex(program, pc); pc += INDEX_LEN; - result = backrefMatcher(gData, parenIndex, input, end); + result = backrefMatcher(gData, parenIndex, input, end, matchBackward); } break; case REOP_FLAT: @@ -2011,15 +2142,17 @@ private static int simpleMatch( pc += INDEX_LEN; length = getIndex(program, pc); pc += INDEX_LEN; - result = flatNMatcher(gData, offset, length, input, end); + + if (matchBackward) result = flatNMatcherBackward(gData, offset, length, input); + else result = flatNMatcher(gData, offset, length, input, end); } break; case REOP_FLAT1: { matchCh = (char) (program[pc++] & 0xFF); - if (gData.cp != end && input.charAt(gData.cp) == matchCh) { + if (cpInBounds && input.charAt(cpToMatch) == matchCh) { result = true; - gData.cp++; + gData.cp += cpDelta; } } break; @@ -2029,17 +2162,19 @@ private static int simpleMatch( pc += INDEX_LEN; length = getIndex(program, pc); pc += INDEX_LEN; - result = flatNIMatcher(gData, offset, length, input, end); + + if (matchBackward) result = flatNIMatcherBackward(gData, offset, length, input); + else result = flatNIMatcher(gData, offset, length, input, end); } break; case REOP_FLAT1i: { matchCh = (char) (program[pc++] & 0xFF); - if (gData.cp != end) { - char c = input.charAt(gData.cp); + if (cpInBounds) { + char c = input.charAt(cpToMatch); if (matchCh == c || upcase(matchCh) == upcase(c)) { result = true; - gData.cp++; + gData.cp += cpDelta; } } } @@ -2048,9 +2183,9 @@ private static int simpleMatch( { matchCh = (char) getIndex(program, pc); pc += INDEX_LEN; - if (gData.cp != end && input.charAt(gData.cp) == matchCh) { + if (cpInBounds && input.charAt(cpToMatch) == matchCh) { result = true; - gData.cp++; + gData.cp += cpDelta; } } break; @@ -2058,11 +2193,11 @@ private static int simpleMatch( { matchCh = (char) getIndex(program, pc); pc += INDEX_LEN; - if (gData.cp != end) { - char c = input.charAt(gData.cp); + if (cpInBounds) { + char c = input.charAt(cpToMatch); if (matchCh == c || upcase(matchCh) == upcase(c)) { result = true; - gData.cp++; + gData.cp += cpDelta; } } } @@ -2073,10 +2208,10 @@ private static int simpleMatch( { index = getIndex(program, pc); pc += INDEX_LEN; - if (gData.cp != end) { + if (cpInBounds) { if (classMatcher( - gData, gData.regexp.classList[index], input.charAt(gData.cp))) { - gData.cp++; + gData, gData.regexp.classList[index], input.charAt(cpToMatch))) { + gData.cp += cpDelta; result = true; break; } @@ -2102,6 +2237,7 @@ private static boolean executeREBytecode( int continuationOp = REOP_END; int continuationPc = 0; boolean result = false; + boolean matchBackward = false; /* match forward by default */ int op = program[pc++]; @@ -2112,7 +2248,7 @@ private static boolean executeREBytecode( if (gData.regexp.anchorCh < 0 && reopIsSimple(op)) { boolean anchor = false; while (gData.cp <= end) { - int match = simpleMatch(gData, input, op, program, pc, end, true); + int match = simpleMatch(gData, input, op, program, pc, end, true, false); if (match < 0) { if ((gData.regexp.flags & JSREG_STICKY) != 0) { return false; @@ -2136,7 +2272,7 @@ private static boolean executeREBytecode( } if (reopIsSimple(op)) { - int match = simpleMatch(gData, input, op, program, pc, end, true); + int match = simpleMatch(gData, input, op, program, pc, end, true, matchBackward); result = match >= 0; if (result) pc = match; /* accept skip to next opcode */ } else { @@ -2151,11 +2287,14 @@ private static boolean executeREBytecode( char matchCh2 = (char) getIndex(program, pc); pc += INDEX_LEN; - if (gData.cp == end) { + final int cpToMatch = gData.cp + (matchBackward ? -1 : 0); + final boolean cpInBounds = cpToMatch >= 0 && cpToMatch < end; + + if (!cpInBounds) { result = false; break; } - char c = input.charAt(gData.cp); + char c = input.charAt(cpToMatch); if (op == REOP_ALTPREREQ2) { if (c != matchCh1 && !classMatcher( @@ -2180,7 +2319,16 @@ private static boolean executeREBytecode( op = program[pc++]; int startcp = gData.cp; if (reopIsSimple(op)) { - int match = simpleMatch(gData, input, op, program, pc, end, true); + int match = + simpleMatch( + gData, + input, + op, + program, + pc, + end, + true, + matchBackward); if (match < 0) { op = program[nextpc++]; pc = nextpc; @@ -2217,11 +2365,85 @@ private static boolean executeREBytecode( int parenIndex = getIndex(program, pc); pc += INDEX_LEN; int cap_index = gData.parensIndex(parenIndex); - gData.setParens(parenIndex, cap_index, gData.cp - cap_index); + if (matchBackward) + // paren content is captured backwards. Therefore we + // reverse the capture here + gData.setParens(parenIndex, gData.cp, cap_index - gData.cp); + else gData.setParens(parenIndex, cap_index, gData.cp - cap_index); op = program[pc++]; } continue; + case REOP_ASSERTBACK: + { + int nextpc = + pc + getIndex(program, pc); /* start of term after ASSERT */ + pc += INDEX_LEN; /* start of ASSERT child */ + op = program[pc++]; + + if (reopIsSimple(op) + && simpleMatch(gData, input, op, program, pc, end, false, true) + < 0) { + result = false; + break; + } + + pushProgState( + gData, + 0, + 0, + gData.cp, + matchBackward, + gData.backTrackStackTop, + continuationOp, + continuationPc); + + pushBackTrackState( + gData, + REOP_ASSERTBACKTEST, + nextpc, + gData.cp, + continuationOp, + continuationPc); + matchBackward = true; + } + continue; + case REOP_ASSERTBACK_NOT: + { + int nextpc = + pc + getIndex(program, pc); /* start of term after ASSERT */ + pc += INDEX_LEN; /* start of ASSERT child */ + op = program[pc++]; + + if (reopIsSimple(op)) { + int match = + simpleMatch( + gData, input, op, program, pc, end, false, true); + if (match >= 0 && program[match] == REOP_ASSERTBACKNOTTEST) { + result = false; + break; + } + } + + pushProgState( + gData, + 0, + 0, + gData.cp, + matchBackward, + gData.backTrackStackTop, + continuationOp, + continuationPc); + pushBackTrackState( + gData, + REOP_ASSERTBACKNOTTEST, + nextpc, + gData.cp, + continuationOp, + continuationPc); + matchBackward = true; + } + continue; case REOP_ASSERT: { int nextpc = @@ -2229,7 +2451,8 @@ private static boolean executeREBytecode( pc += INDEX_LEN; /* start of ASSERT child */ op = program[pc++]; if (reopIsSimple(op) - && simpleMatch(gData, input, op, program, pc, end, false) < 0) { + && simpleMatch(gData, input, op, program, pc, end, false, false) + < 0) { result = false; break; } @@ -2238,10 +2461,12 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { 0, 0, gData.cp, + matchBackward, gData.backTrackStackTop, continuationOp, continuationPc); pushBackTrackState(gData, REOP_ASSERTTEST, nextpc); + matchBackward = false; } continue; case REOP_ASSERT_NOT: @@ -2251,7 +2476,9 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { pc += INDEX_LEN; /* start of ASSERT child */ op = program[pc++]; if (reopIsSimple(op)) { - int match = simpleMatch(gData, input, op, program, pc, end, false); + int match = + simpleMatch( + gData, input, op, program, pc, end, false, false); if (match >= 0 && program[match] == REOP_ASSERTNOTTEST) { result = false; break; @@ -2262,22 +2489,27 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { 0, 0, gData.cp, + matchBackward, gData.backTrackStackTop, continuationOp, continuationPc); pushBackTrackState(gData, REOP_ASSERTNOTTEST, nextpc); + matchBackward = false; } continue; case REOP_ASSERTTEST: + case REOP_ASSERTBACKTEST: case REOP_ASSERTNOTTEST: + case REOP_ASSERTBACKNOTTEST: { REProgState state = popProgState(gData); gData.cp = state.index; gData.backTrackStackTop = state.backTrack; + matchBackward = state.matchBackward; continuationPc = state.continuationPc; continuationOp = state.continuationOp; - if (op == REOP_ASSERTNOTTEST) { + if (op == REOP_ASSERTNOTTEST || op == REOP_ASSERTBACKNOTTEST) { result = !result; } } @@ -2334,6 +2566,7 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { min, max, gData.cp, + matchBackward, null, continuationOp, continuationPc); @@ -2411,8 +2644,14 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { nextpc++; int match = simpleMatch( - gData, input, nextop, program, nextpc, end, - true); + gData, + input, + nextop, + program, + nextpc, + end, + true, + matchBackward); if (match < 0) { result = (new_min == 0); continuationPc = state.continuationPc; @@ -2431,6 +2670,7 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { new_min, new_max, startcp, + matchBackward, null, state.continuationOp, state.continuationPc); @@ -2468,6 +2708,7 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { state.min, state.max, gData.cp, + matchBackward, null, state.continuationOp, state.continuationPc); @@ -2503,6 +2744,7 @@ && simpleMatch(gData, input, op, program, pc, end, false) < 0) { new_min, new_max, gData.cp, + matchBackward, null, state.continuationOp, state.continuationPc); @@ -3260,6 +3502,7 @@ class REProgState { int max, int index, REBackTrackData backTrack, + boolean matchBackward, int continuationOp, int continuationPc) { this.previous = previous; @@ -3269,6 +3512,7 @@ class REProgState { this.continuationOp = continuationOp; this.continuationPc = continuationPc; this.backTrack = backTrack; + this.matchBackward = matchBackward; } final REProgState previous; // previous state in stack @@ -3279,6 +3523,7 @@ class REProgState { final int continuationOp; final int continuationPc; final REBackTrackData backTrack; // used by ASSERT_ to recover state + final boolean matchBackward; } class REBackTrackData { diff --git a/rhino/src/test/java/org/mozilla/javascript/tests/NativeRegExpTest.java b/rhino/src/test/java/org/mozilla/javascript/tests/NativeRegExpTest.java index 0fb4371aa2..a342e5be8c 100644 --- a/rhino/src/test/java/org/mozilla/javascript/tests/NativeRegExpTest.java +++ b/rhino/src/test/java/org/mozilla/javascript/tests/NativeRegExpTest.java @@ -553,4 +553,101 @@ public void objectToString() throws Exception { "TypeError: Method \"toString\" called on incompatible object", "var toString = RegExp.prototype.toString; try { toString(); } catch (e) { ('' + e).substr(0, 58) }"); } + + @Test + public void lookbehindPositive() throws Exception { + // matches numbers that are preceded by a dollar sign + final String script = + "var regex = /(?<=\\$)\\d+/g;\n" + + "var result = '$123 $456 789'.match(regex);\n" + + "var res = '' + result.length;\n" + + "res = res + '-' + result[0];\n" + + "res = res + '-' + result[1];\n" + + "res;"; + Utils.assertWithAllModes_ES6("2-123-456", script); + } + + @Test + public void lookbehindNegative() throws Exception { + // matches numbers that are not preceded by a dollar sign + final String script = + "var regex = /(?