Skip to content

Commit 6e462ad

Browse files
authored
Synthesize display names for de facto suites with raw identifiers. (#1105)
This PR ensures that suite types that don't have the `@Suite` attribute but which _do_ have raw identifiers for names are correctly given display names the same way those with `@Suite` would be. This PR also ensures that we transform spaces in raw identifiers after they are demangled by the runtime--namely, the runtime replaces ASCII spaces (as typed by the user) with Unicode non-breaking spaces (which aren't otherwise valid in raw identifers) in order to avoid issues with existing uses of spaces in demangled names. We want to make sure that identifiers as presented to the user match what the user has typed, so we need to transform these spaces back. No changes in this area are needed for display names derived during macro expansion because we do the relevant work based on the source text which still has the original ASCII spaces. This PR also deletes the "`raw$`" hack that I put in place when originally implementing raw identifier support as the entire toolchain supports them now. Resolves #1104. ### Checklist: - [x] Code and documentation should follow the style of the [Style Guide](https://github.com/apple/swift-testing/blob/main/Documentation/StyleGuide.md). - [x] If public symbols are renamed or modified, DocC references should be updated.
1 parent 3278217 commit 6e462ad

File tree

5 files changed

+79
-26
lines changed

5 files changed

+79
-26
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,35 @@ func rawIdentifierAwareSplit<S>(_ string: S, separator: Character, maxSplits: In
142142
}
143143

144144
extension TypeInfo {
145+
/// Replace any non-breaking spaces in the given string with normal spaces.
146+
///
147+
/// - Parameters:
148+
/// - rawIdentifier: The string to rewrite.
149+
///
150+
/// - Returns: A copy of `rawIdentifier` with non-breaking spaces (`U+00A0`)
151+
/// replaced with normal spaces (`U+0020`).
152+
///
153+
/// When the Swift runtime demangles a raw identifier, it [replaces](https://github.com/swiftlang/swift/blob/d033eec1aa427f40dcc38679d43b83d9dbc06ae7/lib/Basic/Mangler.cpp#L250)
154+
/// normal ASCII spaces with non-breaking spaces to maintain compatibility
155+
/// with historical usages of spaces in mangled name forms. Non-breaking
156+
/// spaces are not otherwise valid in raw identifiers, so this transformation
157+
/// is reversible.
158+
private static func _rewriteNonBreakingSpacesAsASCIISpaces(in rawIdentifier: some StringProtocol) -> String? {
159+
let nbsp = "\u{00A0}" as UnicodeScalar
160+
161+
// If there are no non-breaking spaces in the string, exit early to avoid
162+
// any further allocations.
163+
let unicodeScalars = rawIdentifier.unicodeScalars
164+
guard unicodeScalars.contains(nbsp) else {
165+
return nil
166+
}
167+
168+
// Replace non-breaking spaces, then construct a new string from the
169+
// resulting sequence.
170+
let result = unicodeScalars.lazy.map { $0 == nbsp ? " " : $0 }
171+
return String(String.UnicodeScalarView(result))
172+
}
173+
145174
/// An in-memory cache of fully-qualified type name components.
146175
private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>()
147176

@@ -166,12 +195,21 @@ extension TypeInfo {
166195
components[0] = moduleName
167196
}
168197

169-
// If a type is private or embedded in a function, its fully qualified
170-
// name may include "(unknown context at $xxxxxxxx)" as a component. Strip
171-
// those out as they're uninteresting to us.
172-
components = components.filter { !$0.starts(with: "(unknown context at") }
173-
174-
return components.map(String.init)
198+
return components.lazy
199+
.filter { component in
200+
// If a type is private or embedded in a function, its fully qualified
201+
// name may include "(unknown context at $xxxxxxxx)" as a component.
202+
// Strip those out as they're uninteresting to us.
203+
!component.starts(with: "(unknown context at")
204+
}.map { component in
205+
// Replace non-breaking spaces with spaces. See the helper function's
206+
// documentation for more information.
207+
if let component = _rewriteNonBreakingSpacesAsASCIISpaces(in: component) {
208+
component[...]
209+
} else {
210+
component
211+
}
212+
}.map(String.init)
175213
}
176214

177215
/// The complete name of this type, with the names of all referenced types
@@ -242,9 +280,14 @@ extension TypeInfo {
242280
public var unqualifiedName: String {
243281
switch _kind {
244282
case let .type(type):
245-
String(describing: type)
283+
// Replace non-breaking spaces with spaces. See the helper function's
284+
// documentation for more information.
285+
var result = String(describing: type)
286+
result = Self._rewriteNonBreakingSpacesAsASCIISpaces(in: result) ?? result
287+
288+
return result
246289
case let .nameOnly(_, unqualifiedName, _):
247-
unqualifiedName
290+
return unqualifiedName
248291
}
249292
}
250293

Sources/Testing/Test.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,13 @@ public struct Test: Sendable {
209209
containingTypeInfo: TypeInfo,
210210
isSynthesized: Bool = false
211211
) {
212-
self.name = containingTypeInfo.unqualifiedName
213-
self.displayName = displayName
212+
let name = containingTypeInfo.unqualifiedName
213+
self.name = name
214+
if let displayName {
215+
self.displayName = displayName
216+
} else if isSynthesized && name.count > 2 && name.first == "`" && name.last == "`" {
217+
self.displayName = String(name.dropFirst().dropLast())
218+
}
214219
self.traits = traits
215220
self.sourceLocation = sourceLocation
216221
self.containingTypeInfo = containingTypeInfo

Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,6 @@ extension TokenSyntax {
3939
return textWithoutBackticks
4040
}
4141

42-
// TODO: remove this mock path once the toolchain fully supports raw IDs.
43-
let mockPrefix = "__raw__$"
44-
if backticksRemoved, textWithoutBackticks.starts(with: mockPrefix) {
45-
return String(textWithoutBackticks.dropFirst(mockPrefix.count))
46-
}
47-
4842
return nil
4943
}
5044
}

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,17 @@ struct TestDeclarationMacroTests {
217217
]
218218
),
219219

220-
#"@Test("Goodbye world") func `__raw__$helloWorld`()"#:
220+
#"@Test("Goodbye world") func `hello world`()"#:
221221
(
222-
message: "Attribute 'Test' specifies display name 'Goodbye world' for function with implicit display name 'helloWorld'",
222+
message: "Attribute 'Test' specifies display name 'Goodbye world' for function with implicit display name 'hello world'",
223223
fixIts: [
224224
ExpectedFixIt(
225225
message: "Remove 'Goodbye world'",
226226
changes: [.replace(oldSourceCode: #""Goodbye world""#, newSourceCode: "")]
227227
),
228228
ExpectedFixIt(
229-
message: "Rename '__raw__$helloWorld'",
230-
changes: [.replace(oldSourceCode: "`__raw__$helloWorld`", newSourceCode: "\(EditorPlaceholderExprSyntax("name"))")]
229+
message: "Rename 'hello world'",
230+
changes: [.replace(oldSourceCode: "`hello world`", newSourceCode: "\(EditorPlaceholderExprSyntax("name"))")]
231231
),
232232
]
233233
),
@@ -281,10 +281,10 @@ struct TestDeclarationMacroTests {
281281
@Test("Raw function name components")
282282
func rawFunctionNameComponents() throws {
283283
let decl = """
284-
func `__raw__$hello`(`__raw__$world`: T, etc: U, `blah`: V) {}
284+
func `hello there`(`world of mine`: T, etc: U, `blah`: V) {}
285285
""" as DeclSyntax
286286
let functionDecl = try #require(decl.as(FunctionDeclSyntax.self))
287-
#expect(functionDecl.completeName.trimmedDescription == "`hello`(`world`:etc:blah:)")
287+
#expect(functionDecl.completeName.trimmedDescription == "`hello there`(`world of mine`:etc:blah:)")
288288
}
289289

290290
@Test("Warning diagnostics emitted on API misuse",

Tests/TestingTests/MiscellaneousTests.swift

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,15 +297,26 @@ struct MiscellaneousTests {
297297
#expect(testType.displayName == "Named Sendable test type")
298298
}
299299

300-
@Test func `__raw__$raw_identifier_provides_a_display_name`() throws {
300+
#if compiler(>=6.2) && hasFeature(RawIdentifiers)
301+
@Test func `Test with raw identifier gets a display name`() throws {
301302
let test = try #require(Test.current)
302-
#expect(test.displayName == "raw_identifier_provides_a_display_name")
303-
#expect(test.name == "`raw_identifier_provides_a_display_name`()")
303+
#expect(test.displayName == "Test with raw identifier gets a display name")
304+
#expect(test.name == "`Test with raw identifier gets a display name`()")
304305
let id = test.id
305306
#expect(id.moduleName == "TestingTests")
306-
#expect(id.nameComponents == ["MiscellaneousTests", "`raw_identifier_provides_a_display_name`()"])
307+
#expect(id.nameComponents == ["MiscellaneousTests", "`Test with raw identifier gets a display name`()"])
307308
}
308309

310+
@Test func `Suite type with raw identifier gets a display name`() throws {
311+
struct `Suite With De Facto Display Name` {}
312+
let typeInfo = TypeInfo(describing: `Suite With De Facto Display Name`.self)
313+
let suite = Test(traits: [], sourceLocation: #_sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true)
314+
#expect(suite.name == "`Suite With De Facto Display Name`")
315+
let displayName = try #require(suite.displayName)
316+
#expect(displayName == "Suite With De Facto Display Name")
317+
}
318+
#endif
319+
309320
@Test("Free functions are runnable")
310321
func freeFunction() async throws {
311322
await Test(testFunction: freeSyncFunction).run()

0 commit comments

Comments
 (0)