@@ -28,89 +28,117 @@ import XCTest
2828final 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