diff --git a/Package.swift b/Package.swift index c9d90cc..e954f17 100644 --- a/Package.swift +++ b/Package.swift @@ -15,7 +15,7 @@ let package = Package( dependencies: [ .package( url: "https://github.com/apple/swift-syntax.git", - exact: "509.0.0" + from: "509.0.0" ), .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.3.0"), .package(url: "https://github.com/SwiftPackageIndex/SPIManifest.git", from: "0.12.0"), diff --git a/Sources/MacroToolkitExample/MacroToolkitExample.swift b/Sources/MacroToolkitExample/MacroToolkitExample.swift index 985d6b7..0b414c1 100644 --- a/Sources/MacroToolkitExample/MacroToolkitExample.swift +++ b/Sources/MacroToolkitExample/MacroToolkitExample.swift @@ -36,3 +36,7 @@ public macro CustomCodable() = @attached(memberAttribute) public macro DictionaryStorage() = #externalMacro(module: "MacroToolkitExamplePlugin", type: "DictionaryStorageMacro") + +@attached(member, names: arbitrary) +public macro AddAsyncAllMembers() = + #externalMacro(module: "MacroToolkitExamplePlugin", type: "AddAsyncAllMembersMacro") diff --git a/Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift b/Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift new file mode 100644 index 0000000..15cd34a --- /dev/null +++ b/Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift @@ -0,0 +1,11 @@ +import SwiftSyntax +import MacroToolkit +import SwiftSyntaxMacros + +public enum AddAsyncAllMembersMacro: MemberMacro { + public static func expansion(of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext) throws -> [DeclSyntax] { + declaration.memberBlock.members.map(\.decl).compactMap { + try? AddAsyncMacroCore.expansion(of: nil, providingFunctionOf: $0) + } + } +} diff --git a/Sources/MacroToolkitExamplePlugin/AddAsyncMacro.swift b/Sources/MacroToolkitExamplePlugin/AddAsyncMacro.swift index e1cddee..b551df1 100644 --- a/Sources/MacroToolkitExamplePlugin/AddAsyncMacro.swift +++ b/Sources/MacroToolkitExamplePlugin/AddAsyncMacro.swift @@ -3,7 +3,7 @@ import SwiftSyntax import SwiftSyntaxMacros // Modified from: https://github.com/DougGregor/swift-macro-examples/blob/f61ac7cdca8dc3557e53f86e7e03df1353908d3e/MacroExamplesPlugin/AddAsyncMacro.swift -public struct AddAsyncMacro: PeerMacro { +public enum AddAsyncMacro: PeerMacro { public static func expansion< Context: MacroExpansionContext, Declaration: DeclSyntaxProtocol @@ -12,95 +12,6 @@ public struct AddAsyncMacro: PeerMacro { providingPeersOf declaration: Declaration, in context: Context ) throws -> [DeclSyntax] { - // Only on functions at the moment. - guard let function = Function(declaration) else { - throw MacroError("@AddAsync only works on functions") - } - - // This only makes sense for non async functions. - guard !function.isAsync else { - throw MacroError("@AddAsync requires a non async function") - } - - // This only makes sense void functions - guard function.returnsVoid else { - throw MacroError("@AddAsync requires a function that returns void") - } - - // Requires a completion handler block as last parameter - guard - let completionHandlerType = function.parameters.last?.type.asFunctionType - else { - throw MacroError( - "@AddAsync requires a function that has a completion handler as last parameter") - } - - // Completion handler needs to return Void - guard completionHandlerType.returnType.isVoid else { - throw MacroError( - "@AddAsync requires a function that has a completion handler that returns Void") - } - - guard let returnType = completionHandlerType.parameters.first else { - throw MacroError( - "@AddAsync requires a function that has a completion handler that has one parameter" - ) - } - - // Destructure return type - let successReturnType: Type - let isResultReturn: Bool - if case let .simple("Result", (successType, _)) = destructure(returnType) { - isResultReturn = true - successReturnType = successType - } else { - isResultReturn = false - successReturnType = returnType - } - - // Remove completionHandler and comma from the previous parameter - let newParameters = function.parameters.dropLast() - - // Drop the @AddAsync attribute from the new declaration. - let filteredAttributes = function.attributes.removing(node) - - let callArguments = newParameters.asPassthroughArguments - - let switchBody: ExprSyntax = - """ - switch returnValue { - case .success(let value): - continuation.resume(returning: value) - case .failure(let error): - continuation.resume(throwing: error) - } - """ - - let continuationExpr = - isResultReturn - ? "try await withCheckedThrowingContinuation { continuation in" - : "await withCheckedContinuation { continuation in" - - let newBody: ExprSyntax = - """ - \(raw: continuationExpr) - \(raw: function.identifier)(\(raw: callArguments.joined(separator: ", "))) { returnValue in - \(isResultReturn ? switchBody : "continuation.resume(returning: returnValue)") - } - } - """ - - // TODO: Make better codeblock init - let newFunc = - function._syntax - .withParameters(newParameters) - .withReturnType(successReturnType) - .withAsyncModifier() - .withThrowsModifier(isResultReturn) - .withBody(CodeBlockSyntax([newBody])) - .withAttributes(filteredAttributes) - .withLeadingBlankLine() - - return [DeclSyntax(newFunc)] + [try AddAsyncMacroCore.expansion(of: node, providingFunctionOf: declaration)] } } diff --git a/Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift b/Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift new file mode 100644 index 0000000..25e64eb --- /dev/null +++ b/Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift @@ -0,0 +1,108 @@ +import SwiftSyntax +import MacroToolkit +import SwiftSyntaxMacros + +// Modified from: https://github.com/DougGregor/swift-macro-examples/blob/f61ac7cdca8dc3557e53f86e7e03df1353908d3e/MacroExamplesPlugin/AddAsyncMacro.swift + +enum AddAsyncMacroCore { + static func expansion(of node: AttributeSyntax?, providingFunctionOf declaration: some DeclSyntaxProtocol) throws -> DeclSyntax { + // Only on functions at the moment. + guard let function = Function(declaration) else { + throw MacroError("@AddAsync only works on functions") + } + + // This only makes sense for non async functions. + guard !function.isAsync else { + throw MacroError("@AddAsync requires a non async function") + } + + // This only makes sense void functions + guard function.returnsVoid else { + throw MacroError("@AddAsync requires a function that returns void") + } + + // Requires a completion handler block as last parameter + guard + let completionHandlerType = function.parameters.last?.type.asFunctionType + else { + throw MacroError( + "@AddAsync requires a function that has a completion handler as last parameter") + } + + // Completion handler needs to return Void + guard completionHandlerType.returnType.isVoid else { + throw MacroError( + "@AddAsync requires a function that has a completion handler that returns Void") + } + + guard let returnType = completionHandlerType.parameters.first else { + throw MacroError( + "@AddAsync requires a function that has a completion handler that has one parameter" + ) + } + + // Destructure return type + let successReturnType: Type + let isResultReturn: Bool + if case let .simple("Result", (successType, _)) = destructure(returnType) { + isResultReturn = true + successReturnType = successType + } else { + isResultReturn = false + successReturnType = returnType + } + + // Remove completionHandler and comma from the previous parameter + let newParameters = function.parameters.dropLast() + + // Drop the @AddAsync attribute from the new declaration. + var filteredAttributes = function.attributes + if let node { + filteredAttributes = filteredAttributes.removing(node) + } + + let callArguments = newParameters.asPassthroughArguments + + let newBody = function._syntax.body.map { _ in + let switchBody: ExprSyntax = + """ + switch returnValue { + case .success(let value): + continuation.resume(returning: value) + case .failure(let error): + continuation.resume(throwing: error) + } + """ + + let continuationExpr = + isResultReturn + ? "try await withCheckedThrowingContinuation { continuation in" + : "await withCheckedContinuation { continuation in" + + let newBody: ExprSyntax = + """ + \(raw: continuationExpr) + \(raw: function.identifier)(\(raw: callArguments.joined(separator: ", "))) { returnValue in + \(isResultReturn ? switchBody : "continuation.resume(returning: returnValue)") + } + } + """ + return CodeBlockSyntax([newBody]) + } + // TODO: Make better codeblock init + var newFunc = + function._syntax + .withParameters(newParameters) + .withReturnType(successReturnType) + .withAsyncModifier() + .withThrowsModifier(isResultReturn) + .withAttributes(filteredAttributes) + .withLeadingBlankLine() + + if let newBody { + newFunc = newFunc.withBody(newBody) + } + + return DeclSyntax(newFunc) + } +} diff --git a/Sources/MacroToolkitExamplePlugin/MacroToolkitExamplePlugin.swift b/Sources/MacroToolkitExamplePlugin/MacroToolkitExamplePlugin.swift index be4da19..be3e44d 100644 --- a/Sources/MacroToolkitExamplePlugin/MacroToolkitExamplePlugin.swift +++ b/Sources/MacroToolkitExamplePlugin/MacroToolkitExamplePlugin.swift @@ -4,6 +4,15 @@ import SwiftSyntaxMacros @main struct MacroToolkitExamplePlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - AddAsyncMacro.self + AddAsyncMacro.self, + AddCompletionHandlerMacro.self, + CaseDetectionMacro.self, + AddBlockerMacro.self, + OptionSetMacro.self, + MetaEnumMacro.self, + CustomCodableMacro.self, + CodableKeyMacro.self, + DictionaryStorageMacro.self, + AddAsyncAllMembersMacro.self, ] } diff --git a/Tests/MacroToolkitTests/MacroToolkitTests.swift b/Tests/MacroToolkitTests/MacroToolkitTests.swift index 925de2f..4b82a3e 100644 --- a/Tests/MacroToolkitTests/MacroToolkitTests.swift +++ b/Tests/MacroToolkitTests/MacroToolkitTests.swift @@ -16,6 +16,7 @@ let testMacros: [String: Macro.Type] = [ "CustomCodable": CustomCodableMacro.self, "CodableKey": CodableKeyMacro.self, "DictionaryStorage": DictionaryStorageMacro.self, + "AddAsyncAllMembers": AddAsyncAllMembersMacro.self, ] final class MacroToolkitTests: XCTestCase { @@ -561,4 +562,120 @@ final class MacroToolkitTests: XCTestCase { XCTAssertEqual(n.initialValue?._syntax.description, "[1, 2.5]") XCTAssertEqual(n.isLazy, true) } + + func testAsyncInterfaceMacro() throws { + assertMacroExpansion( + """ + protocol API { + @AddAsync + func request(completion: (Int) -> Void) + } + """, + expandedSource: + """ + protocol API { + func request(completion: (Int) -> Void) + + func request() async -> Int + } + """, + macros: testMacros + ) + } + + func testAsyncInterfaceAllMembersMacro() throws { + assertMacroExpansion( + """ + @AddAsyncAllMembers + protocol API { + func request1(completion: (Int) -> Void) + func request2(completion: (String) -> Void) + } + """, + expandedSource: + """ + protocol API { + func request1(completion: (Int) -> Void) + func request2(completion: (String) -> Void) + + func request1() async -> Int + + func request2() async -> String + } + """, + macros: testMacros + ) + } + func testAsyncImplementationMacro() throws { + assertMacroExpansion( + """ + struct Client { + @AddAsync + func request1(completion: (Int) -> Void) { + completion(0) + } + } + """, + expandedSource: + """ + struct Client { + func request1(completion: (Int) -> Void) { + completion(0) + } + + func request1() async -> Int { + await withCheckedContinuation { continuation in + request1() { returnValue in + continuation.resume(returning: returnValue) + } + } + } + } + """, + macros: testMacros + ) + } + func testAsyncImplementationAllMembersMacro() throws { + assertMacroExpansion( + """ + @AddAsyncAllMembers + struct Client { + func request1(completion: (Int) -> Void) { + completion(0) + } + func request2(completion: (String) -> Void) { + completion("") + } + } + """, + expandedSource: + """ + struct Client { + func request1(completion: (Int) -> Void) { + completion(0) + } + func request2(completion: (String) -> Void) { + completion("") + } + + func request1() async -> Int { + await withCheckedContinuation { continuation in + request1() { returnValue in + continuation.resume(returning: returnValue) + } + } + } + + func request2() async -> String { + await withCheckedContinuation { continuation in + request2() { returnValue in + continuation.resume(returning: returnValue) + } + } + } + } + """, + macros: testMacros + ) + } }