@@ -289,8 +289,8 @@ 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 {
@@ -311,22 +311,41 @@ extension Parser {
311
311
mapOption ( . namedCapturesOnly, . namedCapturesOnly)
312
312
313
313
// (?x), (?xx)
314
- // We skip this for multi-line, as extended syntax is always enabled there.
314
+ // This cannot be unset in a multi-line literal, unless in a scoped group
315
+ // e.g (?-x:...). We later enforce that such a group does not span multiple
316
+ // lines.
315
317
// TODO: PCRE differentiates between (?x) and (?xx) where only the latter
316
318
// handles non-semantic whitespace in a custom character class. Other
317
319
// engines such as Oniguruma, Java, and ICU do this under (?x). Therefore,
318
320
// treat (?x) and (?xx) as the same option here. If we ever get a strict
319
321
// PCRE mode, we will need to change this to handle that.
320
- if !context. syntax. contains ( . multilineExtendedSyntax) {
322
+ if !isScoped && context. syntax. contains ( . multilineCompilerLiteral) {
323
+ // An unscoped removal of extended syntax is not allowed in a multi-line
324
+ // literal.
325
+ if let opt = opts. removing. first ( where: \. isAnyExtended) {
326
+ throw Source . LocatedError (
327
+ ParseError . cannotRemoveExtendedSyntaxInMultilineMode, opt. location)
328
+ }
329
+ if opts. resetsCurrentOptions {
330
+ throw Source . LocatedError (
331
+ ParseError . cannotResetExtendedSyntaxInMultilineMode, opts. caretLoc!)
332
+ }
333
+ // The only remaning case is an unscoped addition of extended syntax,
334
+ // which is a no-op.
335
+ } else {
336
+ // We either have a scoped change of extended syntax, or this is a
337
+ // single-line literal.
321
338
mapOption ( . extendedSyntax, \. isAnyExtended)
322
339
}
323
340
}
324
341
325
342
/// Apply the syntax options of a matching option changing group to the
326
343
/// current set of options.
327
- private mutating func applySyntaxOptions( of group: AST . Group . Kind ) {
344
+ private mutating func applySyntaxOptions(
345
+ of group: AST . Group . Kind , isScoped: Bool
346
+ ) throws {
328
347
if case . changeMatchingOptions( let seq) = group {
329
- applySyntaxOptions ( of: seq)
348
+ try applySyntaxOptions ( of: seq, isScoped : isScoped )
330
349
}
331
350
}
332
351
@@ -337,14 +356,25 @@ extension Parser {
337
356
context. recordGroup ( kind. value)
338
357
339
358
let currentSyntax = context. syntax
340
- applySyntaxOptions ( of: kind. value)
359
+ try applySyntaxOptions ( of: kind. value, isScoped : true )
341
360
defer {
342
361
context. syntax = currentSyntax
343
362
}
344
-
363
+ let unsetsExtendedSyntax = currentSyntax. contains ( . extendedSyntax) &&
364
+ !context. syntax. contains ( . extendedSyntax)
345
365
let child = try parseNode ( )
346
366
try source. expect ( " ) " )
347
- return . init( kind, child, loc ( start) )
367
+ let groupLoc = loc ( start)
368
+
369
+ // In multi-line literals, the body of a group that unsets extended syntax
370
+ // may not span multiple lines.
371
+ if unsetsExtendedSyntax &&
372
+ context. syntax. contains ( . multilineCompilerLiteral) &&
373
+ source [ child. location. range] . spansMultipleLinesInRegexLiteral {
374
+ throw Source . LocatedError (
375
+ ParseError . unsetExtendedSyntaxMayNotSpanMultipleLines, groupLoc)
376
+ }
377
+ return . init( kind, child, groupLoc)
348
378
}
349
379
350
380
/// Consume the body of an absent function.
@@ -438,7 +468,7 @@ extension Parser {
438
468
// If we have a change matching options atom, apply the syntax options. We
439
469
// already take care of scoping syntax options within a group.
440
470
if case . changeMatchingOptions( let opts) = atom. kind {
441
- applySyntaxOptions ( of: opts)
471
+ try applySyntaxOptions ( of: opts, isScoped : false )
442
472
}
443
473
// TODO: track source locations
444
474
return . atom( atom)
@@ -592,7 +622,7 @@ public func parse<S: StringProtocol>(
592
622
return ast
593
623
}
594
624
595
- extension String {
625
+ extension StringProtocol {
596
626
/// Whether the given string is considered multi-line for a regex literal.
597
627
var spansMultipleLinesInRegexLiteral : Bool {
598
628
unicodeScalars. contains ( where: { $0 == " \n " || $0 == " \r " } )
@@ -609,7 +639,7 @@ fileprivate func defaultSyntaxOptions(
609
639
// For an extended syntax forward slash e.g #/.../#, extended syntax is
610
640
// permitted if it spans multiple lines.
611
641
if delim. poundCount > 0 && contents. spansMultipleLinesInRegexLiteral {
612
- return . multilineExtendedSyntax
642
+ return [ . multilineCompilerLiteral , . extendedSyntax ]
613
643
}
614
644
return . traditional
615
645
case . reSingleQuote:
0 commit comments