1
1
import type { RegExpVisitor } from "regexpp/visitor"
2
- import type { CapturingGroup , Element } from "regexpp/ast"
2
+ import type {
3
+ CapturingGroup ,
4
+ Element ,
5
+ LookaroundAssertion ,
6
+ Pattern ,
7
+ } from "regexpp/ast"
3
8
import type { RegExpContext } from "../utils"
4
9
import { createRule , defineRegexpVisitor } from "../utils"
5
10
import { createTypeTracker } from "../utils/type-tracker"
@@ -11,7 +16,10 @@ import {
11
16
extractExpressionReferences ,
12
17
isKnownMethodCall ,
13
18
} from "../utils/ast-utils"
14
- import type { PatternReplaceRange } from "../utils/ast-utils/pattern-source"
19
+ import type {
20
+ PatternRange ,
21
+ PatternReplaceRange ,
22
+ } from "../utils/ast-utils/pattern-source"
15
23
import type { Expression , Literal } from "estree"
16
24
import type { Rule } from "eslint"
17
25
import { mention } from "../utils/mention"
@@ -172,6 +180,184 @@ function getSideEffectsWhenReplacingCapturingGroup(
172
180
}
173
181
}
174
182
183
+ /** Checks whether the given element is a capturing group of length 1 or greater. */
184
+ function isCapturingGroupAndNotZeroLength (
185
+ element : Element ,
186
+ ) : element is CapturingGroup {
187
+ return element . type === "CapturingGroup" && ! isZeroLength ( element )
188
+ }
189
+
190
+ type ParsedStartPattern = {
191
+ // A list of zero-length elements placed before the start capturing group.
192
+ // e.g.
193
+ // /^(foo)bar/ -> ^
194
+ // /\b(foo)bar/ -> \b
195
+ // /(?:^|\b)(foo)bar/ -> (?:^|\b)
196
+ // /(?<=f)(oo)bar/ -> (?<=f)
197
+ // /(foo)bar/ -> null
198
+ leadingElements : Element [ ]
199
+ // Capturing group used to replace the starting string.
200
+ capturingGroup : CapturingGroup
201
+ // The pattern used when replacing lookbehind assertions.
202
+ replacedAssertion : string
203
+ range : PatternRange
204
+ }
205
+ type ParsedEndPattern = {
206
+ // Capturing group used to replace the ending string.
207
+ capturingGroup : CapturingGroup
208
+ // A list of zero-length elements placed after the end capturing group.
209
+ // e.g.
210
+ // /foo(bar)$/ -> $
211
+ // /foo(bar)\b/ -> \b
212
+ // /foo(bar)(?:\b|$)/ -> (?:\b|$)
213
+ // /foo(ba)(?=r)/ -> (?=r)
214
+ // /foo(bar)/ -> null
215
+ trailingElements : Element [ ]
216
+ // The pattern used when replacing lookahead assertions.
217
+ replacedAssertion : string
218
+ range : PatternRange
219
+ }
220
+ type ParsedElements = {
221
+ // All elements
222
+ elements : readonly Element [ ]
223
+ start : ParsedStartPattern | null
224
+ end : ParsedEndPattern | null
225
+ }
226
+
227
+ /**
228
+ * Parse the elements of the pattern.
229
+ */
230
+ function parsePatternElements ( node : Pattern ) : ParsedElements | null {
231
+ if ( node . alternatives . length > 1 ) {
232
+ return null
233
+ }
234
+ const elements = node . alternatives [ 0 ] . elements
235
+ const leadingElements : Element [ ] = [ ]
236
+ let start : ParsedStartPattern | null = null
237
+
238
+ for ( const element of elements ) {
239
+ if ( isZeroLength ( element ) ) {
240
+ leadingElements . push ( element )
241
+ continue
242
+ }
243
+ if ( isCapturingGroupAndNotZeroLength ( element ) ) {
244
+ const capturingGroup = element
245
+ start = {
246
+ leadingElements,
247
+ capturingGroup,
248
+ replacedAssertion : startElementsToLookbehindAssertionText (
249
+ leadingElements ,
250
+ capturingGroup ,
251
+ ) ,
252
+ range : {
253
+ start : ( leadingElements [ 0 ] || capturingGroup ) . start ,
254
+ end : capturingGroup . end ,
255
+ } ,
256
+ }
257
+ }
258
+ break
259
+ }
260
+
261
+ let end : ParsedEndPattern | null = null
262
+ const trailingElements : Element [ ] = [ ]
263
+ for ( const element of [ ...elements ] . reverse ( ) ) {
264
+ if ( isZeroLength ( element ) ) {
265
+ trailingElements . unshift ( element )
266
+ continue
267
+ }
268
+
269
+ if ( isCapturingGroupAndNotZeroLength ( element ) ) {
270
+ const capturingGroup = element
271
+ end = {
272
+ capturingGroup,
273
+ trailingElements,
274
+ replacedAssertion : endElementsToLookaheadAssertionText (
275
+ capturingGroup ,
276
+ trailingElements ,
277
+ ) ,
278
+ range : {
279
+ start : capturingGroup . start ,
280
+ end : (
281
+ trailingElements [ trailingElements . length - 1 ] ||
282
+ capturingGroup
283
+ ) . end ,
284
+ } ,
285
+ }
286
+ }
287
+ break
288
+ }
289
+ if ( ! start && ! end ) {
290
+ // No capturing groups.
291
+ return null
292
+ }
293
+ if ( start && end && start . capturingGroup === end . capturingGroup ) {
294
+ // There is only one capturing group.
295
+ return null
296
+ }
297
+
298
+ return {
299
+ elements,
300
+ start,
301
+ end,
302
+ }
303
+ }
304
+
305
+ /** Convert end capturing group to lookahead assertion text. */
306
+ function endElementsToLookaheadAssertionText (
307
+ capturingGroup : CapturingGroup ,
308
+ trailingElements : Element [ ] ,
309
+ ) : string {
310
+ const groupPattern = capturingGroup . alternatives . map ( ( a ) => a . raw ) . join ( "|" )
311
+
312
+ const trailing = leadingTrailingElementsToLookaroundAssertionPatternText (
313
+ trailingElements ,
314
+ "lookahead" ,
315
+ )
316
+ if ( trailing && capturingGroup . alternatives . length !== 1 ) {
317
+ return `(?=(?:${ groupPattern } )${ trailing } )`
318
+ }
319
+ return `(?=${ groupPattern } ${ trailing } )`
320
+ }
321
+
322
+ /** Convert start capturing group to lookbehind assertion text. */
323
+ function startElementsToLookbehindAssertionText (
324
+ leadingElements : Element [ ] ,
325
+ capturingGroup : CapturingGroup ,
326
+ ) : string {
327
+ const leading = leadingTrailingElementsToLookaroundAssertionPatternText (
328
+ leadingElements ,
329
+ "lookbehind" ,
330
+ )
331
+ const groupPattern = capturingGroup . alternatives . map ( ( a ) => a . raw ) . join ( "|" )
332
+ if ( leading && capturingGroup . alternatives . length !== 1 ) {
333
+ return `(?<=${ leading } (?:${ groupPattern } ))`
334
+ }
335
+ return `(?<=${ leading } ${ groupPattern } )`
336
+ }
337
+
338
+ /** Convert leading/trailing elements to lookaround assertion pattern text. */
339
+ function leadingTrailingElementsToLookaroundAssertionPatternText (
340
+ leadingTrailingElements : Element [ ] ,
341
+ lookaroundAssertionKind : LookaroundAssertion [ "kind" ] ,
342
+ ) : string {
343
+ if (
344
+ leadingTrailingElements . length === 1 &&
345
+ leadingTrailingElements [ 0 ] . type === "Assertion"
346
+ ) {
347
+ const assertion = leadingTrailingElements [ 0 ]
348
+ if (
349
+ assertion . kind === lookaroundAssertionKind &&
350
+ ! assertion . negate &&
351
+ assertion . alternatives . length === 1
352
+ ) {
353
+ // If the leading/trailing assertion is simple (single alternative, and positive) lookaround assertion, unwrap the parens.
354
+ return assertion . alternatives [ 0 ] . raw
355
+ }
356
+ }
357
+
358
+ return leadingTrailingElements . map ( ( e ) => e . raw ) . join ( "" )
359
+ }
360
+
175
361
/**
176
362
* Parse option
177
363
*/
@@ -230,10 +416,8 @@ export default createRule("prefer-lookaround", {
230
416
regexpContext : RegExpContext ,
231
417
) : RegExpVisitor . Handlers {
232
418
const { regexpNode, patternAst } = regexpContext
233
- if (
234
- patternAst . alternatives . length > 1 ||
235
- patternAst . alternatives [ 0 ] . elements . length < 2
236
- ) {
419
+ const parsedElements = parsePatternElements ( patternAst )
420
+ if ( ! parsedElements ) {
237
421
return { }
238
422
}
239
423
const replaceReferenceList : ReplaceReferences [ ] = [ ]
@@ -297,6 +481,7 @@ export default createRule("prefer-lookaround", {
297
481
}
298
482
return createVerifyVisitor (
299
483
regexpContext ,
484
+ parsedElements ,
300
485
new ReplaceReferencesList ( replaceReferenceList ) ,
301
486
)
302
487
}
@@ -408,6 +593,7 @@ export default createRule("prefer-lookaround", {
408
593
*/
409
594
function createVerifyVisitor (
410
595
regexpContext : RegExpContext ,
596
+ parsedElements : ParsedElements ,
411
597
replaceReferenceList : ReplaceReferencesList ,
412
598
) : RegExpVisitor . Handlers {
413
599
type RefState = {
@@ -457,43 +643,29 @@ export default createRule("prefer-lookaround", {
457
643
}
458
644
}
459
645
} ,
460
- onPatternLeave ( pNode ) {
646
+ onPatternLeave ( ) {
461
647
// verify
462
- const alt = pNode . alternatives [ 0 ]
463
648
let reportStart = null
464
649
if (
465
650
! startRefState . isUseOther &&
466
651
startRefState . capturingGroups . length === 1 && // It will not be referenced from more than one, but check it just in case.
467
- startRefState . capturingGroups [ 0 ] === alt . elements [ 0 ] &&
468
- ! isZeroLength ( startRefState . capturingGroups [ 0 ] )
652
+ startRefState . capturingGroups [ 0 ] ===
653
+ parsedElements . start ?. capturingGroup
469
654
) {
470
- const capturingGroup = startRefState . capturingGroups [ 0 ]
471
- reportStart = {
472
- capturingGroup,
473
- expr : `(?<=${ capturingGroup . alternatives
474
- . map ( ( a ) => a . raw )
475
- . join ( "|" ) } )`,
476
- }
655
+ reportStart = parsedElements . start
477
656
}
478
657
let reportEnd = null
479
658
if (
480
659
! endRefState . isUseOther &&
481
660
endRefState . capturingGroups . length === 1 && // It will not be referenced from more than one, but check it just in case.
482
661
endRefState . capturingGroups [ 0 ] ===
483
- alt . elements [ alt . elements . length - 1 ] &&
484
- ! isZeroLength ( endRefState . capturingGroups [ 0 ] )
662
+ parsedElements . end ?. capturingGroup
485
663
) {
486
- const capturingGroup = endRefState . capturingGroups [ 0 ]
487
- reportEnd = {
488
- capturingGroup,
489
- expr : `(?=${ capturingGroup . alternatives
490
- . map ( ( a ) => a . raw )
491
- . join ( "|" ) } )`,
492
- }
664
+ reportEnd = parsedElements . end
493
665
}
494
666
const sideEffects =
495
667
getSideEffectsWhenReplacingCapturingGroup (
496
- alt . elements ,
668
+ parsedElements . elements ,
497
669
reportStart ?. capturingGroup ,
498
670
reportEnd ?. capturingGroup ,
499
671
regexpContext ,
@@ -530,12 +702,14 @@ export default createRule("prefer-lookaround", {
530
702
for ( const report of [ reportStart , reportEnd ] ) {
531
703
context . report ( {
532
704
loc : regexpContext . getRegexpLocation (
533
- report . capturingGroup ,
705
+ report . range ,
534
706
) ,
535
707
messageId : "preferLookarounds" ,
536
708
data : {
537
- expr1 : mention ( reportStart . expr ) ,
538
- expr2 : mention ( reportEnd . expr ) ,
709
+ expr1 : mention (
710
+ reportStart . replacedAssertion ,
711
+ ) ,
712
+ expr2 : mention ( reportEnd . replacedAssertion ) ,
539
713
} ,
540
714
fix,
541
715
} )
@@ -559,12 +733,12 @@ export default createRule("prefer-lookaround", {
559
733
)
560
734
context . report ( {
561
735
loc : regexpContext . getRegexpLocation (
562
- reportStart . capturingGroup ,
736
+ reportStart . range ,
563
737
) ,
564
738
messageId : "prefer" ,
565
739
data : {
566
740
kind : "lookbehind assertion" ,
567
- expr : mention ( reportStart . expr ) ,
741
+ expr : mention ( reportStart . replacedAssertion ) ,
568
742
} ,
569
743
fix,
570
744
} )
@@ -595,12 +769,12 @@ export default createRule("prefer-lookaround", {
595
769
)
596
770
context . report ( {
597
771
loc : regexpContext . getRegexpLocation (
598
- reportEnd . capturingGroup ,
772
+ reportEnd . range ,
599
773
) ,
600
774
messageId : "prefer" ,
601
775
data : {
602
776
kind : "lookahead assertion" ,
603
- expr : mention ( reportEnd . expr ) ,
777
+ expr : mention ( reportEnd . replacedAssertion ) ,
604
778
} ,
605
779
fix,
606
780
} )
@@ -614,10 +788,7 @@ export default createRule("prefer-lookaround", {
614
788
*/
615
789
function buildFixer (
616
790
regexpContext : RegExpContext ,
617
- replaceCapturingGroups : {
618
- capturingGroup : CapturingGroup
619
- expr : string
620
- } [ ] ,
791
+ replaceCapturingGroups : ( ParsedStartPattern | ParsedEndPattern ) [ ] ,
621
792
replaceReferenceList : ReplaceReferencesList ,
622
793
getRemoveRanges : (
623
794
replaceReference : ReplaceReferences ,
@@ -638,17 +809,17 @@ export default createRule("prefer-lookaround", {
638
809
}
639
810
const replaces : {
640
811
replaceRange : PatternReplaceRange
641
- expr : string
812
+ replacedAssertion : string
642
813
} [ ] = [ ]
643
- for ( const { capturingGroup , expr } of replaceCapturingGroups ) {
814
+ for ( const { range , replacedAssertion } of replaceCapturingGroups ) {
644
815
const replaceRange =
645
- regexpContext . patternSource . getReplaceRange ( capturingGroup )
816
+ regexpContext . patternSource . getReplaceRange ( range )
646
817
if ( ! replaceRange ) {
647
818
return null
648
819
}
649
820
replaces . push ( {
650
821
replaceRange,
651
- expr ,
822
+ replacedAssertion ,
652
823
} )
653
824
}
654
825
@@ -660,10 +831,11 @@ export default createRule("prefer-lookaround", {
660
831
fix : ( ) => fixer . removeRange ( removeRange ) ,
661
832
} )
662
833
}
663
- for ( const { replaceRange, expr } of replaces ) {
834
+ for ( const { replaceRange, replacedAssertion } of replaces ) {
664
835
list . push ( {
665
836
offset : replaceRange . range [ 0 ] ,
666
- fix : ( ) => replaceRange . replace ( fixer , expr ) ,
837
+ fix : ( ) =>
838
+ replaceRange . replace ( fixer , replacedAssertion ) ,
667
839
} )
668
840
}
669
841
return list
0 commit comments