Skip to content

Commit 7d03ece

Browse files
Tests: Added test for zero-width lookbehinds (#2220)
1 parent 8d2c5a3 commit 7d03ece

File tree

1 file changed

+53
-34
lines changed

1 file changed

+53
-34
lines changed

tests/pattern-tests.js

+53-34
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,31 @@ function testPatterns(Prism) {
147147
});
148148
}
149149

150+
/**
151+
* Returns whether the given element will always have zero width meaning that it doesn't consume characters.
152+
*
153+
* @param {Element} element
154+
* @returns {boolean}
155+
*/
156+
function isAlwaysZeroWidth(element) {
157+
switch (element.type) {
158+
case 'Assertion':
159+
// assertions == ^, $, \b, lookarounds
160+
return true;
161+
case 'Quantifier':
162+
return element.max === 0 || isAlwaysZeroWidth(element.element);
163+
case 'CapturingGroup':
164+
case 'Group':
165+
// every element in every alternative has to be of zero length
166+
return element.alternatives.every(alt => alt.elements.every(isAlwaysZeroWidth));
167+
case 'Backreference':
168+
// on if the group referred to is of zero length
169+
return isAlwaysZeroWidth(element.resolved);
170+
default:
171+
return false; // what's left are characters
172+
}
173+
}
174+
150175

151176
it('- should not match the empty string', function () {
152177
forEachPattern(({ pattern, tokenPath }) => {
@@ -168,32 +193,7 @@ function testPatterns(Prism) {
168193
});
169194
});
170195

171-
it('- should not have lookbehind groups which can be preceded by other some characters', function () {
172-
/**
173-
* Returns whether the given element will have zero length meaning that it doesn't extend the matched string.
174-
*
175-
* @param {Element} element
176-
* @returns {boolean}
177-
*/
178-
function isZeroLength(element) {
179-
switch (element.type) {
180-
case 'Assertion':
181-
// assertions == ^, $, \b, lookarounds
182-
return true;
183-
case 'Quantifier':
184-
return element.max === 0 || isZeroLength(element.element);
185-
case 'CapturingGroup':
186-
case 'Group':
187-
// every element in every alternative has to be of zero length
188-
return element.alternatives.every(alt => alt.elements.every(isZeroLength));
189-
case 'Backreference':
190-
// on if the group referred to is of zero length
191-
return isZeroLength(element.resolved);
192-
default:
193-
return false; // what's left are characters
194-
}
195-
}
196-
196+
it('- should not have lookbehind groups that can be preceded by other some characters', function () {
197197
/**
198198
* Returns whether the given element will always match the start of the string.
199199
*
@@ -205,7 +205,7 @@ function testPatterns(Prism) {
205205
switch (parent.type) {
206206
case 'Alternative':
207207
// all elements before this element have to of zero length
208-
if (!parent.elements.slice(0, parent.elements.indexOf(element)).every(isZeroLength)) {
208+
if (!parent.elements.slice(0, parent.elements.indexOf(element)).every(isAlwaysZeroWidth)) {
209209
return false;
210210
}
211211
const grandParent = parent.parent;
@@ -216,7 +216,7 @@ function testPatterns(Prism) {
216216
}
217217

218218
case 'Quantifier':
219-
if (parent.max === null /* null == open ended */ || parent.max >= 2) {
219+
if (parent.max >= 2) {
220220
return false;
221221
} else {
222222
return isFirstMatch(parent);
@@ -228,13 +228,32 @@ function testPatterns(Prism) {
228228
}
229229

230230
forEachPattern(({ ast, tokenPath, lookbehind }) => {
231-
if (lookbehind) {
232-
forEachCapturingGroup(ast.pattern, ({ group, number }) => {
233-
if (number === 1 && !isFirstMatch(group)) {
234-
assert.fail(`Token ${tokenPath}: The lookbehind group (if matched at all) always has to be at index 0 relative to the whole match.`);
235-
}
236-
});
231+
if (!lookbehind) {
232+
return;
237233
}
234+
forEachCapturingGroup(ast.pattern, ({ group, number }) => {
235+
if (number === 1 && !isFirstMatch(group)) {
236+
assert.fail(`Token ${tokenPath}: `
237+
+ `The lookbehind group (if matched) always has to be at index 0 relative to the whole match.`);
238+
}
239+
});
240+
});
241+
});
242+
243+
it('- should not have lookbehind groups that only have zero-width alternatives', function () {
244+
forEachPattern(({ ast, tokenPath, lookbehind, reportError }) => {
245+
if (!lookbehind) {
246+
return;
247+
}
248+
forEachCapturingGroup(ast.pattern, ({ group, number }) => {
249+
if (number === 1 && isAlwaysZeroWidth(group)) {
250+
const groupContent = group.raw.substr(1, group.raw.length - 2);
251+
const replacement = group.alternatives.length === 1 ? groupContent : `(?:${groupContent})`;
252+
reportError(`Token ${tokenPath}: The lookbehind group ${group.raw} does not consume characters. `
253+
+ `Therefor it is not necessary to use a lookbehind group. `
254+
+ `Replacing the lookbehind group with: ${replacement}`);
255+
}
256+
});
238257
});
239258
});
240259

0 commit comments

Comments
 (0)