Skip to content

Commit 2a4b3a6

Browse files
committed
Implement the (?n) option
This switches `(...)` groups to being non-capturing, with only named groups capturing.
1 parent 0e5cfa8 commit 2a4b3a6

File tree

4 files changed

+36
-11
lines changed

4 files changed

+36
-11
lines changed

Sources/_RegexParser/Regex/Parse/LexicalAnalysis.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -914,6 +914,10 @@ extension Source {
914914
}
915915
// TODO: (name:)
916916

917+
// If (?n) is set, a bare (...) group is non-capturing.
918+
if context.syntax.contains(.namedCapturesOnly) {
919+
return .nonCapture
920+
}
917921
return .capture
918922
}
919923
}

Sources/_RegexParser/Regex/Parse/Parse.swift

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -287,23 +287,34 @@ extension Parser {
287287
private mutating func applySyntaxOptions(
288288
of opts: AST.MatchingOptionSequence
289289
) {
290-
// We skip this for multi-line, as extended syntax is always enabled there.
291-
if context.syntax.contains(.multilineExtendedSyntax) { return }
290+
func mapOption(_ option: SyntaxOptions,
291+
_ pred: (AST.MatchingOption) -> Bool) {
292+
if opts.resetsCurrentOptions {
293+
context.syntax.remove(option)
294+
}
295+
if opts.adding.contains(where: pred) {
296+
context.syntax.insert(option)
297+
}
298+
if opts.removing.contains(where: pred) {
299+
context.syntax.remove(option)
300+
}
301+
}
302+
func mapOption(_ option: SyntaxOptions, _ kind: AST.MatchingOption.Kind) {
303+
mapOption(option, { $0.kind == kind })
304+
}
305+
306+
// (?n)
307+
mapOption(.namedCapturesOnly, .namedCapturesOnly)
292308

293-
// Check if we're introducing or removing extended syntax.
309+
// (?x), (?xx)
310+
// We skip this for multi-line, as extended syntax is always enabled there.
294311
// TODO: PCRE differentiates between (?x) and (?xx) where only the latter
295312
// handles non-semantic whitespace in a custom character class. Other
296313
// engines such as Oniguruma, Java, and ICU do this under (?x). Therefore,
297314
// treat (?x) and (?xx) as the same option here. If we ever get a strict
298315
// PCRE mode, we will need to change this to handle that.
299-
if opts.resetsCurrentOptions {
300-
context.syntax.remove(.extendedSyntax)
301-
}
302-
if opts.adding.contains(where: \.isAnyExtended) {
303-
context.syntax.insert(.extendedSyntax)
304-
}
305-
if opts.removing.contains(where: \.isAnyExtended) {
306-
context.syntax.remove(.extendedSyntax)
316+
if !context.syntax.contains(.multilineExtendedSyntax) {
317+
mapOption(.extendedSyntax, \.isAnyExtended)
307318
}
308319
}
309320

Sources/_RegexParser/Regex/Parse/SyntaxOptions.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ public struct SyntaxOptions: OptionSet {
6363
return [Self(1 << 6), .extendedSyntax]
6464
}
6565

66+
/// `(?n)`
67+
public static var namedCapturesOnly: Self { Self(1 << 7) }
68+
6669
/*
6770

6871
/// `<digit>*` == `[[:digit:]]*` == `\d*`

Tests/RegexTests/ParseTests.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,13 @@ extension RegexTests {
973973
"d"
974974
)), captures: [.cap])
975975

976+
parseTest("(?n)(?^:())(?<x>)()", concat(
977+
changeMatchingOptions(matchingOptions(adding: .namedCapturesOnly)),
978+
changeMatchingOptions(unsetMatchingOptions(), capture(empty())),
979+
namedCapture("x", empty()),
980+
nonCapture(empty())
981+
), captures: [.cap, .named("x")])
982+
976983
// MARK: References
977984

978985
// \1 ... \9 are always backreferences.

0 commit comments

Comments
 (0)