Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test: Added test for zero-width lookbehinds #2220

Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 53 additions & 34 deletions tests/pattern-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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.
*
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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}`);
}
});
});
});

Expand Down