Skip to content

Add some examples #13

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
4 changes: 4 additions & 0 deletions Sources/MacroToolkitExample/MacroToolkitExample.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
11 changes: 11 additions & 0 deletions Sources/MacroToolkitExamplePlugin/AddAsyncAllMembersMacro.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
93 changes: 2 additions & 91 deletions Sources/MacroToolkitExamplePlugin/AddAsyncMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)]
}
}
108 changes: 108 additions & 0 deletions Sources/MacroToolkitExamplePlugin/AddAsyncMacroCore.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]
}
117 changes: 117 additions & 0 deletions Tests/MacroToolkitTests/MacroToolkitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
)
}
}