From 0059b5db8d5d4b5c43713abd9f6fb1a635e3c908 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sat, 3 May 2025 23:00:40 -0400 Subject: [PATCH 1/4] 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. --- .../Testing/Parameterization/TypeInfo.swift | 46 ++++++++++++++++++- Sources/Testing/Test.swift | 9 +++- .../Additions/TokenSyntaxAdditions.swift | 6 --- .../TestDeclarationMacroTests.swift | 12 ++--- Tests/TestingTests/MiscellaneousTests.swift | 18 ++++++-- 5 files changed, 71 insertions(+), 20 deletions(-) diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index b0ed814b9..b550e4c32 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -142,6 +142,34 @@ 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? { + guard rawIdentifier.contains("\u{00A0}") else { + return nil + } + + let result = rawIdentifier.lazy.map { c in + if c == "\u{00A0}" { + " " as Character + } else { + c + } + } + return String(result) + } + /// An in-memory cache of fully-qualified type name components. private static let _fullyQualifiedNameComponentsCache = Locked<[ObjectIdentifier: [String]]>() @@ -171,6 +199,16 @@ extension TypeInfo { // those out as they're uninteresting to us. components = components.filter { !$0.starts(with: "(unknown context at") } + // Replace non-breaking spaces with spaces. See the helper function's + // documentation for more information. + components = components.map { component in + if let component = _rewriteNonBreakingSpacesAsASCIISpaces(in: component) { + component[...] + } else { + component + } + } + return components.map(String.init) } @@ -242,9 +280,13 @@ 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 b4b12a217..6a33eb9a4 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -293,15 +293,25 @@ struct MiscellaneousTests { #expect(testType.displayName == "Named Sendable test type") } - @Test func `__raw__$raw_identifier_provides_a_display_name`() throws { +#if compiler(>=6.2) + @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) + let displayName = try #require(suite.displayName) + #expect(Array(displayName.unicodeScalars) == Array("Suite With De Facto Display Name".unicodeScalars)) + } +#endif + @Test("Free functions are runnable") func freeFunction() async throws { await Test(testFunction: freeSyncFunction).run() From d3425a5b0e19556927cafe3e105dc79c86f910af Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 4 May 2025 21:18:12 -0400 Subject: [PATCH 2/4] Disable tests for raw identifiers if the feature is not present --- Tests/TestingTests/MiscellaneousTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 6a33eb9a4..97f01f864 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -293,7 +293,7 @@ struct MiscellaneousTests { #expect(testType.displayName == "Named Sendable test type") } -#if compiler(>=6.2) +#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 == "Test with raw identifier gets a display name") From 2a4cf81d01f960e9e0f4ab945ab1e78517df5a5b Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 4 May 2025 21:21:53 -0400 Subject: [PATCH 3/4] Don't need this silly Unicode dance --- Tests/TestingTests/MiscellaneousTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/TestingTests/MiscellaneousTests.swift b/Tests/TestingTests/MiscellaneousTests.swift index 97f01f864..20046525c 100644 --- a/Tests/TestingTests/MiscellaneousTests.swift +++ b/Tests/TestingTests/MiscellaneousTests.swift @@ -308,7 +308,7 @@ struct MiscellaneousTests { let typeInfo = TypeInfo(describing: `Suite With De Facto Display Name`.self) let suite = Test(traits: [], sourceLocation: #_sourceLocation, containingTypeInfo: typeInfo, isSynthesized: true) let displayName = try #require(suite.displayName) - #expect(Array(displayName.unicodeScalars) == Array("Suite With De Facto Display Name".unicodeScalars)) + #expect(displayName == "Suite With De Facto Display Name") } #endif From b2e86486051fa8f8fd8b58484315fdfa80e1fd12 Mon Sep 17 00:00:00 2001 From: Jonathan Grynspan Date: Sun, 4 May 2025 21:40:59 -0400 Subject: [PATCH 4/4] Functionalize transformations of FQN components --- .../Testing/Parameterization/TypeInfo.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/Testing/Parameterization/TypeInfo.swift b/Sources/Testing/Parameterization/TypeInfo.swift index b550e4c32..cd66bb3bc 100644 --- a/Sources/Testing/Parameterization/TypeInfo.swift +++ b/Sources/Testing/Parameterization/TypeInfo.swift @@ -194,22 +194,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") } - - // Replace non-breaking spaces with spaces. See the helper function's - // documentation for more information. - components = components.map { component in - if let component = _rewriteNonBreakingSpacesAsASCIISpaces(in: component) { - component[...] - } else { - component - } - } - - 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 @@ -284,6 +283,7 @@ extension TypeInfo { // documentation for more information. var result = String(describing: type) result = Self._rewriteNonBreakingSpacesAsASCIISpaces(in: result) ?? result + return result case let .nameOnly(_, unqualifiedName, _): return unqualifiedName