@@ -147,6 +147,31 @@ function testPatterns(Prism) {
147
147
} ) ;
148
148
}
149
149
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
+
150
175
151
176
it ( '- should not match the empty string' , function ( ) {
152
177
forEachPattern ( ( { pattern, tokenPath } ) => {
@@ -168,32 +193,7 @@ function testPatterns(Prism) {
168
193
} ) ;
169
194
} ) ;
170
195
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 ( ) {
197
197
/**
198
198
* Returns whether the given element will always match the start of the string.
199
199
*
@@ -205,7 +205,7 @@ function testPatterns(Prism) {
205
205
switch ( parent . type ) {
206
206
case 'Alternative' :
207
207
// 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 ) ) {
209
209
return false ;
210
210
}
211
211
const grandParent = parent . parent ;
@@ -216,7 +216,7 @@ function testPatterns(Prism) {
216
216
}
217
217
218
218
case 'Quantifier' :
219
- if ( parent . max === null /* null == open ended */ || parent . max >= 2 ) {
219
+ if ( parent . max >= 2 ) {
220
220
return false ;
221
221
} else {
222
222
return isFirstMatch ( parent ) ;
@@ -228,13 +228,32 @@ function testPatterns(Prism) {
228
228
}
229
229
230
230
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 ;
237
233
}
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
+ } ) ;
238
257
} ) ;
239
258
} ) ;
240
259
0 commit comments