Skip to content

Commit 7316f01

Browse files
committed
Derive a display name from a raw identifier and disallow raw IDs with explicit display name string literals
1 parent 9875b1f commit 7316f01

File tree

5 files changed

+90
-1
lines changed

5 files changed

+90
-1
lines changed

Sources/TestingMacros/Support/Additions/TokenSyntaxAdditions.swift

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,39 @@
1111
import SwiftSyntax
1212

1313
extension TokenSyntax {
14+
/// A tuple containing the text of this instance with enclosing backticks
15+
/// removed and whether or not they were removed.
16+
private var _textWithoutBackticks: (String, backticksRemoved: Bool) {
17+
let text = text
18+
if case .identifier = tokenKind, text.first == "`" && text.last == "`" && text.count >= 2 {
19+
return (String(text.dropFirst().dropLast()), true)
20+
}
21+
22+
return (text, false)
23+
}
24+
1425
/// The text of this instance with all backticks removed.
1526
///
1627
/// - Bug: This property works around the presence of backticks in `text.`
1728
/// ([swift-syntax-#1936](https://github.com/swiftlang/swift-syntax/issues/1936))
1829
var textWithoutBackticks: String {
19-
text.filter { $0 != "`" }
30+
_textWithoutBackticks.0
31+
}
32+
33+
/// The raw identifier, not including enclosing backticks, represented by this
34+
/// token, or `nil` if it does not represent one.
35+
var rawIdentifier: String? {
36+
let (textWithoutBackticks, backticksRemoved) = _textWithoutBackticks
37+
if backticksRemoved, textWithoutBackticks.contains(where: \.isWhitespace) {
38+
return textWithoutBackticks
39+
}
40+
41+
// TODO: remove this mock path once the toolchain fully supports raw IDs.
42+
let mockPrefix = "__raw__$"
43+
if backticksRemoved, textWithoutBackticks.starts(with: mockPrefix) {
44+
return String(textWithoutBackticks.dropFirst(mockPrefix.count))
45+
}
46+
47+
return nil
2048
}
2149
}

Sources/TestingMacros/Support/AttributeDiscovery.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ struct AttributeInfo {
100100
init(byParsing attribute: AttributeSyntax, on declaration: some SyntaxProtocol, in context: some MacroExpansionContext) {
101101
self.attribute = attribute
102102

103+
var displayNameArgument: LabeledExprListSyntax.Element?
103104
var nonDisplayNameArguments: [Argument] = []
104105
if let arguments = attribute.arguments, case let .argumentList(argumentList) = arguments {
105106
// If the first argument is an unlabelled string literal, it's the display
@@ -109,15 +110,27 @@ struct AttributeInfo {
109110
let firstArgumentHasLabel = (firstArgument.label != nil)
110111
if !firstArgumentHasLabel, let stringLiteral = firstArgument.expression.as(StringLiteralExprSyntax.self) {
111112
displayName = stringLiteral
113+
displayNameArgument = firstArgument
112114
nonDisplayNameArguments = argumentList.dropFirst().map(Argument.init)
113115
} else if !firstArgumentHasLabel, firstArgument.expression.is(NilLiteralExprSyntax.self) {
116+
displayNameArgument = firstArgument
114117
nonDisplayNameArguments = argumentList.dropFirst().map(Argument.init)
115118
} else {
116119
nonDisplayNameArguments = argumentList.map(Argument.init)
117120
}
118121
}
119122
}
120123

124+
// Disallow an explicit display name for tests and suites with raw
125+
// identifier names as it's redundant and potentially confusing.
126+
if let namedDecl = declaration.asProtocol((any NamedDeclSyntax).self),
127+
let rawIdentifier = namedDecl.name.rawIdentifier {
128+
if let displayName, let displayNameArgument {
129+
context.diagnose(.declaration(namedDecl, hasExtraneousDisplayName: displayName, fromArgument: displayNameArgument, using: attribute))
130+
}
131+
displayName = StringLiteralExprSyntax(content: rawIdentifier)
132+
}
133+
121134
// Remove leading "Self." expressions from the arguments of the attribute.
122135
// See _SelfRemover for more information. Rewriting a syntax tree discards
123136
// location information from the copy, so only invoke the rewriter if the

Sources/TestingMacros/Support/DiagnosticMessage.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,34 @@ struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage {
645645
)
646646
}
647647

648+
/// Create a diagnostic message stating that a declaration has two display
649+
/// names.
650+
///
651+
/// - Returns: A diagnostic message.
652+
static func declaration(
653+
_ decl: some NamedDeclSyntax,
654+
hasExtraneousDisplayName displayNameFromAttribute: StringLiteralExprSyntax,
655+
fromArgument argumentContainingDisplayName: LabeledExprListSyntax.Element,
656+
using attribute: AttributeSyntax
657+
) -> Self {
658+
// FIXME: implement fixits
659+
Self(
660+
syntax: Syntax(decl),
661+
message: "Attribute \(_macroName(attribute)) specifies display name '\(displayNameFromAttribute.representedLiteralValue!)' for \(_kindString(for: decl)) with implicit display name '\(decl.name.rawIdentifier!)'",
662+
severity: .error,
663+
fixIts: [
664+
FixIt(
665+
message: MacroExpansionFixItMessage("Remove '\(displayNameFromAttribute.representedLiteralValue!)'"),
666+
changes: [.replace(oldNode: Syntax(argumentContainingDisplayName), newNode: Syntax("" as ExprSyntax))]
667+
),
668+
FixIt(
669+
message: MacroExpansionFixItMessage("Rename '\(decl.name.textWithoutBackticks)'"),
670+
changes: [.replace(oldNode: Syntax(decl.name), newNode: Syntax(EditorPlaceholderExprSyntax("name")))]
671+
),
672+
]
673+
)
674+
}
675+
648676
/// Create a diagnostic messages stating that the expression passed to
649677
/// `#require()` is ambiguous.
650678
///

Tests/TestingMacrosTests/TestDeclarationMacroTests.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,21 @@ struct TestDeclarationMacroTests {
209209
),
210210
]
211211
),
212+
213+
#"@Test("Goodbye world") func `__raw__$helloWorld`()"#:
214+
(
215+
message: "Attribute 'Test' specifies display name 'Goodbye world' for function with implicit display name 'helloWorld'",
216+
fixIts: [
217+
ExpectedFixIt(
218+
message: "Remove 'Goodbye world'",
219+
changes: [.replace(oldSourceCode: #""Goodbye world""#, newSourceCode: "")]
220+
),
221+
ExpectedFixIt(
222+
message: "Rename '__raw__$helloWorld'",
223+
changes: [.replace(oldSourceCode: "`__raw__$helloWorld`", newSourceCode: "\(EditorPlaceholderExprSyntax("name"))")]
224+
),
225+
]
226+
),
212227
]
213228
}
214229

Tests/TestingTests/MiscellaneousTests.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,11 @@ struct MiscellaneousTests {
286286
#expect(testType.displayName == "Named Sendable test type")
287287
}
288288

289+
@Test func `__raw__$raw_identifier_provides_a_display_name`() throws {
290+
let test = try #require(Test.current)
291+
#expect(test.displayName == "raw_identifier_provides_a_display_name")
292+
}
293+
289294
@Test("Free functions are runnable")
290295
func freeFunction() async throws {
291296
await Test(testFunction: freeSyncFunction).run()

0 commit comments

Comments
 (0)