Tests: Added test for zero-width lookbehinds (#2220)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
diff --git a/tests/pattern-tests.js b/tests/pattern-tests.js
index a4d1ac2..beb33c7 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}`);
+ }
+ });
});
});