Skip to content

Commit 8640814

Browse files
committed
Add support for raw identifiers.
This PR adds support for the raw identifiers feature introduced with [SE-0451](https://forums.swift.org/t/accepted-with-revision-se-0451-raw-identifiers/76387). At this time, I _think_ I don't need to make any changes to our macro expansion code for it to compile and run, however I may need to revisit later in order to treat functions with raw identifiers as having "display names" and to synthesize test IDs for them that are concise (CRC-32 to the rescue?) We'll cross that bridge when we come to it, after the feature has landed in the toolchain. Resolves #842.
1 parent aee0821 commit 8640814

File tree

4 files changed

+121
-20
lines changed

4 files changed

+121
-20
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public struct TypeInfo: Sendable {
6969
/// - mangled: The mangled name of the type, if available.
7070
init(fullyQualifiedName: String, unqualifiedName: String, mangledName: String?) {
7171
self.init(
72-
fullyQualifiedNameComponents: fullyQualifiedName.split(separator: ".").map(String.init),
72+
fullyQualifiedNameComponents: Self.fullyQualifiedNameComponents(ofTypeWithName: fullyQualifiedName),
7373
unqualifiedName: unqualifiedName,
7474
mangledName: mangledName
7575
)
@@ -99,6 +99,48 @@ extension TypeInfo {
9999
/// An in-memory cache of fully-qualified type name components.
100100
private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>()
101101

102+
/// Split the given fully-qualified type name into its components.
103+
///
104+
/// - Parameters:
105+
/// - fullyQualifiedName: The string to split.
106+
///
107+
/// - Returns: The components of `fullyQualifiedName` as substrings thereof.
108+
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...])
123+
124+
// If a type is extended in another module and then referenced by name,
125+
// its name according to the String(reflecting:) API will be prefixed with
126+
// "(extension in MODULE_NAME):". For our purposes, we never want to
127+
// preserve that prefix.
128+
if let firstComponent = components.first, firstComponent.starts(with: "(extension in "),
129+
let moduleName = firstComponent.split(separator: ":", maxSplits: 1).last {
130+
// NOTE: even if the module name is a raw identifier, it comprises a
131+
// single identifier (no splitting required) so we don't need to process
132+
// it any further.
133+
components[0] = moduleName
134+
}
135+
136+
// If a type is private or embedded in a function, its fully qualified
137+
// name may include "(unknown context at $xxxxxxxx)" as a component. Strip
138+
// those out as they're uninteresting to us.
139+
components = components.filter { !$0.starts(with: "(unknown context at") }
140+
141+
return components.map(String.init)
142+
}
143+
102144
/// The complete name of this type, with the names of all referenced types
103145
/// fully-qualified by their module names when possible.
104146
///
@@ -121,22 +163,7 @@ extension TypeInfo {
121163
return cachedResult
122164
}
123165

124-
var result = String(reflecting: type)
125-
.split(separator: ".")
126-
.map(String.init)
127-
128-
// If a type is extended in another module and then referenced by name,
129-
// its name according to the String(reflecting:) API will be prefixed with
130-
// "(extension in MODULE_NAME):". For our purposes, we never want to
131-
// preserve that prefix.
132-
if let firstComponent = result.first, firstComponent.starts(with: "(extension in ") {
133-
result[0] = String(firstComponent.split(separator: ":", maxSplits: 1).last!)
134-
}
135-
136-
// If a type is private or embedded in a function, its fully qualified
137-
// name may include "(unknown context at $xxxxxxxx)" as a component. Strip
138-
// those out as they're uninteresting to us.
139-
result = result.filter { !$0.starts(with: "(unknown context at") }
166+
let result = Self.fullyQualifiedNameComponents(ofTypeWithName: String(reflecting: type))
140167

141168
Self._fullyQualifiedNameComponentsCache.withLock { fullyQualifiedNameComponentsCache in
142169
fullyQualifiedNameComponentsCache[ObjectIdentifier(type)] = result

Sources/Testing/SourceAttribution/SourceLocation.swift

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public struct SourceLocation: Sendable {
4646
/// - ``moduleName``
4747
public var fileName: String {
4848
let lastSlash = fileID.lastIndex(of: "/")!
49-
return String(fileID[fileID.index(after: lastSlash)...])
49+
return String(fileID[lastSlash...].dropFirst())
5050
}
5151

5252
/// The name of the module containing the source file.
@@ -67,8 +67,18 @@ public struct SourceLocation: Sendable {
6767
/// - ``fileName``
6868
/// - [`#fileID`](https://developer.apple.com/documentation/swift/fileID())
6969
public var moduleName: String {
70-
let firstSlash = fileID.firstIndex(of: "/")!
71-
return String(fileID[..<firstSlash])
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)'.")
7282
}
7383

7484
/// The path to the source file.

Tests/TestingTests/SourceLocationTests.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,20 @@ struct SourceLocationTests {
4444
#expect(sourceLocation.moduleName == "FakeModule")
4545
}
4646

47+
@Test("SourceLocation.moduleName property with raw identifier",
48+
arguments: [
49+
("Foo/Bar.swift", "Foo", "Bar.swift"),
50+
("`Foo`/Bar.swift", "`Foo`", "Bar.swift"),
51+
("`Foo.Bar`/Quux.swift", "`Foo.Bar`", "Quux.swift"),
52+
("`Foo./.Bar`/Quux.swift", "`Foo./.Bar`", "Quux.swift"),
53+
]
54+
)
55+
func sourceLocationModuleNameWithRawIdentifier(fileID: String, expectedModuleName: String, expectedFileName: String) throws {
56+
let sourceLocation = SourceLocation(fileID: fileID, filePath: "", line: 1, column: 1)
57+
#expect(sourceLocation.moduleName == expectedModuleName)
58+
#expect(sourceLocation.fileName == expectedFileName)
59+
}
60+
4761
@Test("SourceLocation.fileID property ignores middle components")
4862
func sourceLocationFileIDMiddleIgnored() {
4963
let sourceLocation = SourceLocation(fileID: "A/B/C/D.swift", filePath: "", line: 1, column: 1)

Tests/TestingTests/TypeInfoTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,56 @@ struct TypeInfoTests {
5050
#expect(TypeInfo(describing: T.self).fullyQualifiedName == "(Swift.Int, Swift.String) -> Swift.Bool")
5151
}
5252

53+
@Test("Splitting raw identifiers",
54+
arguments: [
55+
("Foo.Bar", ["Foo", "Bar"]),
56+
("`Foo`.Bar", ["`Foo`", "Bar"]),
57+
("`Foo`.`Bar`", ["`Foo`", "`Bar`"]),
58+
("Foo.`Bar`", ["Foo", "`Bar`"]),
59+
("Foo.`Bar`.Quux", ["Foo", "`Bar`", "Quux"]),
60+
("Foo.`B.ar`.Quux", ["Foo", "`B.ar`", "Quux"]),
61+
62+
// These have substrings we intentionally strip out.
63+
("Foo.`B.ar`.(unknown context at $0).Quux", ["Foo", "`B.ar`", "Quux"]),
64+
("(extension in Module):Foo.`B.ar`.(unknown context at $0).Quux", ["Foo", "`B.ar`", "Quux"]),
65+
("(extension in `Module`):Foo.`B.ar`.(unknown context at $0).Quux", ["Foo", "`B.ar`", "Quux"]),
66+
("(extension in `Module`):`Foo`.`B.ar`.(unknown context at $0).Quux", ["`Foo`", "`B.ar`", "Quux"]),
67+
68+
// These aren't syntactically valid, but we should at least not crash.
69+
("Foo.`B.ar`.Quux.`Alpha`..Beta", ["Foo", "`B.ar`", "Quux", "`Alpha`", "", "Beta"]),
70+
("Foo.`B.ar`.Quux.`Alpha", ["Foo", "`B.ar`", "Quux", "`Alpha"]),
71+
("Foo.`B.ar`.Quux.`Alpha``", ["Foo", "`B.ar`", "Quux", "`Alpha``"]),
72+
("Foo.`B.ar`.Quux.`Alpha...", ["Foo", "`B.ar`", "Quux", "`Alpha..."]),
73+
]
74+
)
75+
func rawIdentifiers(fqn: String, expectedComponents: [String]) throws {
76+
let actualComponents = TypeInfo.fullyQualifiedNameComponents(ofTypeWithName: fqn)
77+
#expect(expectedComponents == actualComponents)
78+
}
79+
80+
// As above, but round-tripping through .fullyQualifiedName.
81+
@Test("Round-tripping raw identifiers",
82+
arguments: [
83+
("Foo.Bar", ["Foo", "Bar"]),
84+
("`Foo`.Bar", ["`Foo`", "Bar"]),
85+
("`Foo`.`Bar`", ["`Foo`", "`Bar`"]),
86+
("Foo.`Bar`", ["Foo", "`Bar`"]),
87+
("Foo.`Bar`.Quux", ["Foo", "`Bar`", "Quux"]),
88+
("Foo.`B.ar`.Quux", ["Foo", "`B.ar`", "Quux"]),
89+
90+
// These aren't syntactically valid, but we should at least not crash.
91+
("Foo.`B.ar`.Quux.`Alpha`..Beta", ["Foo", "`B.ar`", "Quux", "`Alpha`", "", "Beta"]),
92+
("Foo.`B.ar`.Quux.`Alpha", ["Foo", "`B.ar`", "Quux", "`Alpha"]),
93+
("Foo.`B.ar`.Quux.`Alpha``", ["Foo", "`B.ar`", "Quux", "`Alpha``"]),
94+
("Foo.`B.ar`.Quux.`Alpha...", ["Foo", "`B.ar`", "Quux", "`Alpha..."]),
95+
]
96+
)
97+
func roundTrippedRawIdentifiers(fqn: String, expectedComponents: [String]) throws {
98+
let typeInfo = TypeInfo(fullyQualifiedName: fqn, unqualifiedName: "", mangledName: "")
99+
#expect(typeInfo.fullyQualifiedName == fqn)
100+
#expect(typeInfo.fullyQualifiedNameComponents == expectedComponents)
101+
}
102+
53103
@available(_mangledTypeNameAPI, *)
54104
@Test func mangledTypeName() {
55105
#expect(_mangledTypeName(String.self) == TypeInfo(describing: String.self).mangledName)

0 commit comments

Comments
 (0)