192 lines
6.4 KiB
Swift
192 lines
6.4 KiB
Swift
|
import SwiftCompilerPlugin
|
||
|
import SwiftDiagnostics
|
||
|
import SwiftSyntax
|
||
|
import SwiftSyntaxBuilder
|
||
|
import SwiftSyntaxMacros
|
||
|
|
||
|
/**
|
||
|
Macro declaration for the ``ObservableDefault`` macro.
|
||
|
*/
|
||
|
public struct ObservableDefaultMacro {}
|
||
|
|
||
|
/**
|
||
|
Conforming to ``AccessorMacro`` allows us to add the property accessors (get/set) that integrate with ``Observable``.
|
||
|
*/
|
||
|
extension ObservableDefaultMacro: AccessorMacro {
|
||
|
public static func expansion(
|
||
|
of node: AttributeSyntax,
|
||
|
providingAccessorsOf declaration: some DeclSyntaxProtocol,
|
||
|
in context: some MacroExpansionContext
|
||
|
) throws(ObservableDefaultMacroError) -> [AccessorDeclSyntax] {
|
||
|
let property = try propertyPattern(of: declaration)
|
||
|
let expression = try keyExpression(of: node)
|
||
|
let associatedKey = associatedKeyToken(for: property)
|
||
|
|
||
|
// The get/set accessors follow the same pattern that @Observable uses to handle the mutations.
|
||
|
//
|
||
|
// The get accessor also sets up an observation to update the value when the UserDefaults
|
||
|
// changes from elsewhere. Doing so requires attaching it as an Objective-C associated
|
||
|
// object due to limitations with current macro capabilities and Swift concurrency.
|
||
|
return [
|
||
|
#"""
|
||
|
get {
|
||
|
if objc_getAssociatedObject(self, &Self.\#(associatedKey)) == nil {
|
||
|
let cancellable = Defaults.publisher(\#(expression))
|
||
|
.sink { [weak self] in
|
||
|
self?.\#(property) = $0.newValue
|
||
|
}
|
||
|
objc_setAssociatedObject(self, &Self.\#(associatedKey), cancellable, .OBJC_ASSOCIATION_RETAIN)
|
||
|
}
|
||
|
access(keyPath: \.\#(property))
|
||
|
return Defaults[\#(expression)]
|
||
|
}
|
||
|
"""#,
|
||
|
#"""
|
||
|
set {
|
||
|
withMutation(keyPath: \.\#(property)) {
|
||
|
Defaults[\#(expression)] = newValue
|
||
|
}
|
||
|
}
|
||
|
"""#
|
||
|
]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Conforming to ``PeerMacro`` we can add a new property of type Defaults.Observation that will update the original property whenever
|
||
|
the UserDefaults value changes outside the class.
|
||
|
*/
|
||
|
extension ObservableDefaultMacro: PeerMacro {
|
||
|
public static func expansion(
|
||
|
of node: SwiftSyntax.AttributeSyntax,
|
||
|
providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol,
|
||
|
in context: some SwiftSyntaxMacros.MacroExpansionContext
|
||
|
) throws -> [SwiftSyntax.DeclSyntax] {
|
||
|
let property = try propertyPattern(of: declaration)
|
||
|
let associatedKey = associatedKeyToken(for: property)
|
||
|
|
||
|
return [
|
||
|
"private nonisolated(unsafe) static var \(associatedKey): Void?"
|
||
|
]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Logic used by both macro implementations
|
||
|
extension ObservableDefaultMacro {
|
||
|
/**
|
||
|
Extracts the pattern (i.e. the name) of the attached property.
|
||
|
*/
|
||
|
private static func propertyPattern(
|
||
|
of declaration: some SwiftSyntax.DeclSyntaxProtocol
|
||
|
) throws(ObservableDefaultMacroError) -> TokenSyntax {
|
||
|
// Must be attached to a property declaration.
|
||
|
guard let variableDeclaration = declaration.as(VariableDeclSyntax.self) else {
|
||
|
throw .notAttachedToProperty
|
||
|
}
|
||
|
|
||
|
// Must be attached to a variable property (i.e. `var` and not `let`).
|
||
|
guard variableDeclaration.bindingSpecifier.tokenKind == .keyword(.var) else {
|
||
|
throw .notAttachedToVariable
|
||
|
}
|
||
|
|
||
|
// Must be attached to a single property.
|
||
|
guard variableDeclaration.bindings.count == 1, let binding = variableDeclaration.bindings.first else {
|
||
|
throw .notAttachedToSingleProperty
|
||
|
}
|
||
|
|
||
|
// Must not provide an initializer for the property (i.e. not assign a value).
|
||
|
guard binding.initializer == nil else {
|
||
|
throw .attachedToPropertyWithInitializer
|
||
|
}
|
||
|
|
||
|
// Must not be attached to property with existing accessor block.
|
||
|
guard binding.accessorBlock == nil else {
|
||
|
throw .attachedToPropertyWithAccessorBlock
|
||
|
}
|
||
|
|
||
|
// Must use Identifier Pattern.
|
||
|
// See https://swiftinit.org/docs/swift-syntax/swiftsyntax/identifierpatternsyntax
|
||
|
guard let pattern = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
|
||
|
throw .attachedToPropertyWithoutIdentifierProperty
|
||
|
}
|
||
|
|
||
|
return pattern
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Extracts the expression used to define the Defaults.Key in the macro call.
|
||
|
*/
|
||
|
private static func keyExpression(
|
||
|
of node: AttributeSyntax
|
||
|
) throws(ObservableDefaultMacroError) -> ExprSyntax {
|
||
|
// Must receive arguments
|
||
|
guard let arguments = node.arguments else {
|
||
|
throw .calledWithoutArguments
|
||
|
}
|
||
|
|
||
|
// Must be called with Labeled Expression.
|
||
|
// See https://swiftinit.org/docs/swift-syntax/swiftsyntax/labeledexprlistsyntax
|
||
|
guard let expressionList = arguments.as(LabeledExprListSyntax.self) else {
|
||
|
throw .calledWithoutLabeledExpression
|
||
|
}
|
||
|
|
||
|
// Must only receive one argument.
|
||
|
guard expressionList.count == 1, let expression = expressionList.first?.expression else {
|
||
|
throw .calledWithMultipleArguments
|
||
|
}
|
||
|
|
||
|
return expression
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Generates the token to use as key for the associated object used to hold the UserDefaults observation.
|
||
|
*/
|
||
|
private static func associatedKeyToken(for property: TokenSyntax) -> TokenSyntax {
|
||
|
"_objcAssociatedKey_\(property)"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
Error handling for ``ObservableDefaultMacro``.
|
||
|
*/
|
||
|
public enum ObservableDefaultMacroError: Error {
|
||
|
case notAttachedToProperty
|
||
|
case notAttachedToVariable
|
||
|
case notAttachedToSingleProperty
|
||
|
case attachedToPropertyWithInitializer
|
||
|
case attachedToPropertyWithAccessorBlock
|
||
|
case attachedToPropertyWithoutIdentifierProperty
|
||
|
case calledWithoutArguments
|
||
|
case calledWithoutLabeledExpression
|
||
|
case calledWithMultipleArguments
|
||
|
case calledWithoutFunctionSyntax
|
||
|
case calledWithoutKeyArgument
|
||
|
case calledWithUnsupportedExpression
|
||
|
}
|
||
|
|
||
|
extension ObservableDefaultMacroError: CustomStringConvertible {
|
||
|
public var description: String {
|
||
|
switch self {
|
||
|
case .notAttachedToProperty:
|
||
|
"@ObservableDefault must be attached to a property."
|
||
|
case .notAttachedToVariable:
|
||
|
"@ObservableDefault must be attached to a `var` property."
|
||
|
case .notAttachedToSingleProperty:
|
||
|
"@ObservableDefault can only be attached to a single property."
|
||
|
case .attachedToPropertyWithInitializer:
|
||
|
"@ObservableDefault must not be attached with a property with a value assigned. To create set default value, provide it in the `Defaults.Key` definition."
|
||
|
case .attachedToPropertyWithAccessorBlock:
|
||
|
"@ObservableDefault must not be attached to a property with accessor block."
|
||
|
case .attachedToPropertyWithoutIdentifierProperty:
|
||
|
"@ObservableDefault could not identify the attached property."
|
||
|
case .calledWithoutArguments,
|
||
|
.calledWithoutLabeledExpression,
|
||
|
.calledWithMultipleArguments,
|
||
|
.calledWithoutFunctionSyntax,
|
||
|
.calledWithoutKeyArgument,
|
||
|
.calledWithUnsupportedExpression:
|
||
|
"@ObservableDefault must be called with (1) argument of type `Defaults.Key`"
|
||
|
}
|
||
|
}
|
||
|
}
|