Skip to content

Commit d002466

Browse files
rxweiAzoy
authored andcommitted
Merge pull request swiftlang#273 from itingliu/throwing-hooks
Throwing matches and update to CustomMatchingRegexComponent
1 parent fde4c58 commit d002466

File tree

5 files changed

+263
-14
lines changed

5 files changed

+263
-14
lines changed

Documentation/Evolution/StringProcessingAlgorithms.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public protocol CustomMatchingRegexComponent : RegexComponent {
187187
_ input: String,
188188
startingAt index: String.Index,
189189
in bounds: Range<String.Index>
190-
) -> (upperBound: String.Index, match: Match)?
190+
) throws -> (upperBound: String.Index, match: Match)?
191191
}
192192
```
193193

Sources/_StringProcessing/ByteCodeGen.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,9 @@ extension Compiler.ByteCodeGen {
299299
// not captured. This may mean we should store
300300
// an existential instead of a closure...
301301

302-
let matcher = builder.makeMatcherFunction(matcher)
302+
let matcher = builder.makeMatcherFunction { input, start, range in
303+
try matcher(input, start, range)
304+
}
303305

304306
let valReg = builder.makeValueRegister()
305307
builder.buildMatcher(matcher, into: valReg)

Sources/_StringProcessing/Regex/DSLConsumers.swift

+8-6
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
@available(SwiftStdlib 5.7, *)
13-
public protocol CustomRegexComponent: RegexComponent {
13+
public protocol CustomMatchingRegexComponent: RegexComponent {
1414
func match(
1515
_ input: String,
1616
startingAt index: String.Index,
1717
in bounds: Range<String.Index>
18-
) -> (upperBound: String.Index, output: RegexOutput)?
18+
) throws -> (upperBound: String.Index, output: RegexOutput)?
1919
}
2020

2121
@available(SwiftStdlib 5.7, *)
22-
extension CustomRegexComponent {
22+
extension CustomMatchingRegexComponent {
2323
public var regex: Regex<RegexOutput> {
24-
Regex(node: .matcher(.init(RegexOutput.self), { input, index, bounds in
25-
match(input, startingAt: index, in: bounds)
26-
}))
24+
25+
let node: DSLTree.Node = .matcher(.init(RegexOutput.self), { input, index, bounds in
26+
try match(input, startingAt: index, in: bounds)
27+
})
28+
return Regex(node: node)
2729
}
2830
}

Tests/RegexBuilderTests/CustomTests.swift

+247-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import _StringProcessing
1414
@testable import RegexBuilder
1515

1616
// A nibbler processes a single character from a string
17-
private protocol Nibbler: CustomRegexComponent {
17+
private protocol Nibbler: CustomMatchingRegexComponent {
1818
func nibble(_: Character) -> RegexOutput?
1919
}
2020

@@ -24,7 +24,7 @@ extension Nibbler {
2424
_ input: String,
2525
startingAt index: String.Index,
2626
in bounds: Range<String.Index>
27-
) -> (upperBound: String.Index, output: RegexOutput)? {
27+
) throws -> (upperBound: String.Index, output: RegexOutput)? {
2828
guard index != bounds.upperBound, let res = nibble(input[index]) else {
2929
return nil
3030
}
@@ -49,6 +49,69 @@ private struct Asciibbler: Nibbler {
4949
}
5050
}
5151

52+
private struct IntParser: CustomMatchingRegexComponent {
53+
struct ParseError: Error, Hashable {}
54+
typealias RegexOutput = Int
55+
func match(_ input: String,
56+
startingAt index: String.Index,
57+
in bounds: Range<String.Index>
58+
) throws -> (upperBound: String.Index, output: Int)? {
59+
guard index != bounds.upperBound else { return nil }
60+
61+
let r = Regex {
62+
Capture(OneOrMore(.digit)) { Int($0) }
63+
}
64+
65+
guard let match = input[index..<bounds.upperBound].prefixMatch(of: r),
66+
let output = match.1 else {
67+
throw ParseError()
68+
}
69+
70+
return (match.range.upperBound, output)
71+
}
72+
}
73+
74+
private struct CurrencyParser: CustomMatchingRegexComponent {
75+
enum Currency: String, Hashable {
76+
case usd = "USD"
77+
case ntd = "NTD"
78+
case dem = "DEM"
79+
}
80+
81+
enum ParseError: Error, Hashable {
82+
case unrecognized
83+
case deprecated
84+
}
85+
86+
typealias RegexOutput = Currency
87+
func match(_ input: String,
88+
startingAt index: String.Index,
89+
in bounds: Range<String.Index>
90+
) throws -> (upperBound: String.Index, output: Currency)? {
91+
92+
guard index != bounds.upperBound else { return nil }
93+
94+
let substr = input[index..<bounds.upperBound]
95+
guard !substr.isEmpty else { return nil }
96+
97+
let currencies: [Currency] = [ .usd, .ntd ]
98+
let deprecated: [Currency] = [ .dem ]
99+
100+
for currency in currencies {
101+
if substr.hasPrefix(currency.rawValue) {
102+
return (input.range(of: currency.rawValue)!.upperBound, currency)
103+
}
104+
}
105+
106+
for dep in deprecated {
107+
if substr.hasPrefix(dep.rawValue) {
108+
throw ParseError.deprecated
109+
}
110+
}
111+
throw ParseError.unrecognized
112+
}
113+
}
114+
52115
enum MatchCall {
53116
case match
54117
case firstMatch
@@ -223,4 +286,186 @@ class CustomRegexComponentTests: XCTestCase {
223286

224287

225288
}
289+
290+
func testCustomRegexThrows() {
291+
292+
func customTest<Match: Equatable, E: Error & Equatable>(
293+
_ regex: Regex<Match>,
294+
_ tests: (input: String, match: Match?, expectError: E?)...,
295+
file: StaticString = #file,
296+
line: UInt = #line
297+
) {
298+
for (input, match, expectError) in tests {
299+
do {
300+
let result = try regex.wholeMatch(in: input)?.output
301+
XCTAssertEqual(result, match)
302+
} catch let e as E {
303+
XCTAssertEqual(e, expectError)
304+
} catch {
305+
XCTFail()
306+
}
307+
}
308+
}
309+
310+
func customTest<Match: Equatable, Error1: Error & Equatable, Error2: Error & Equatable>(
311+
_ regex: Regex<Match>,
312+
_ tests: (input: String, match: Match?, expectError1: Error1?, expectError2: Error2?)...,
313+
file: StaticString = #file,
314+
line: UInt = #line
315+
) {
316+
for (input, match, expectError1, expectError2) in tests {
317+
do {
318+
let result = try regex.wholeMatch(in: input)?.output
319+
XCTAssertEqual(result, match)
320+
} catch let e as Error1 {
321+
XCTAssertEqual(e, expectError1, input, file: file, line: line)
322+
} catch let e as Error2 {
323+
XCTAssertEqual(e, expectError2, input, file: file, line: line)
324+
} catch {
325+
XCTFail("caught error: \(error.localizedDescription)")
326+
}
327+
}
328+
}
329+
330+
func customTest<Capture: Equatable, Error1: Error & Equatable, Error2: Error & Equatable>(
331+
_ regex: Regex<(Substring, Capture)>,
332+
_ tests: (input: String, match: (Substring, Capture)?, expectError1: Error1?, expectError2: Error2?)...,
333+
file: StaticString = #file,
334+
line: UInt = #line
335+
) {
336+
for (input, match, expectError1, expectError2) in tests {
337+
do {
338+
let result = try regex.wholeMatch(in: input)?.output
339+
XCTAssertEqual(result?.0, match?.0, file: file, line: line)
340+
XCTAssertEqual(result?.1, match?.1, file: file, line: line)
341+
} catch let e as Error1 {
342+
XCTAssertEqual(e, expectError1, input, file: file, line: line)
343+
} catch let e as Error2 {
344+
XCTAssertEqual(e, expectError2, input, file: file, line: line)
345+
} catch {
346+
XCTFail("caught error: \(error.localizedDescription)")
347+
}
348+
}
349+
}
350+
351+
func customTest<Capture1: Equatable, Capture2: Equatable, Error1: Error & Equatable, Error2: Error & Equatable>(
352+
_ regex: Regex<(Substring, Capture1, Capture2)>,
353+
_ tests: (input: String, match: (Substring, Capture1, Capture2)?, expectError1: Error1?, expectError2: Error2?)...,
354+
file: StaticString = #file,
355+
line: UInt = #line
356+
) {
357+
for (input, match, expectError1, expectError2) in tests {
358+
do {
359+
let result = try regex.wholeMatch(in: input)?.output
360+
XCTAssertEqual(result?.0, match?.0, file: file, line: line)
361+
XCTAssertEqual(result?.1, match?.1, file: file, line: line)
362+
XCTAssertEqual(result?.2, match?.2, file: file, line: line)
363+
} catch let e as Error1 {
364+
XCTAssertEqual(e, expectError1, input, file: file, line: line)
365+
} catch let e as Error2 {
366+
XCTAssertEqual(e, expectError2, input, file: file, line: line)
367+
} catch {
368+
XCTFail("caught error: \(error.localizedDescription)")
369+
}
370+
}
371+
}
372+
373+
// No capture, one error
374+
customTest(
375+
Regex {
376+
IntParser()
377+
},
378+
("zzz", nil, IntParser.ParseError()),
379+
("x10x", nil, IntParser.ParseError()),
380+
("30", 30, nil)
381+
)
382+
383+
customTest(
384+
Regex {
385+
CurrencyParser()
386+
},
387+
("USD", .usd, nil),
388+
("NTD", .ntd, nil),
389+
("NTD USD", nil, nil),
390+
("DEM", nil, CurrencyParser.ParseError.deprecated),
391+
("XXX", nil, CurrencyParser.ParseError.unrecognized)
392+
)
393+
394+
// No capture, two errors
395+
customTest(
396+
Regex {
397+
IntParser()
398+
" "
399+
IntParser()
400+
},
401+
("20304 100", "20304 100", nil, nil),
402+
("20304.445 200", nil, IntParser.ParseError(), nil),
403+
("20304 200.123", nil, nil, IntParser.ParseError()),
404+
("20304.445 200.123", nil, IntParser.ParseError(), IntParser.ParseError())
405+
)
406+
407+
customTest(
408+
Regex {
409+
CurrencyParser()
410+
IntParser()
411+
},
412+
("USD100", "USD100", nil, nil),
413+
("XXX100", nil, CurrencyParser.ParseError.unrecognized, nil),
414+
("USD100.000", nil, nil, IntParser.ParseError()),
415+
("XXX100.0000", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
416+
)
417+
418+
// One capture, two errors: One error is thrown from inside a capture,
419+
// while the other one is thrown from outside
420+
customTest(
421+
Regex {
422+
Capture { CurrencyParser() }
423+
IntParser()
424+
},
425+
("USD100", ("USD100", .usd), nil, nil),
426+
("NTD305.5", nil, nil, IntParser.ParseError()),
427+
("DEM200", ("DEM200", .dem), CurrencyParser.ParseError.deprecated, nil),
428+
("XXX", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
429+
)
430+
431+
customTest(
432+
Regex {
433+
CurrencyParser()
434+
Capture { IntParser() }
435+
},
436+
("USD100", ("USD100", 100), nil, nil),
437+
("NTD305.5", nil, nil, IntParser.ParseError()),
438+
("DEM200", ("DEM200", 200), CurrencyParser.ParseError.deprecated, nil),
439+
("XXX", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
440+
)
441+
442+
// One capture, two errors: Both errors are thrown from inside the capture
443+
customTest(
444+
Regex {
445+
Capture {
446+
CurrencyParser()
447+
IntParser()
448+
}
449+
},
450+
("USD100", ("USD100", "USD100"), nil, nil),
451+
("NTD305.5", nil, nil, IntParser.ParseError()),
452+
("DEM200", ("DEM200", "DEM200"), CurrencyParser.ParseError.deprecated, nil),
453+
("XXX", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError())
454+
)
455+
456+
// Two captures, two errors: Different erros are thrown from inside captures
457+
customTest(
458+
Regex {
459+
Capture(CurrencyParser())
460+
Capture(IntParser())
461+
},
462+
("USD100", ("USD100", .usd, 100), nil, nil),
463+
("NTD500", ("NTD500", .ntd, 500), nil, nil),
464+
("XXX20", nil, CurrencyParser.ParseError.unrecognized, IntParser.ParseError()),
465+
("DEM500", nil, CurrencyParser.ParseError.deprecated, nil),
466+
("DEM500.345", nil, CurrencyParser.ParseError.deprecated, IntParser.ParseError()),
467+
("NTD100.345", nil, nil, IntParser.ParseError())
468+
)
469+
470+
}
226471
}

Tests/RegexBuilderTests/RegexDSLTests.swift

+4-4
Original file line numberDiff line numberDiff line change
@@ -743,13 +743,13 @@ class RegexDSLTests: XCTestCase {
743743
var patch: Int
744744
var dev: String?
745745
}
746-
struct SemanticVersionParser: CustomRegexComponent {
746+
struct SemanticVersionParser: CustomMatchingRegexComponent {
747747
typealias RegexOutput = SemanticVersion
748748
func match(
749749
_ input: String,
750750
startingAt index: String.Index,
751751
in bounds: Range<String.Index>
752-
) -> (upperBound: String.Index, output: SemanticVersion)? {
752+
) throws -> (upperBound: String.Index, output: SemanticVersion)? {
753753
let regex = Regex {
754754
TryCapture(OneOrMore(.digit)) { Int($0) }
755755
"."
@@ -776,13 +776,13 @@ class RegexDSLTests: XCTestCase {
776776
return (match.range.upperBound, result)
777777
}
778778
}
779-
779+
780780
let versions = [
781781
("1.0", SemanticVersion(major: 1, minor: 0, patch: 0)),
782782
("1.0.1", SemanticVersion(major: 1, minor: 0, patch: 1)),
783783
("12.100.5-dev", SemanticVersion(major: 12, minor: 100, patch: 5, dev: "dev")),
784784
]
785-
785+
786786
let parser = SemanticVersionParser()
787787
for (str, version) in versions {
788788
XCTAssertEqual(str.wholeMatch(of: parser)?.output, version)

0 commit comments

Comments
 (0)