Skip to content

Commit 4ec2b5e

Browse files
committed
Add fixed source to assertMacroExpansion
1 parent 7cff260 commit 4ec2b5e

File tree

3 files changed

+144
-66
lines changed

3 files changed

+144
-66
lines changed

Release Notes/511.md

+7
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## New APIs
44

5+
- `assertMacroExpansion` now have new parameters named `applyFixIts` and `fixedSource`
6+
- Description: `applyFixIts` and `fixedSource` are used to assert so ensure that the source code after applying Fix-Its matches this string.
7+
- Issue: https://github.com/apple/swift-syntax/issues/2015
8+
- Pull Request: https://github.com/apple/swift-syntax/pull/2021
9+
510
## API Behavior Changes
611

712
## Deprecations
@@ -21,9 +26,11 @@
2126
- Effect specifiers:
2227
- Description: The `unexpectedAfterThrowsSpecifier` node of the various effect specifiers has been removed.
2328
- Pull request: https://github.com/apple/swift-syntax/pull/2219
29+
2430
- `SyntaxKind` removed conformance to `CaseIterable`
2531
- Description: `SyntaxKind` no longer conforms to `CaseIterable` since there is no good use case to iterate over all syntax kinds.
2632
- Pull request: https://github.com/apple/swift-syntax/pull/2292
33+
2734
- `IntegerLiteralExprSyntax.Radix` removed conformance to `CaseIterable`
2835
- Description: `IntegerLiteralExprSyntax.Radix` no longer conforms to `CaseIterable` since there is no good use case to iterate over all radix kinds.
2936
- Pull request: https://github.com/apple/swift-syntax/pull/2292

Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift

+17
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,18 @@ func assertDiagnostic(
254254
/// - macros: The macros that should be expanded, provided as a dictionary
255255
/// mapping macro names (e.g., `"stringify"`) to implementation types
256256
/// (e.g., `StringifyMacro.self`).
257+
/// - applyFixIts: If specified, filters the Fix-Its that are applied to generate `fixedSource` to only those whose message occurs in this array. If `nil`, all Fix-Its from the diagnostics are applied.
258+
/// - fixedSource: If specified, asserts that the source code after applying Fix-Its matches this string.
257259
/// - testModuleName: The name of the test module to use.
258260
/// - testFileName: The name of the test file name to use.
261+
/// - indentationWidth: The indentation width used in the expansion.
259262
public func assertMacroExpansion(
260263
_ originalSource: String,
261264
expandedSource expectedExpandedSource: String,
262265
diagnostics: [DiagnosticSpec] = [],
263266
macros: [String: Macro.Type],
267+
applyFixIts: [String]? = nil,
268+
fixedSource expectedFixedSource: String? = nil,
264269
testModuleName: String = "TestModule",
265270
testFileName: String = "test.swift",
266271
indentationWidth: Trivia = .spaces(4),
@@ -317,4 +322,16 @@ public func assertMacroExpansion(
317322
assertDiagnostic(actualDiag, in: context, expected: expectedDiag)
318323
}
319324
}
325+
326+
// Applying Fix-Its
327+
if let expectedFixedSource = expectedFixedSource {
328+
let fixedTree = FixItApplier.applyFixes(from: context.diagnostics, filterByMessages: applyFixIts, to: origSourceFile)
329+
let fixedTreeDescription = fixedTree.description
330+
assertStringsEqualWithDiff(
331+
fixedTreeDescription.trimmingTrailingWhitespace(),
332+
expectedFixedSource.trimmingTrailingWhitespace(),
333+
file: file,
334+
line: line
335+
)
336+
}
320337
}

Tests/SwiftSyntaxMacroExpansionTest/PeerMacroTests.swift

+120-66
Original file line numberDiff line numberDiff line change
@@ -28,89 +28,117 @@ import XCTest
2828
final class PeerMacroTests: XCTestCase {
2929
private let indentationWidth: Trivia = .spaces(2)
3030

31-
func testAddCompletionHandler() {
32-
struct AddCompletionHandler: PeerMacro {
33-
static func expansion(
34-
of node: AttributeSyntax,
35-
providingPeersOf declaration: some DeclSyntaxProtocol,
36-
in context: some MacroExpansionContext
37-
) throws -> [DeclSyntax] {
38-
// Only on functions at the moment. We could handle initializers as well
39-
// with a bit of work.
40-
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
41-
throw MacroExpansionErrorMessage("@addCompletionHandler only works on functions")
42-
}
31+
fileprivate struct AddCompletionHandler: PeerMacro {
32+
static func expansion(
33+
of node: AttributeSyntax,
34+
providingPeersOf declaration: some DeclSyntaxProtocol,
35+
in context: some MacroExpansionContext
36+
) throws -> [DeclSyntax] {
37+
// Only on functions at the moment. We could handle initializers as well
38+
// with a bit of work.
39+
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
40+
throw MacroExpansionErrorMessage("@addCompletionHandler only works on functions")
41+
}
4342

44-
// This only makes sense for async functions.
45-
if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil {
46-
throw MacroExpansionErrorMessage(
47-
"@addCompletionHandler requires an async function"
48-
)
43+
// This only makes sense for async functions.
44+
if funcDecl.signature.effectSpecifiers?.asyncSpecifier == nil {
45+
let newEffects: FunctionEffectSpecifiersSyntax
46+
if let existingEffects = funcDecl.signature.effectSpecifiers {
47+
newEffects = existingEffects.with(\.asyncSpecifier, .keyword(.async))
48+
} else {
49+
newEffects = FunctionEffectSpecifiersSyntax(asyncSpecifier: .keyword(.async))
4950
}
5051

51-
// Form the completion handler parameter.
52-
let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.trimmed
53-
54-
let completionHandlerParam =
55-
FunctionParameterSyntax(
56-
firstName: .identifier("completionHandler"),
57-
colon: .colonToken(trailingTrivia: .space),
58-
type: TypeSyntax("(\(resultType ?? "")) -> Void")
59-
)
60-
61-
// Add the completion handler parameter to the parameter list.
62-
let parameterList = funcDecl.signature.parameterClause.parameters
63-
var newParameterList = parameterList
64-
if !parameterList.isEmpty {
65-
// We need to add a trailing comma to the preceding list.
66-
newParameterList[newParameterList.index(before: newParameterList.endIndex)].trailingComma = .commaToken(trailingTrivia: .space)
67-
}
68-
newParameterList.append(completionHandlerParam)
52+
let newSignature = funcDecl.signature.with(\.effectSpecifiers, newEffects)
53+
54+
let diag = Diagnostic(
55+
node: Syntax(funcDecl.funcKeyword),
56+
message: MacroExpansionErrorMessage(
57+
"can only add a completion-handler variant to an 'async' function"
58+
),
59+
fixIts: [
60+
FixIt(
61+
message: MacroExpansionFixItMessage(
62+
"add 'async'"
63+
),
64+
changes: [
65+
FixIt.Change.replace(
66+
oldNode: Syntax(funcDecl.signature),
67+
newNode: Syntax(newSignature)
68+
)
69+
]
70+
)
71+
]
72+
)
73+
74+
context.diagnose(diag)
75+
return []
76+
}
6977

70-
let callArguments: [String] = parameterList.map { param in
71-
let argName = param.secondName ?? param.firstName
78+
// Form the completion handler parameter.
79+
let resultType: TypeSyntax? = funcDecl.signature.returnClause?.type.trimmed
80+
81+
let completionHandlerParam =
82+
FunctionParameterSyntax(
83+
firstName: .identifier("completionHandler"),
84+
colon: .colonToken(trailingTrivia: .space),
85+
type: TypeSyntax("(\(resultType ?? "")) -> Void")
86+
)
87+
88+
// Add the completion handler parameter to the parameter list.
89+
let parameterList = funcDecl.signature.parameterClause.parameters
90+
var newParameterList = parameterList
91+
if !parameterList.isEmpty {
92+
// We need to add a trailing comma to the preceding list.
93+
newParameterList[newParameterList.index(before: newParameterList.endIndex)].trailingComma = .commaToken(trailingTrivia: .space)
94+
}
95+
newParameterList.append(completionHandlerParam)
7296

73-
if param.firstName.text != "_" {
74-
return "\(param.firstName.text): \(argName.text)"
75-
}
97+
let callArguments: [String] = parameterList.map { param in
98+
let argName = param.secondName ?? param.firstName
7699

77-
return "\(argName.text)"
100+
if param.firstName.text != "_" {
101+
return "\(param.firstName.text): \(argName.text)"
78102
}
79103

80-
let call: ExprSyntax =
81-
"\(funcDecl.name)(\(raw: callArguments.joined(separator: ", ")))"
82-
83-
// FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation,
84-
// so that the full body could go here.
85-
let newBody: ExprSyntax =
86-
"""
104+
return "\(argName.text)"
105+
}
87106

88-
Task {
89-
completionHandler(await \(call))
90-
}
107+
let call: ExprSyntax =
108+
"\(funcDecl.name)(\(raw: callArguments.joined(separator: ", ")))"
91109

92-
"""
110+
// FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation,
111+
// so that the full body could go here.
112+
let newBody: ExprSyntax =
113+
"""
93114
94-
// Drop the @addCompletionHandler attribute from the new declaration.
95-
let newAttributeList = funcDecl.attributes.filter {
96-
guard case let .attribute(attribute) = $0 else {
97-
return true
115+
Task {
116+
completionHandler(await \(call))
98117
}
99-
return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name == "addCompletionHandler"
100-
}
101118
102-
var newFunc = funcDecl
103-
newFunc.signature.effectSpecifiers?.asyncSpecifier = nil // drop async
104-
newFunc.signature.returnClause = nil // drop result type
105-
newFunc.signature.parameterClause.parameters = newParameterList
106-
newFunc.signature.parameterClause.trailingTrivia = []
107-
newFunc.body = CodeBlockSyntax { newBody }
108-
newFunc.attributes = newAttributeList
119+
"""
109120

110-
return [DeclSyntax(newFunc)]
121+
// Drop the @addCompletionHandler attribute from the new declaration.
122+
let newAttributeList = funcDecl.attributes.filter {
123+
guard case let .attribute(attribute) = $0 else {
124+
return true
125+
}
126+
return attribute.attributeName.as(IdentifierTypeSyntax.self)?.name == "addCompletionHandler"
111127
}
128+
129+
var newFunc = funcDecl
130+
newFunc.signature.effectSpecifiers?.asyncSpecifier = nil // drop async
131+
newFunc.signature.returnClause = nil // drop result type
132+
newFunc.signature.parameterClause.parameters = newParameterList
133+
newFunc.signature.parameterClause.trailingTrivia = []
134+
newFunc.body = CodeBlockSyntax { newBody }
135+
newFunc.attributes = newAttributeList
136+
137+
return [DeclSyntax(newFunc)]
112138
}
139+
}
113140

141+
func testAddCompletionHandler() {
114142
assertMacroExpansion(
115143
"""
116144
@addCompletionHandler
@@ -193,4 +221,30 @@ final class PeerMacroTests: XCTestCase {
193221
]
194222
)
195223
}
224+
225+
func testAddCompletionHandlerWhereThereIsNotAsync() {
226+
assertMacroExpansion(
227+
"""
228+
@addCompletionHandler
229+
func f(a: Int, for b: String, _ value: Double) -> String { }
230+
""",
231+
expandedSource: """
232+
func f(a: Int, for b: String, _ value: Double) -> String { }
233+
""",
234+
diagnostics: [
235+
DiagnosticSpec(
236+
message: "can only add a completion-handler variant to an 'async' function",
237+
line: 2,
238+
column: 1,
239+
fixIts: [FixItSpec(message: "add 'async'")]
240+
)
241+
],
242+
macros: ["addCompletionHandler": AddCompletionHandler.self],
243+
fixedSource: """
244+
@addCompletionHandler
245+
func f(a: Int, for b: String, _ value: Double) async-> String { }
246+
""",
247+
indentationWidth: indentationWidth
248+
)
249+
}
196250
}

0 commit comments

Comments
 (0)