// // ObservableMacro.swift // LuminateObservationMacros // // Created by Brendan Szymanski on 6/15/26. // import SwiftSyntax import SwiftSyntaxMacros /// The `@Observable` macro implementation. /// /// Expands in three phases: /// 1. **MemberMacro**: Adds `_$observationRegistrar` + one stored backing property /// (`_$`) per tracked `var`, preserving type and initial value. /// 2. **MemberAttributeMacro**: Applies `@ObservationTracked` to each stored `var`. /// 3. **AccessorMacro**: Wraps each tracked property with `get`/`set` that read/write /// the `_$` backing property and call `_$observationRegistrar.didChange()` on /// every set. No `init` accessor needed — the backing stored property handles /// the initial value naturally. public enum ObservableMacro: MemberMacro, MemberAttributeMacro, AccessorMacro { // MARK: - MemberMacro /// Adds the observation registrar and a stored backing property for each /// tracked `var`, preserving the original type and initializer expression. public static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, conformingTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax] { var members: [DeclSyntax] = [ """ public var _$observationRegistrar: ObservationRegistrar = .init() """ ] for member in declaration.memberBlock.members { guard let variableDecl = member.decl.as(VariableDeclSyntax.self), variableDecl.isStoredVar, !variableDecl.isObservationSynthesized else { continue } // For each binding in the declaration, synthesize a backing property for binding in variableDecl.bindings { guard let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { continue } let backingName = "_$" + name let initializer = binding.initializer?.trimmedDescription ?? "" // Include type annotation if present, otherwise let Swift infer from initializer if let typeAnnotation = binding.typeAnnotation { let type = typeAnnotation.type.trimmedDescription if initializer.isEmpty { members.append("internal var \(raw: backingName): \(raw: type)") } else { members.append( "internal var \(raw: backingName): \(raw: type) \(raw: initializer)") } } else if !initializer.isEmpty { members.append("internal var \(raw: backingName) \(raw: initializer)") } // If neither type annotation nor initializer, skip (would be a compile error in the original code anyway) } } return members } // MARK: - MemberAttributeMacro /// Adds `@ObservationTracked` to each stored `var` property (excluding synthesized backing members). public static func expansion( of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingAttributesFor member: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [AttributeSyntax] { guard let variableDecl = member.as(VariableDeclSyntax.self), variableDecl.isStoredVar, !variableDecl.isObservationSynthesized, !variableDecl.isBackingProperty else { return [] } return [ AttributeSyntax(stringLiteral: "@ObservationTracked") ] } // MARK: - AccessorMacro /// Wraps the property with `get`/`set` that read from / write to `_$` backing property /// and call `_$observationRegistrar.didChange()` on every set. public static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext ) throws -> [AccessorDeclSyntax] { guard let varDecl = declaration.as(VariableDeclSyntax.self), let binding = varDecl.bindings.first, let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text else { return [] } let backingName = "_$" + name return [ """ get { \(raw: backingName) } """, """ set { \(raw: backingName) = newValue _$observationRegistrar.didChange() } """, ] } } // MARK: - VariableDeclSyntax Helpers extension VariableDeclSyntax { /// Whether this declaration is a stored `var` (not `let`, not computed). var isStoredVar: Bool { guard bindingSpecifier.tokenKind == .keyword(.var) else { return false } return bindings.allSatisfy { $0.accessorBlock == nil } } /// Whether this declaration was synthesized by `@Observable` (`_$observationRegistrar`). var isObservationSynthesized: Bool { bindings.contains { binding in guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else { return false } return pattern.identifier.text == "_$observationRegistrar" } } /// Whether this declaration is a backing property (`_$`) synthesized by `@Observable`. var isBackingProperty: Bool { bindings.allSatisfy { binding in guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self) else { return false } return pattern.identifier.text.hasPrefix("_$") } } }