hariszaman
hariszaman

Reputation: 8432

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
 }
}

Upvotes: 3

Views: 357

Answers (2)

Sweeper
Sweeper

Reputation: 273850

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:

  • Type checking is performed after macro expansion. That is, you cannot check that the key path's root type matches the type of the enclosing class/struct, or that the type of the property matches the type of the key path.
  • You have to write a full key path like \Foo.value. The compiler cannot infer the root type.

Upvotes: 0

Bram
Bram

Reputation: 3283

I created a basic solution to your problem; both with the String approach as well as with the KeyPath approach.

extension String: Error {}

public struct GetSetMacro: AccessorMacro {
    public static func expansion(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
        guard let arguments = node.arguments,
              let expression = arguments.as(LabeledExprListSyntax.self)?.first?.expression else {
            throw "Incorrect argument"
        }

        if let string = expression.as(StringLiteralExprSyntax.self) {
            return [
                "get { \(raw: string.segments) }",
                "set { \(raw: string.segments) = newValue }",
            ]
        } else if let keyPath = expression.as(KeyPathExprSyntax.self) {
            return [
                "get { self[keyPath: \(raw: keyPath.description)] }",
                "set { self[keyPath: \(raw: keyPath.description)] = newValue }"
            ]
        } else {
            throw "Unsupported argument"
        }
    }
}

So in order to extract the variable passed into your macro, you'd have to traverse the node.arguments in some way. If you use the Xcode template to create your macro, you can breakpoint inside your macro code and use lldb to see how you can extract the data you'd like to have.

Upvotes: 0

Related Questions