Skip to content

Commit f5f45e2

Browse files
authored
Merge pull request #1196 from DougGregor/peer-declaration-macros
2 parents c61cf2e + 9f84c6b commit f5f45e2

File tree

6 files changed

+255
-2
lines changed

6 files changed

+255
-2
lines changed

Sources/_SwiftSyntaxMacros/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ add_swift_host_library(_SwiftSyntaxMacros
1313
Macro.swift
1414
MacroExpansionContext.swift
1515
MacroSystem.swift
16+
PeerDeclarationMacro.swift
1617
Syntax+MacroEvaluation.swift
1718
)
1819

Sources/_SwiftSyntaxMacros/ExpressionMacro.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
import SwiftSyntax
1414
import SwiftParser
15-
import SwiftDiagnostics
1615

1716
/// Describes a macro that is explicitly expanded as an expression.
1817
public protocol ExpressionMacro: Macro {

Sources/_SwiftSyntaxMacros/FreestandingDeclarationMacro.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import SwiftSyntax
1111
import SwiftParser
12-
import SwiftDiagnostics
1312

1413
/// Describes a macro that forms declarations.
1514
public protocol FreestandingDeclarationMacro: DeclarationMacro {

Sources/_SwiftSyntaxMacros/MacroSystem.swift

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,16 @@ class MacroApplication: SyntaxRewriter {
120120
// Recurse on the child node.
121121
let newItem = visit(item.item)
122122
newItems.append(item.withItem(newItem))
123+
124+
// Expand any peer declarations triggered by macros used as attributes.
125+
if case let .decl(decl) = item.item {
126+
let peers = expandPeers(of: decl)
127+
newItems.append(
128+
contentsOf: peers.map {
129+
newDecl in CodeBlockItemSyntax(item: .decl(newDecl))
130+
}
131+
)
132+
}
123133
}
124134

125135
return CodeBlockItemListSyntax(newItems)
@@ -160,10 +170,97 @@ class MacroApplication: SyntaxRewriter {
160170
// Recurse on the child node.
161171
let newDecl = visit(item.decl)
162172
newItems.append(item.withDecl(newDecl))
173+
174+
// Expand any peer declarations triggered by macros used as attributes.
175+
let peers = expandPeers(of: item.decl)
176+
newItems.append(
177+
contentsOf: peers.map {
178+
newDecl in MemberDeclListItemSyntax(decl: newDecl)
179+
}
180+
)
163181
}
164182

165183
return .init(newItems)
166184
}
185+
186+
override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
187+
let visitedNode = super.visit(node)
188+
189+
// FIXME: Generalize this to DeclSyntax, once we have attributes.
190+
// Visit the node first.
191+
192+
guard let visitedFunc = visitedNode.as(FunctionDeclSyntax.self),
193+
let attributes = visitedFunc.attributes
194+
else {
195+
return visitedNode
196+
}
197+
198+
// Remove any attached attributes.
199+
let newAttributes = attributes.filter {
200+
guard case let .customAttribute(customAttr) = $0 else {
201+
return true
202+
}
203+
204+
guard let attributeName = customAttr.attributeName.as(SimpleTypeIdentifierSyntax.self)?.name.text,
205+
let macro = macroSystem.macros[attributeName]
206+
else {
207+
return true
208+
}
209+
210+
return !(macro is PeerDeclarationMacro.Type)
211+
}
212+
213+
if newAttributes.isEmpty {
214+
return DeclSyntax(visitedFunc.withAttributes(nil))
215+
}
216+
217+
return DeclSyntax(visitedFunc.withAttributes(AttributeListSyntax(newAttributes)))
218+
}
219+
}
220+
221+
extension MacroApplication {
222+
// If any of the custom attributes associated with the given declaration
223+
// refer to "peer" declaration macros, expand them and return the resulting
224+
// set of peer declarations.
225+
private func expandPeers(of decl: DeclSyntax) -> [DeclSyntax] {
226+
// Dig out the attribute list.
227+
// FIXME: We should have a better way to get the attributes from any
228+
// declaration.
229+
guard
230+
let attributes =
231+
(decl.children(viewMode: .sourceAccurate).compactMap {
232+
$0.as(AttributeListSyntax.self)
233+
}).first
234+
else {
235+
return []
236+
}
237+
238+
var peers: [DeclSyntax] = []
239+
for attribute in attributes {
240+
guard case let .customAttribute(customAttribute) = attribute,
241+
let attributeName = customAttribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.name.text,
242+
let macro = macroSystem.macros[attributeName],
243+
let peerMacro = macro as? PeerDeclarationMacro.Type
244+
else {
245+
continue
246+
}
247+
248+
do {
249+
let newPeers = try peerMacro.expansion(of: customAttribute, attachedTo: decl, in: &context)
250+
peers.append(contentsOf: newPeers)
251+
} catch {
252+
// Record the error
253+
context.diagnose(
254+
Diagnostic(
255+
node: Syntax(attribute),
256+
message: ThrownErrorDiagnostic(message: String(describing: error))
257+
)
258+
)
259+
}
260+
}
261+
262+
return peers
263+
}
167264
}
168265

169266
extension SyntaxProtocol {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//
2+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
3+
// Licensed under Apache License v2.0 with Runtime Library Exception
4+
//
5+
// See https://swift.org/LICENSE.txt for license information
6+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
7+
//
8+
//===----------------------------------------------------------------------===//
9+
10+
import SwiftSyntax
11+
import SwiftParser
12+
13+
public protocol PeerDeclarationMacro: DeclarationMacro {
14+
/// Expand a macro described by the given custom attribute and
15+
/// attached to the given declaration and evaluated within a
16+
/// particular expansion context.
17+
///
18+
/// The macro expansion can introduce "peer" declarations that sit alongside
19+
/// the
20+
static func expansion(
21+
of node: CustomAttributeSyntax,
22+
attachedTo declaration: DeclSyntax,
23+
in context: inout MacroExpansionContext
24+
) throws -> [DeclSyntax]
25+
}

Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,117 @@ struct DefineBitwidthNumberedStructsMacro: FreestandingDeclarationMacro {
208208
}
209209
}
210210

211+
public struct AddCompletionHandler: PeerDeclarationMacro {
212+
public static func expansion(
213+
of node: CustomAttributeSyntax,
214+
attachedTo declaration: DeclSyntax,
215+
in context: inout MacroExpansionContext
216+
) throws -> [DeclSyntax] {
217+
// Only on functions at the moment. We could handle initializers as well
218+
// with a bit of work.
219+
guard let funcDecl = declaration.as(FunctionDeclSyntax.self) else {
220+
throw CustomError.message("@addCompletionHandler only works on functions")
221+
}
222+
223+
// This only makes sense for async functions.
224+
if funcDecl.signature.asyncOrReasyncKeyword == nil {
225+
throw CustomError.message(
226+
"@addCompletionHandler requires an async function"
227+
)
228+
}
229+
230+
// Form the completion handler parameter.
231+
let resultType: TypeSyntax? = funcDecl.signature.output?.returnType.withoutTrivia()
232+
233+
let completionHandlerParam =
234+
FunctionParameterSyntax(
235+
firstName: .identifier("completionHandler"),
236+
colon: .colonToken(trailingTrivia: .space),
237+
type: "(\(resultType ?? "")) -> Void" as TypeSyntax
238+
)
239+
240+
// Add the completion handler parameter to the parameter list.
241+
let parameterList = funcDecl.signature.input.parameterList
242+
let newParameterList: FunctionParameterListSyntax
243+
if let lastParam = parameterList.last {
244+
// We need to add a trailing comma to the preceding list.
245+
newParameterList = parameterList.removingLast()
246+
.appending(
247+
lastParam.withTrailingComma(
248+
.commaToken(trailingTrivia: .space)
249+
)
250+
)
251+
.appending(completionHandlerParam)
252+
} else {
253+
newParameterList = parameterList.appending(completionHandlerParam)
254+
}
255+
256+
let callArguments: [String] = try parameterList.map { param in
257+
guard let argName = param.secondName ?? param.firstName else {
258+
throw CustomError.message(
259+
"@addCompletionHandler argument must have a name"
260+
)
261+
}
262+
263+
if let paramName = param.firstName, paramName.text != "_" {
264+
return "\(paramName.withoutTrivia()): \(argName.withoutTrivia())"
265+
}
266+
267+
return "\(argName.withoutTrivia())"
268+
}
269+
270+
let call: ExprSyntax =
271+
"\(funcDecl.identifier)(\(raw: callArguments.joined(separator: ", ")))"
272+
273+
// FIXME: We should make CodeBlockSyntax ExpressibleByStringInterpolation,
274+
// so that the full body could go here.
275+
let newBody: ExprSyntax =
276+
"""
277+
278+
Task {
279+
completionHandler(await \(call))
280+
}
281+
282+
"""
283+
284+
// Drop the @addCompletionHandler attribute from the new declaration.
285+
let newAttributeList = AttributeListSyntax(
286+
funcDecl.attributes?.filter {
287+
guard case let .customAttribute(customAttr) = $0 else {
288+
return true
289+
}
290+
291+
return customAttr != node
292+
} ?? []
293+
)
294+
295+
let newFunc =
296+
funcDecl
297+
.withSignature(
298+
funcDecl.signature
299+
.withAsyncOrReasyncKeyword(nil) // drop async
300+
.withOutput(nil) // drop result type
301+
.withInput( // add completion handler parameter
302+
funcDecl.signature.input.withParameterList(newParameterList)
303+
.withoutTrailingTrivia()
304+
)
305+
)
306+
.withBody(
307+
CodeBlockSyntax(
308+
leftBrace: .leftBraceToken(leadingTrivia: .space),
309+
statements: CodeBlockItemListSyntax(
310+
[CodeBlockItemSyntax(item: .expr(newBody))]
311+
),
312+
rightBrace: .rightBraceToken(leadingTrivia: .newline)
313+
)
314+
)
315+
.withAttributes(newAttributeList)
316+
.withLeadingTrivia(.newlines(2))
317+
318+
return [DeclSyntax(newFunc)]
319+
}
320+
}
321+
211322
// MARK: Assertion helper functions
212323

213324
/// Assert that expanding the given macros in the original source produces
@@ -269,6 +380,7 @@ public let testMacros: [String: Macro.Type] = [
269380
"stringify": StringifyMacro.self,
270381
"myError": ErrorMacro.self,
271382
"bitwidthNumberedStructs": DefineBitwidthNumberedStructsMacro.self,
383+
"addCompletionHandler": AddCompletionHandler.self,
272384
]
273385

274386
final class MacroSystemTests: XCTestCase {
@@ -386,4 +498,24 @@ final class MacroSystemTests: XCTestCase {
386498
"""
387499
)
388500
}
501+
502+
func testAddCompletionHandler() {
503+
AssertMacroExpansion(
504+
macros: testMacros,
505+
"""
506+
@addCompletionHandler
507+
func f(a: Int, for b: String, _ value: Double) async -> String { }
508+
""",
509+
"""
510+
511+
func f(a: Int, for b: String, _ value: Double) async -> String { }
512+
513+
func f(a: Int, for b: String, _ value: Double, completionHandler: (String) -> Void) {
514+
Task {
515+
completionHandler(await f(a: a, for: b, value))
516+
}
517+
}
518+
"""
519+
)
520+
}
389521
}

0 commit comments

Comments
 (0)