diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index b0ed814b9..300004e16 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -142,6 +142,35 @@ func rawIdentifierAwareSplit(_ string: S, separator: Character, maxSplits: In } extension TypeInfo { + /// Replace any non-breaking spaces in the given string with normal spaces. + /// + /// - Parameters: + /// - rawIdentifier: The string to rewrite. + /// + /// - Returns: A copy of `rawIdentifier` with non-breaking spaces (`U+00A0`) + /// replaced with normal spaces (`U+0020`). + /// + /// When the Swift runtime demangles a raw identifier, it [replaces](https://github.com/swiftlang/swift/blob/d033eec1aa427f40dcc38679d43b83d9dbc06ae7/lib/Basic/Mangler.cpp#L250) + /// normal ASCII spaces with non-breaking spaces to maintain compatibility + /// with historical usages of spaces in mangled name forms. Non-breaking + /// spaces are not otherwise valid in raw identifiers, so this transformation + /// is reversible. + private static func _rewriteNonBreakingSpacesAsASCIISpaces(in rawIdentifier: some StringProtocol) -> String? { + let nbsp = "\u{00A0}" as UnicodeScalar + + // If there are no non-breaking spaces in the string, exit early to avoid + // any further allocations. + let unicodeScalars = rawIdentifier.unicodeScalars + guard unicodeScalars.contains(nbsp) else { + return nil + } + + // Replace non-breaking spaces, then construct a new string from the + // resulting sequence. + let result = unicodeScalars.lazy.map { $0 == nbsp ? " " : $0 } + return String(String.UnicodeScalarView(result)) + } + /// An in-memory cache of fully-qualified type name components. private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>() @@ -166,12 +195,21 @@ extension TypeInfo { components[0] = moduleName } - // If a type is private or embedded in a function, its fully qualified - // name may include "(unknown context at $xxxxxxxx)" as a component. Strip - // those out as they're uninteresting to us. - components = components.filter { !$0.starts(with: "(unknown context at") } - - return components.map(String.init) + return components.lazy + .filter { component in + // If a type is private or embedded in a function, its fully qualified + // name may include "(unknown context at $xxxxxxxx)" as a component. + // Strip those out as they're uninteresting to us. + !component.starts(with: "(unknown context at") + }.map { component in + // Replace non-breaking spaces with spaces. See the helper function's + // documentation for more information. + if let component = _rewriteNonBreakingSpacesAsASCIISpaces(in: component) { + component[...] + } else { + component + } + }.map(String.init) } /// The complete name of this type, with the names of all referenced types @@ -242,9 +280,14 @@ extension TypeInfo { public var unqualifiedName: String { switch _kind { case let .type(type): - String(describing: type) + // Replace non-breaking spaces with spaces. See the helper function's + // documentation for more information. + var result = String(describing: type) + result = Self._rewriteNonBreakingSpacesAsASCIISpaces(in: result) ?? result + + return result case let .nameOnly(_, unqualifiedName, _): - unqualifiedName + return unqualifiedName } } diff --git a/Sources/Testing/Test.swift b/Sources/Testing/Test.swift index 738daf72d..5f2ac2406 100644 --- a/Sources/Testing/Test.swift +++ b/Sources/Testing/Test.swift @@ -209,8 +209,13 @@ public struct Test: Sendable { containingTypeInfo: TypeInfo, isSynthesized: Bool = false ) { - self.name = containingTypeInfo.unqualifiedName - self.displayName = displayName + let name = containingTypeInfo.unqualifiedName + self.name = name + if let displayName { + self.displayName = displayName + } else if isSynthesized && name.count > 2 && name.first == "`" && name.last == "`" { + self.displayName = String(name.dropFirst().dropLast()) + } self.traits = traits self.sourceLocation = sourceLocation self.containingTypeInfo = containingTypeInfo diff --git a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift index 447a18dee..26b9d1923 100644 --- a/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift +++ b/Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift @@ -39,12 +39,6 @@ extension TokenSyntax { return textWithoutBackticks } - // TODO: remove this mock path once the toolchain fully supports raw IDs. - let mockPrefix = "__raw__$" - if backticksRemoved, textWithoutBackticks.starts(with: mockPrefix) { - return String(textWithoutBackticks.dropFirst(mockPrefix.count)) - } - return nil } } diff --git a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift index 13ae3d180..6c04eb9eb 100644 --- a/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift +++ b/Tests/TestingMacrosTests/TestDeclarationMacroTests.swift @@ -217,17 +217,17 @@ struct TestDeclarationMacroTests { ] ), - #"@Test("Goodbye world") func `__raw__$helloWorld`()"#: + #"@Test("Goodbye world") func `hello world`()"#: ( - message: "Attribute 'Test' specifies display name 'Goodbye world' for function with implicit display name 'helloWorld'", + message: "Attribute 'Test' specifies display name 'Goodbye world' for function with implicit display name 'hello world'", fixIts: [ ExpectedFixIt( message: "Remove 'Goodbye world'", changes: [.replace(oldSourceCode: #""Goodbye world""#, newSourceCode: "")] ), ExpectedFixIt( - message: "Rename '__raw__$helloWorld'", - changes: [.replace(oldSourceCode: "`__raw__$helloWorld`", newSourceCode: "\(EditorPlaceholderExprSyntax("name"))")] + message: "Rename 'hello world'", + changes: [.replace(oldSourceCode: "`hello world`", newSourceCode: "\(EditorPlaceholderExprSyntax("name"))")] ), ] ), @@ -281,10 +281,10 @@ struct TestDeclarationMacroTests { @Test("Raw function name components") func rawFunctionNameComponents() throws { let decl = """ - func `__raw__$hello`(`__raw__$world`: T, etc: U, `blah`: V) {} + func `hello there`(`world of mine`: T, etc: U, `blah`: V) {} """ as DeclSyntax let functionDecl = try #require(decl.as(FunctionDeclSyntax.self)) - #expect(functionDecl.completeName.trimmedDescription == "`hello`(`world`:etc:blah:)") + #expect(functionDecl.completeName.trimmedDescription == "`hello there`(`world of mine`:etc:blah:)") } @Test("Warning diagnostics emitted on API misuse", diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 9ae326afe..b895f6c1b 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -297,15 +297,26 @@ struct MiscellaneousTests { #expect(testType.displayName == "Named Sendable test type") } - @Test func `__raw__$raw_identifier_provides_a_display_name`() throws { +#if compiler(>=6.2) && hasFeature(RawIdentifiers) + @Test func `Test with raw identifier gets a display name`() throws { let test = try #require(Test.current) - #expect(test.displayName == "raw_identifier_provides_a_display_name") - #expect(test.name == "`raw_identifier_provides_a_display_name`()") + #expect(test.displayName == "Test with raw identifier gets a display name") + #expect(test.name == "`Test with raw identifier gets a display name`()") let id = test.id #expect(id.moduleName == "TestingTests") - #expect(id.nameComponents == ["MiscellaneousTests", "`raw_identifier_provides_a_display_name`()"]) + #expect(id.nameComponents == ["MiscellaneousTests", "`Test with raw identifier gets a display name`()"]) } + @Test func `Suite type with raw identifier gets a display name`() throws { + struct `Suite With De Facto Display Name` {} + let typeInfo = TypeInfo(describing: `Suite With De Facto Display Name`.self) + let suite = Test(traits: [], sourceLocation: #_sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true) + #expect(suite.name == "`Suite With De Facto Display Name`") + let displayName = try #require(suite.displayName) + #expect(displayName == "Suite With De Facto Display Name") + } +#endif + @Test("Free functions are runnable") func freeFunction() async throws { await Test(testFunction: freeSyncFunction).run()