162 lines
5.8 KiB
Swift
162 lines
5.8 KiB
Swift
//
|
|
// 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
|
|
/// (`_$<name>`) 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 `_$<name>` 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 `_$<name>` 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 (`_$<name>`) 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("_$")
|
|
}
|
|
}
|
|
}
|