swiftswift-macro

How to initialize an enum with associated values coming from a function using swift macros?


Basically I have a macro that is given some protocol and I want to output something like the following as a PeerMacro:

Input:

protocol MyProtocol {
    func myFunction(value1: Int, value2: String)
}

Output:

class MyProtocolMock: MyProtocol {
    enum Call {
        case myFunction(Int, String)
    }
    func myFunction(value1: Int, value2: String) {
        calls.append(.myFunction(value1, value2))
    }
}

I have most of it working and I got to the following point:

func make(from function: FunctionDeclSyntax) throws -> FunctionDeclSyntax {
    FunctionDeclSyntax(name: function.name, signature: function.signature) {
        // how to extract the function param names and add here as enum associated values?
        "calls.append(.\(function.name)())"
    }
}

For more context, the function: FunctionDeclSyntax from the param comes from the ProtocolDeclSyntax using proto.memberBlock.members.compactMap { $0.decl.as(FunctionDeclSyntax.self) } and the protocol comes from calling declaration.as(ProtocolDeclSyntax.self) from the root of the PeerMacro

How can I initialize the associated values from my enum from the function params?


Solution

  • In a function declaration, parameters either has one name, or two names (parameter label and internal name). If the second name exists, we should use that second name in the function body. Then all you need to know is the type of the AST node that you need to create. You can find that out using Swift AST Explorer.

    static func make(from function: FunctionDeclSyntax) -> FunctionDeclSyntax {
        FunctionDeclSyntax(name: function.name, signature: function.signature) {
            let labeledParameters = LabeledExprListSyntax {
                for param in function.signature.parameterClause.parameters {
                    if let secondName = param.secondName {
                        LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: secondName))
                    } else {
                        LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: param.firstName))
                    }
                }
            }
            "calls.append(.\(function.name)(\(labeledParameters))"
        }
    }
    

    This assumes that the associated values of the Call enum will always have no parameter labels, i.e. case myFunction(Int, String), not case myFunction(value1: Int, value2: String)

    This approach of mocking will work for the case where the function returns Void and its parameter types are all "simple types" like classes/structs/enums. If one of the functions has a non-escaping closure, this would not work. If the function is generic, this would also not work.