Skip to content

Synthesize display names for de facto suites with raw identifiers. #1105

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 50 additions & 8 deletions Sources/Testing/Parameterization/TypeInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,34 @@ func rawIdentifierAwareSplit<S>(_ 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important: My experience has led me to operate on Unicode scalar views in situations like this where I don't need grapheme clustering so I don't pay the extra cost of it. It's probably not going to be a big bottleneck here, but that's just my habit 🤷🏻‍♂️

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]]>()

Expand All @@ -166,12 +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") }

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
Expand Down Expand Up @@ -242,9 +279,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
}
}

Expand Down
9 changes: 7 additions & 2 deletions Sources/Testing/Test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
12 changes: 6 additions & 6 deletions Tests/TestingMacrosTests/TestDeclarationMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"))")]
),
]
),
Expand Down Expand Up @@ -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",
Expand Down
18 changes: 14 additions & 4 deletions Tests/TestingTests/MiscellaneousTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) && 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)
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()
Expand Down