Skip to content

Commit af6a199

Browse files
committed
Include optionality in AnyRegexOutput.Element.type
When reconstructing the type of a captured element for access through the `AnyRegexOutput.Element.type` API, any optionality is being omitted (e.g. `Int?` is returned as `Int`). This is both observable through that API and results in a bug when accessing part of a match's output through the dynamic member subscript (e.g. `match.1`). In that case, the lack of optionality causes an incorrect calculation of the output tuple member's position, resulting in the wrong member being loaded and returned. This change adds the missing optionality to the type. Fixes swiftlang/swift#83022
1 parent 650b2c1 commit af6a199

File tree

2 files changed

+57
-2
lines changed

2 files changed

+57
-2
lines changed

Sources/_StringProcessing/Regex/AnyRegexOutput.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,7 +381,12 @@ extension AnyRegexOutput.ElementRepresentation {
381381
}
382382

383383
var type: Any.Type {
384-
content?.value.map { Swift.type(of: $0) }
385-
?? TypeConstruction.optionalType(of: Substring.self, depth: optionalDepth)
384+
func wrapIfNecessary<U>(_: U.Type) -> Any.Type {
385+
TypeConstruction.optionalType(of: U.self, depth: optionalDepth)
386+
}
387+
388+
return content?.value.map {
389+
_openExistential(Swift.type(of: $0), do: wrapIfNecessary)
390+
} ?? TypeConstruction.optionalType(of: Substring.self, depth: optionalDepth)
386391
}
387392
}

Tests/RegexBuilderTests/RegexDSLTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,6 +1962,56 @@ extension RegexDSLTests {
19621962
XCTAssertNotNil(clip.contains(pattern))
19631963
XCTAssertNotNil(clip2.contains(pattern))
19641964
}
1965+
1966+
func testIssue83022() throws {
1967+
// Original report from https://github.com/swiftlang/swift/issues/83022
1968+
// rdar://155710126
1969+
let mixedNumberRegex = Regex {
1970+
// whole number
1971+
Optionally {
1972+
Capture {
1973+
OneOrMore(.digit)
1974+
} transform: { Int($0)! }
1975+
OneOrMore { " " }
1976+
}
1977+
// numerator
1978+
Capture {
1979+
OneOrMore(.digit)
1980+
} transform: { Int($0)! }
1981+
"/"
1982+
// denominator
1983+
Capture {
1984+
OneOrMore(.digit)
1985+
} transform: { Int($0)! }
1986+
}
1987+
1988+
do {
1989+
let match = try XCTUnwrap(mixedNumberRegex.wholeMatch(in: "1 3/4"))
1990+
XCTAssertEqual(match.1, 1)
1991+
XCTAssertEqual(match.2, 3)
1992+
XCTAssertEqual(match.3, 4)
1993+
1994+
let erasedMatch = Regex<AnyRegexOutput>.Match(match)
1995+
XCTAssert(erasedMatch.output[0].type == Substring.self)
1996+
XCTAssert(erasedMatch.output[1].type == Int?.self)
1997+
XCTAssert(erasedMatch.output[2].type == Int.self)
1998+
XCTAssert(erasedMatch.output[3].type == Int.self)
1999+
}
2000+
2001+
do {
2002+
let match = try XCTUnwrap(mixedNumberRegex.wholeMatch(in: "3/4"))
2003+
XCTAssertNil(match.1)
2004+
XCTAssertEqual(match.2, 3)
2005+
XCTAssertEqual(match.3, 4)
2006+
2007+
let erasedMatch = Regex<AnyRegexOutput>.Match(match)
2008+
XCTAssert(erasedMatch.output[0].type == Substring.self)
2009+
print(erasedMatch.output[1].type)
2010+
XCTAssert(erasedMatch.output[1].type == Int?.self)
2011+
XCTAssert(erasedMatch.output[2].type == Int.self)
2012+
XCTAssert(erasedMatch.output[3].type == Int.self)
2013+
}
2014+
}
19652015
}
19662016

19672017
extension Unicode.Scalar {

0 commit comments

Comments
 (0)