From c9e0715b8b87fe9949d57059b276cd4b1903d4ff Mon Sep 17 00:00:00 2001 From: Michael Schmidt Date: Tue, 10 Mar 2020 15:12:08 +0100 Subject: [PATCH] Tests: Added test for zero-width lookbehinds (#2220) --- tests/pattern-tests.js | 87 +++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/tests/pattern-tests.js b/tests/pattern-tests.js index a4d1ac2712..beb33c7b11 100644 --- a/tests/pattern-tests.js +++ b/tests/pattern-tests.js @@ -147,6 +147,31 @@ function testPatterns(Prism) { }); } + /** + * Returns whether the given element will always have zero width meaning that it doesn't consume characters. + * + * @param {Element} element + * @returns {boolean} + */ + function isAlwaysZeroWidth(element) { + switch (element.type) { + case 'Assertion': + // assertions == ^, $, \b, lookarounds + return true; + case 'Quantifier': + return element.max === 0 || isAlwaysZeroWidth(element.element); + case 'CapturingGroup': + case 'Group': + // every element in every alternative has to be of zero length + return element.alternatives.every(alt => alt.elements.every(isAlwaysZeroWidth)); + case 'Backreference': + // on if the group referred to is of zero length + return isAlwaysZeroWidth(element.resolved); + default: + return false; // what's left are characters + } + } + it('- should not match the empty string', function () { forEachPattern(({ pattern, tokenPath }) => { @@ -168,32 +193,7 @@ function testPatterns(Prism) { }); }); - it('- should not have lookbehind groups which can be preceded by other some characters', function () { - /** - * Returns whether the given element will have zero length meaning that it doesn't extend the matched string. - * - * @param {Element} element - * @returns {boolean} - */ - function isZeroLength(element) { - switch (element.type) { - case 'Assertion': - // assertions == ^, $, \b, lookarounds - return true; - case 'Quantifier': - return element.max === 0 || isZeroLength(element.element); - case 'CapturingGroup': - case 'Group': - // every element in every alternative has to be of zero length - return element.alternatives.every(alt => alt.elements.every(isZeroLength)); - case 'Backreference': - // on if the group referred to is of zero length - return isZeroLength(element.resolved); - default: - return false; // what's left are characters - } - } - + it('- should not have lookbehind groups that can be preceded by other some characters', function () { /** * Returns whether the given element will always match the start of the string. * @@ -205,7 +205,7 @@ function testPatterns(Prism) { switch (parent.type) { case 'Alternative': // all elements before this element have to of zero length - if (!parent.elements.slice(0, parent.elements.indexOf(element)).every(isZeroLength)) { + if (!parent.elements.slice(0, parent.elements.indexOf(element)).every(isAlwaysZeroWidth)) { return false; } const grandParent = parent.parent; @@ -216,7 +216,7 @@ function testPatterns(Prism) { } case 'Quantifier': - if (parent.max === null /* null == open ended */ || parent.max >= 2) { + if (parent.max >= 2) { return false; } else { return isFirstMatch(parent); @@ -228,13 +228,32 @@ function testPatterns(Prism) { } forEachPattern(({ ast, tokenPath, lookbehind }) => { - if (lookbehind) { - forEachCapturingGroup(ast.pattern, ({ group, number }) => { - if (number === 1 && !isFirstMatch(group)) { - assert.fail(`Token ${tokenPath}: The lookbehind group (if matched at all) always has to be at index 0 relative to the whole match.`); - } - }); + if (!lookbehind) { + return; } + forEachCapturingGroup(ast.pattern, ({ group, number }) => { + if (number === 1 && !isFirstMatch(group)) { + assert.fail(`Token ${tokenPath}: ` + + `The lookbehind group (if matched) always has to be at index 0 relative to the whole match.`); + } + }); + }); + }); + + it('- should not have lookbehind groups that only have zero-width alternatives', function () { + forEachPattern(({ ast, tokenPath, lookbehind, reportError }) => { + if (!lookbehind) { + return; + } + forEachCapturingGroup(ast.pattern, ({ group, number }) => { + if (number === 1 && isAlwaysZeroWidth(group)) { + const groupContent = group.raw.substr(1, group.raw.length - 2); + const replacement = group.alternatives.length === 1 ? groupContent : `(?:${groupContent})`; + reportError(`Token ${tokenPath}: The lookbehind group ${group.raw} does not consume characters. ` + + `Therefor it is not necessary to use a lookbehind group. ` + + `Replacing the lookbehind group with: ${replacement}`); + } + }); }); });