swiftswift-macro

Swift accessor macro with dynamic value


I want to write a macro GetSetMacro which is taking in the value of the stored property of the type and generate the accessors for another property based on it.

// Definition
@attached(accessor)
public macro GetSetMacro<Value>(_ : Value)  = #externalMacro(module: "MyMacroModule", type: "GetSetMacro")

// Implementation
public struct GetSetMacro: AccessorMacro {
    public static func expansion(of node: SwiftSyntax.AttributeSyntax, providingAccessorsOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.AccessorDeclSyntax] {
        guard
             case let .argumentList(arguments) = node.arguments,
             let argument = arguments.first
           else { return [] }
       
        return [
            """
              get {
                // How to access the member value of the type in here?
                ???
              }
              """,
              """
              set {
                ??? = newValue
              }
            """
        ]
    }
}

Example usage

var value = 1

@GetSetMacro(\.value)
var newIntValue: Int

// expansion
var newIntValue {
 get {
   value
 } set {
    value = newValue
 }
}

Solution

  • In the macro declaration, take a WritableKeyPath:

    @attached(accessor, names: named(get), named(set))
    public macro GetSet<Root, Value>(_ keyPath: WritableKeyPath<Root, Value>) = #externalMacro(...)
    

    The implementation just uses the [keyPath: ...] subscript to get and set the desired key path.

    enum GetSetMacro: AccessorMacro {
        static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
            guard let keyPath = node.arguments?.as(LabeledExprListSyntax.self)?.first?.expression else {
                return []
            }
            return [
                """
                get { self[keyPath: \(keyPath)] }
                """,
                """
                set { self[keyPath: \(keyPath)] = newValue }
                """
            ]
        }
    }
    

    Example usage:

    class Foo {
        var value = 1
        
        @GetSet(\Foo.value)
        var newIntValue: Int
    }
    

    Notes: