Skip to content

Commit f22cb4f

Browse files
authored
Merge pull request #377 from hamishknight/named-captures-only
2 parents 9801855 + 2a4b3a6 commit f22cb4f

File tree

6 files changed

+42
-17
lines changed

6 files changed

+42
-17
lines changed

Sources/_RegexParser/Regex/AST/MatchingOptions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ extension AST {
1717
case caseInsensitive // i
1818
case allowDuplicateGroupNames // J
1919
case multiline // m
20-
case noAutoCapture // n
20+
case namedCapturesOnly // n
2121
case singleLine // s
2222
case reluctantByDefault // U
2323
case extended // x

Sources/_RegexParser/Regex/Parse/LexicalAnalysis.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ extension Source {
616616
case "i": return advanceAndReturn(.caseInsensitive)
617617
case "J": return advanceAndReturn(.allowDuplicateGroupNames)
618618
case "m": return advanceAndReturn(.multiline)
619-
case "n": return advanceAndReturn(.noAutoCapture)
619+
case "n": return advanceAndReturn(.namedCapturesOnly)
620620
case "s": return advanceAndReturn(.singleLine)
621621
case "U": return advanceAndReturn(.reluctantByDefault)
622622
case "x":
@@ -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*`

Sources/_StringProcessing/MatchingOptions.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ extension MatchingOptions {
135135
case caseInsensitive
136136
case allowDuplicateGroupNames
137137
case multiline
138-
case noAutoCapture
138+
case namedCapturesOnly
139139
case singleLine
140140
case reluctantByDefault
141141

@@ -174,8 +174,8 @@ extension MatchingOptions {
174174
self = .allowDuplicateGroupNames
175175
case .multiline:
176176
self = .multiline
177-
case .noAutoCapture:
178-
self = .noAutoCapture
177+
case .namedCapturesOnly:
178+
self = .namedCapturesOnly
179179
case .singleLine:
180180
self = .singleLine
181181
case .reluctantByDefault:

Tests/RegexTests/ParseTests.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -906,7 +906,7 @@ extension RegexTests {
906906
))
907907

908908
let allOptions: [AST.MatchingOption.Kind] = [
909-
.caseInsensitive, .allowDuplicateGroupNames, .multiline, .noAutoCapture,
909+
.caseInsensitive, .allowDuplicateGroupNames, .multiline, .namedCapturesOnly,
910910
.singleLine, .reluctantByDefault, .extraExtended, .extended,
911911
.unicodeWordBoundaries, .asciiOnlyDigit, .asciiOnlyPOSIXProps,
912912
.asciiOnlySpace, .asciiOnlyWord, .textSegmentGraphemeMode,
@@ -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)