@@ -289,12 +289,16 @@ extension Parser {
289
289
/// Apply the syntax options of a given matching option sequence to the
290
290
/// current set of options.
291
291
private mutating func applySyntaxOptions(
292
- of opts: AST . MatchingOptionSequence
293
- ) {
292
+ of opts: AST . MatchingOptionSequence , isScoped : Bool
293
+ ) throws {
294
294
func mapOption( _ option: SyntaxOptions ,
295
295
_ pred: ( AST . MatchingOption ) -> Bool ) {
296
296
if opts. resetsCurrentOptions {
297
- context. syntax. remove ( option)
297
+ // Extended syntax is never reset in a multi-line literal.
298
+ if !( option == . extendedSyntax &&
299
+ context. syntax. contains ( . multilineLiteral) ) {
300
+ context. syntax. remove ( option)
301
+ }
298
302
}
299
303
if opts. adding. contains ( where: pred) {
300
304
context. syntax. insert ( option)
@@ -311,22 +315,42 @@ extension Parser {
311
315
mapOption ( . namedCapturesOnly, . namedCapturesOnly)
312
316
313
317
// (?x), (?xx)
314
- // We skip this for multi-line, as extended syntax is always enabled there.
318
+ // We skip this for unscoped multi-line, as extended syntax is always
319
+ // enabled there. A scoped group however (?-x:...) allows extended syntax to
320
+ // be disabled.
315
321
// TODO: PCRE differentiates between (?x) and (?xx) where only the latter
316
322
// handles non-semantic whitespace in a custom character class. Other
317
323
// engines such as Oniguruma, Java, and ICU do this under (?x). Therefore,
318
324
// treat (?x) and (?xx) as the same option here. If we ever get a strict
319
325
// PCRE mode, we will need to change this to handle that.
320
- if !context. syntax. contains ( . multilineLiteral) {
326
+ if !isScoped && context. syntax. contains ( . multilineLiteral) {
327
+ // An unscoped removal of extended syntax is not allowed in a multi-line
328
+ // literal.
329
+ if let opt = opts. removing. first ( where: \. isAnyExtended) {
330
+ throw Source . LocatedError (
331
+ ParseError . cannotRemoveExtendedSyntaxInMultilineMode, opt. location)
332
+ }
333
+ // We may have an unscoped change of extended syntax, but it must either
334
+ // be:
335
+ // - An addition of extended syntax
336
+ // - A reset of matching options
337
+ // The former is redundant, and the latter shouldn't affect extended
338
+ // syntax in a multi-line literal. So we don't need to do anything extra
339
+ // here.
340
+ } else {
341
+ // We either have a scoped change of extended syntax, or this is a
342
+ // single-line literal.
321
343
mapOption ( . extendedSyntax, \. isAnyExtended)
322
344
}
323
345
}
324
346
325
347
/// Apply the syntax options of a matching option changing group to the
326
348
/// current set of options.
327
- private mutating func applySyntaxOptions( of group: AST . Group . Kind ) {
349
+ private mutating func applySyntaxOptions(
350
+ of group: AST . Group . Kind , isScoped: Bool
351
+ ) throws {
328
352
if case . changeMatchingOptions( let seq) = group {
329
- applySyntaxOptions ( of: seq)
353
+ try applySyntaxOptions ( of: seq, isScoped : isScoped )
330
354
}
331
355
}
332
356
@@ -337,14 +361,25 @@ extension Parser {
337
361
context. recordGroup ( kind. value)
338
362
339
363
let currentSyntax = context. syntax
340
- applySyntaxOptions ( of: kind. value)
364
+ try applySyntaxOptions ( of: kind. value, isScoped : true )
341
365
defer {
342
366
context. syntax = currentSyntax
343
367
}
344
-
368
+ let unsetsExtendedSyntax = currentSyntax. contains ( . extendedSyntax) &&
369
+ !context. syntax. contains ( . extendedSyntax)
345
370
let child = try parseNode ( )
346
371
try source. expect ( " ) " )
347
- return . init( kind, child, loc ( start) )
372
+ let groupLoc = loc ( start)
373
+
374
+ // In multi-line literals, the body of a group that unsets extended syntax
375
+ // may not span multiple lines.
376
+ if unsetsExtendedSyntax &&
377
+ context. syntax. contains ( . multilineLiteral) &&
378
+ source [ child. location. range] . spansMultipleLinesInRegexLiteral {
379
+ throw Source . LocatedError (
380
+ ParseError . unsetExtendedSyntaxMayNotSpanMultipleLines, groupLoc)
381
+ }
382
+ return . init( kind, child, groupLoc)
348
383
}
349
384
350
385
/// Consume the body of an absent function.
@@ -438,7 +473,7 @@ extension Parser {
438
473
// If we have a change matching options atom, apply the syntax options. We
439
474
// already take care of scoping syntax options within a group.
440
475
if case . changeMatchingOptions( let opts) = atom. kind {
441
- applySyntaxOptions ( of: opts)
476
+ try applySyntaxOptions ( of: opts, isScoped : false )
442
477
}
443
478
// TODO: track source locations
444
479
return . atom( atom)
@@ -592,7 +627,7 @@ public func parse<S: StringProtocol>(
592
627
return ast
593
628
}
594
629
595
- extension String {
630
+ extension StringProtocol {
596
631
/// Whether the given string is considered multi-line for a regex literal.
597
632
var spansMultipleLinesInRegexLiteral : Bool {
598
633
unicodeScalars. contains ( where: { $0 == " \n " || $0 == " \r " } )
0 commit comments