rayaantaneja
rayaantaneja

Reputation: 1748

How to account for @Observable Macro expansion when creating your own Peer Macro in Swift

I've made a macro called Resettable which creates a private constant initialised with the same value as the declaration the macro is attached to:

public struct ResettableMacro: PeerMacro {
    public static func expansion(of node: AttributeSyntax,
                                 providingPeersOf declaration: some DeclSyntaxProtocol,
                                 in context: some MacroExpansionContext)
    throws -> [DeclSyntax] {
        guard let variableDecl = declaration.as(VariableDeclSyntax.self) else {
            print("Can only be applied to variable declarations") 
            return []
        }
        guard variableDecl.bindingSpecifier.text == "var" else {
            print("Can only be a variable")
            return []
        }
        guard variableDecl.bindings.allSatisfy({ $0.initializer != nil }) else {
            print("Must be an initialised stored property")
            return []
        }
        
        // abstracted extracting the identifier pattern and the initial value for clarity
        return zip(variableDecl.identifiers, variableDecl.values).map { name, value in
            "private let $\(raw: name) = \(raw: value)"
        }
    }
}

This works very well, except when I use @Resettable on a class marked with @Observable. When I do that I get the following error which I don't understand:

'self' used in property access '$_name' before 'self.init' call

@Observable
class Person { 
    @Resettable var name = "John Doe"
        // << error here due to @ObservationTracked
}

This is because the @Observable macro adds another declaration:

@Resettable  @ObservationIgnored private var _name  = "John Doe"

which is emitting this error.

I thought this wouldn't be a problem because @Resettable will only see "The original version of the declaration without expansions provided by others" (from the "Expand on Swift macros" talk). Even if you consider what the @Observable macro is doing, it's creating one underscored property (_name), and creating accessors for the non-underscored version (name). Since in my macro implementation, an initialiser is required, the name property with a getter and setter would be skipped as there is no initialiser. Yet, I am still getting an error. In fact, I don't even understand what this error means and why it's coming.

I initially thought of skipping delcarations marked with @ObservationTracked, but this doesn't account for other macros that might expand similarly, nor if the @ObservationTracked macro's name changes.

I would love to know how I can account for the @Observable expansion in my macro in the simplest way.

Upvotes: 1

Views: 326

Answers (0)

Related Questions