Skip to content

Commit 0059b5d

Browse files
committed
Synthesize display names for de facto suites with raw identifiers.
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.
1 parent 6e4fe1f commit 0059b5d

File tree

5 files changed

+71
-20
lines changed

5 files changed

+71
-20
lines changed

Sources/Testing/Parameterization/TypeInfo.swift

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,34 @@ 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+
guard rawIdentifier.contains("\u{00A0}") else {
160+
return nil
161+
}
162+
163+
let result = rawIdentifier.lazy.map { c in
164+
if c == "\u{00A0}" {
165+
" " as Character
166+
} else {
167+
c
168+
}
169+
}
170+
return String(result)
171+
}
172+
145173
/// An in-memory cache of fully-qualified type name components.
146174
private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>()
147175

@@ -171,6 +199,16 @@ extension TypeInfo {
171199
// those out as they're uninteresting to us.
172200
components = components.filter { !$0.starts(with: "(unknown context at") }
173201

202+
// Replace non-breaking spaces with spaces. See the helper function's
203+
// documentation for more information.
204+
components = components.map { component in
205+
if let component = _rewriteNonBreakingSpacesAsASCIISpaces(in: component) {
206+
component[...]
207+
} else {
208+
component
209+
}
210+
}
211+
174212
return components.map(String.init)
175213
}
176214

@@ -242,9 +280,13 @@ 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+
return result
246288
case let .nameOnly(_, unqualifiedName, _):
247-
unqualifiedName
289+
return unqualifiedName
248290
}
249291
}
250292

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: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,15 +293,25 @@ struct MiscellaneousTests {
293293
#expect(testType.displayName == "Named Sendable test type")
294294
}
295295

296-
@Test func `__raw__$raw_identifier_provides_a_display_name`() throws {
296+
#if compiler(>=6.2)
297+
@Test func `Test with raw identifier gets a display name`() throws {
297298
let test = try #require(Test.current)
298-
#expect(test.displayName == "raw_identifier_provides_a_display_name")
299-
#expect(test.name == "`raw_identifier_provides_a_display_name`()")
299+
#expect(test.displayName == "Test with raw identifier gets a display name")
300+
#expect(test.name == "`Test with raw identifier gets a display name`()")
300301
let id = test.id
301302
#expect(id.moduleName == "TestingTests")
302-
#expect(id.nameComponents == ["MiscellaneousTests", "`raw_identifier_provides_a_display_name`()"])
303+
#expect(id.nameComponents == ["MiscellaneousTests", "`Test with raw identifier gets a display name`()"])
303304
}
304305

306+
@Test func `Suite type with raw identifier gets a display name`() throws {
307+
struct `Suite With De Facto Display Name` {}
308+
let typeInfo = TypeInfo(describing: `Suite With De Facto Display Name`.self)
309+
let suite = Test(traits: [], sourceLocation: #_sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true)
310+
let displayName = try #require(suite.displayName)
311+
#expect(Array(displayName.unicodeScalars) == Array("Suite With De Facto Display Name".unicodeScalars))
312+
}
313+
#endif
314+
305315
@Test("Free functions are runnable")
306316
func freeFunction() async throws {
307317
await Test(testFunction: freeSyncFunction).run()

0 commit comments

Comments
 (0)