Skip to content

Commit 11db05a

Browse files
committed
Ensure we correctly handle raw identifiers in '(extension in' substrings
1 parent 2810ef5 commit 11db05a

File tree

3 files changed

+51
-27
lines changed

3 files changed

+51
-27
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,52 @@ public struct TypeInfo: Sendable {
9595

9696
// MARK: - Name
9797

98+
/// Split a string with a separator while respecting raw identifiers and their
99+
/// enclosing backtick characters.
100+
///
101+
/// - Parameters:
102+
/// - string: The string to split.
103+
/// - separator: The character that separates components of `string`.
104+
/// - maxSplits: The maximum number of splits to perform on `string`. The
105+
/// resulting array contains up to `maxSplits + 1` elements.
106+
///
107+
/// - Returns: An array of substrings of `string`.
108+
///
109+
/// Unlike `String.split(separator:maxSplits:omittingEmptySubsequences:)`, this
110+
/// function does not split the string on separator characters that occur
111+
/// between pairs of backtick characters. This is useful when splitting strings
112+
/// containing raw identifiers.
113+
///
114+
/// - Complexity: O(_n_), where _n_ is the length of `string`.
115+
func rawIdentifierAwareSplit<S>(_ string: S, separator: Character, maxSplits: Int = .max) -> [S.SubSequence] where S: StringProtocol {
116+
var result = [S.SubSequence]()
117+
118+
var inRawIdentifier = false
119+
var componentStartIndex = string.startIndex
120+
for i in string.indices {
121+
let c = string[i]
122+
if c == "`" {
123+
// We are either entering or exiting a raw identifier. While inside a raw
124+
// identifier, separator characters are ignored.
125+
inRawIdentifier.toggle()
126+
} else if c == separator && !inRawIdentifier {
127+
// Add everything up to this separator as the next component, then start
128+
// a new component after the separator.
129+
result.append(string[componentStartIndex ..< i])
130+
componentStartIndex = string.index(after: i)
131+
132+
if result.count == maxSplits {
133+
// We don't need to find more separators. We'll add the remainder of the
134+
// string outside the loop as the last component, then return.
135+
break
136+
}
137+
}
138+
}
139+
result.append(string[componentStartIndex...])
140+
141+
return result
142+
}
143+
98144
extension TypeInfo {
99145
/// An in-memory cache of fully-qualified type name components.
100146
private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>()
@@ -106,27 +152,14 @@ extension TypeInfo {
106152
///
107153
/// - Returns: The components of `fullyQualifiedName` as substrings thereof.
108154
static func fullyQualifiedNameComponents(ofTypeWithName fullyQualifiedName: String) -> [String] {
109-
var components = [Substring]()
110-
111-
var inRawIdentifier = false
112-
var componentStartIndex = fullyQualifiedName.startIndex
113-
for i in fullyQualifiedName.indices {
114-
let c = fullyQualifiedName[i]
115-
if c == "`" {
116-
inRawIdentifier.toggle()
117-
} else if c == "." && !inRawIdentifier {
118-
components.append(fullyQualifiedName[componentStartIndex ..< i])
119-
componentStartIndex = fullyQualifiedName.index(after: i)
120-
}
121-
}
122-
components.append(fullyQualifiedName[componentStartIndex...])
155+
var components = rawIdentifierAwareSplit(fullyQualifiedName, separator: ".")
123156

124157
// If a type is extended in another module and then referenced by name,
125158
// its name according to the String(reflecting:) API will be prefixed with
126159
// "(extension in MODULE_NAME):". For our purposes, we never want to
127160
// preserve that prefix.
128161
if let firstComponent = components.first, firstComponent.starts(with: "(extension in "),
129-
let moduleName = firstComponent.split(separator: ":", maxSplits: 1).last {
162+
let moduleName = rawIdentifierAwareSplit(firstComponent, separator: ":", maxSplits: 1).last {
130163
// NOTE: even if the module name is a raw identifier, it comprises a
131164
// single identifier (no splitting required) so we don't need to process
132165
// it any further.

Sources/Testing/SourceAttribution/SourceLocation.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,7 @@ public struct SourceLocation: Sendable {
6767
/// - ``fileName``
6868
/// - [`#fileID`](https://developer.apple.com/documentation/swift/fileID())
6969
public var moduleName: String {
70-
var inRawIdentifier = false
71-
for i in fileID.indices {
72-
let c = fileID[i]
73-
if c == "`" {
74-
inRawIdentifier.toggle()
75-
} else if c == "/" && !inRawIdentifier {
76-
return String(fileID[..<i])
77-
}
78-
}
79-
80-
// Normally unreachable.
81-
fatalError("Could not find the end of the module name of Swift file ID '\(fileID)'.")
70+
rawIdentifierAwareSplit(fileID, separator: "/", maxSplits: 1).first.map(String.init)!
8271
}
8372

8473
/// The path to the source file.

Tests/TestingTests/TypeInfoTests.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ struct TypeInfoTests {
6464
("(extension in Module):Foo.`B.ar`.(unknown context at $0).Quux", ["Foo", "`B.ar`", "Quux"]),
6565
("(extension in `Module`):Foo.`B.ar`.(unknown context at $0).Quux", ["Foo", "`B.ar`", "Quux"]),
6666
("(extension in `Module`):`Foo`.`B.ar`.(unknown context at $0).Quux", ["`Foo`", "`B.ar`", "Quux"]),
67+
("(extension in `Mo:dule`):`Foo`.`B.ar`.(unknown context at $0).Quux", ["`Foo`", "`B.ar`", "Quux"]),
68+
("(extension in `Module`):`F:oo`.`B.ar`.(unknown context at $0).Quux", ["`F:oo`", "`B.ar`", "Quux"]),
6769
6870
// These aren't syntactically valid, but we should at least not crash.
6971
("Foo.`B.ar`.Quux.`Alpha`..Beta", ["Foo", "`B.ar`", "Quux", "`Alpha`", "", "Beta"]),

0 commit comments

Comments
 (0)