@@ -289,8 +289,8 @@ extension Parser {
289289 /// Apply the syntax options of a given matching option sequence to the
290290 /// current set of options.
291291 private mutating func applySyntaxOptions(
292- of opts: AST . MatchingOptionSequence
293- ) {
292+ of opts: AST . MatchingOptionSequence , isScoped : Bool
293+ ) throws {
294294 func mapOption( _ option: SyntaxOptions ,
295295 _ pred: ( AST . MatchingOption ) -> Bool ) {
296296 if opts. resetsCurrentOptions {
@@ -311,22 +311,41 @@ extension Parser {
311311 mapOption ( . namedCapturesOnly, . namedCapturesOnly)
312312
313313 // (?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.
315317 // TODO: PCRE differentiates between (?x) and (?xx) where only the latter
316318 // handles non-semantic whitespace in a custom character class. Other
317319 // engines such as Oniguruma, Java, and ICU do this under (?x). Therefore,
318320 // treat (?x) and (?xx) as the same option here. If we ever get a strict
319321 // 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.
321338 mapOption ( . extendedSyntax, \. isAnyExtended)
322339 }
323340 }
324341
325342 /// Apply the syntax options of a matching option changing group to the
326343 /// 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 {
328347 if case . changeMatchingOptions( let seq) = group {
329- applySyntaxOptions ( of: seq)
348+ try applySyntaxOptions ( of: seq, isScoped : isScoped )
330349 }
331350 }
332351
@@ -337,14 +356,25 @@ extension Parser {
337356 context. recordGroup ( kind. value)
338357
339358 let currentSyntax = context. syntax
340- applySyntaxOptions ( of: kind. value)
359+ try applySyntaxOptions ( of: kind. value, isScoped : true )
341360 defer {
342361 context. syntax = currentSyntax
343362 }
344-
363+ let unsetsExtendedSyntax = currentSyntax. contains ( . extendedSyntax) &&
364+ !context. syntax. contains ( . extendedSyntax)
345365 let child = try parseNode ( )
346366 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)
348378 }
349379
350380 /// Consume the body of an absent function.
@@ -438,7 +468,7 @@ extension Parser {
438468 // If we have a change matching options atom, apply the syntax options. We
439469 // already take care of scoping syntax options within a group.
440470 if case . changeMatchingOptions( let opts) = atom. kind {
441- applySyntaxOptions ( of: opts)
471+ try applySyntaxOptions ( of: opts, isScoped : false )
442472 }
443473 // TODO: track source locations
444474 return . atom( atom)
@@ -592,7 +622,7 @@ public func parse<S: StringProtocol>(
592622 return ast
593623}
594624
595- extension String {
625+ extension StringProtocol {
596626 /// Whether the given string is considered multi-line for a regex literal.
597627 var spansMultipleLinesInRegexLiteral : Bool {
598628 unicodeScalars. contains ( where: { $0 == " \n " || $0 == " \r " } )
@@ -609,7 +639,7 @@ fileprivate func defaultSyntaxOptions(
609639 // For an extended syntax forward slash e.g #/.../#, extended syntax is
610640 // permitted if it spans multiple lines.
611641 if delim. poundCount > 0 && contents. spansMultipleLinesInRegexLiteral {
612- return . multilineExtendedSyntax
642+ return [ . multilineCompilerLiteral , . extendedSyntax ]
613643 }
614644 return . traditional
615645 case . reSingleQuote:
0 commit comments