Skip to content

Commit

Permalink
Merge pull request DougGregor#6 from hborla/observable-macro
Browse files Browse the repository at this point in the history
Add an `Observable` macro implementation example.
  • Loading branch information
DougGregor authored Feb 3, 2023
2 parents d7f9a4d + d4330ea commit f5e6cb5
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 0 deletions.
4 changes: 4 additions & 0 deletions MacroExamples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
BDF5AFF82947E95C00FA119B /* StringifyMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5AFF72947E95C00FA119B /* StringifyMacro.swift */; };
BDFB14B52948484000708DA6 /* MacroExamplesPluginTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFB14B42948484000708DA6 /* MacroExamplesPluginTest.swift */; };
BDFB14B62948484000708DA6 /* libMacroExamplesPlugin.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = BDF5AFEE2947E61100FA119B /* libMacroExamplesPlugin.dylib */; platformFilters = (macos, ); };
EC21BDEB298D9F9900D585C6 /* ObservableMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC21BDEA298D9F9900D585C6 /* ObservableMacro.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
Expand Down Expand Up @@ -80,6 +81,7 @@
BDF5AFF72947E95C00FA119B /* StringifyMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StringifyMacro.swift; path = MacroExamplesPlugin/StringifyMacro.swift; sourceTree = SOURCE_ROOT; };
BDFB14B22948484000708DA6 /* MacroExamplesPluginTest.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MacroExamplesPluginTest.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
BDFB14B42948484000708DA6 /* MacroExamplesPluginTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacroExamplesPluginTest.swift; sourceTree = "<group>"; };
EC21BDEA298D9F9900D585C6 /* ObservableMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObservableMacro.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -123,6 +125,7 @@
BD841F81294CE1F600DA4D81 /* AddBlocker.swift */,
BD752BE4294D3BEC00D00A2E /* WarningMacro.swift */,
BD752BE6294D461B00D00A2E /* FontLiteralMacro.swift */,
EC21BDEA298D9F9900D585C6 /* ObservableMacro.swift */,
BD2CDEE8298B24040015A701 /* Diagnostics.swift */,
BD2CDEEA298B34730015A701 /* WrapStoredPropertiesMacro.swift */,
BD2CDEEC298B4A650015A701 /* DictionaryIndirectionMacro.swift */,
Expand Down Expand Up @@ -365,6 +368,7 @@
BD2CDEEB298B34730015A701 /* WrapStoredPropertiesMacro.swift in Sources */,
BD752BE5294D3BEC00D00A2E /* WarningMacro.swift in Sources */,
BDF5AFF82947E95C00FA119B /* StringifyMacro.swift in Sources */,
EC21BDEB298D9F9900D585C6 /* ObservableMacro.swift in Sources */,
BD2CDEE9298B24040015A701 /* Diagnostics.swift in Sources */,
BD2CDEED298B4A650015A701 /* DictionaryIndirectionMacro.swift in Sources */,
BD752BE7294D461B00D00A2E /* FontLiteralMacro.swift in Sources */,
Expand Down
25 changes: 25 additions & 0 deletions MacroExamples/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,28 @@ print("Point storage begins as an empty dictionary: \(point)")
print("Default value for point.x: \(point.x)")
point.y = 17
print("Point storage contains only the value we set: \(point)")

// MARK: - ObservableMacro

struct Treat {}

@Observable
final class Dog: Observable {
var name: String?
var treat: Treat?

var isHappy: Bool = true

init() {}

func bark() {
print("bork bork")
}
}

let dog = Dog()
print(dog.name ?? "")
dog.name = "George"
dog.treat = Treat()
print(dog.name ?? "")
dog.bark()
41 changes: 41 additions & 0 deletions MacroExamplesLib/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,44 @@ public macro wrapStoredProperties(_ attributeName: String) = #externalMacro(modu
@attached(member, names: named(_storage))
@attached(memberAttribute)
public macro DictionaryStorage() = #externalMacro(module: "MacroExamplesPlugin", type: "DictionaryStorageMacro")

public protocol Observable {}

public protocol Observer<Subject> {
associatedtype Subject: Observable
}

public struct ObservationRegistrar<Subject: Observable> {
public init() {}

public func addObserver(_ observer: some Observer<Subject>) {}

public func removeObserver(_ observer: some Observer<Subject>) {}

public func beginAccess<Value>(_ keyPath: KeyPath<Subject, Value>) {
print("beginning access for \(keyPath)")
}

public func beginAccess() {
print("beginning access in \(Subject.self)")
}

public func endAccess() {
print("ending access in \(Subject.self)")
}

public func register<Value>(observable: Subject, willSet: KeyPath<Subject, Value>, to: Value) {
print("registering willSet event for \(willSet)")
}

public func register<Value>(observable: Subject, didSet: KeyPath<Subject, Value>) {
print("registering didSet event for \(didSet)")
}
}

@attached(member)
@attached(memberAttribute)
public macro Observable() = #externalMacro(module: "MacroExamplesPlugin", type: "ObservableMacro")

@attached(accessor)
public macro ObservableProperty() = #externalMacro(module: "MacroExamplesPlugin", type: "ObservablePropertyMacro")
150 changes: 150 additions & 0 deletions MacroExamplesPlugin/ObservableMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import SwiftSyntax
import SwiftSyntaxMacros

private extension DeclSyntaxProtocol {
var isObservableStoredProperty: Bool {
guard let property = self.as(VariableDeclSyntax.self),
let binding = property.bindings.first
else {
return false
}

return binding.accessor == nil
}
}

public struct ObservableMacro: MemberMacro, MemberAttributeMacro {

// MARK: - MemberMacro

public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let identified = declaration.asProtocol(IdentifiedDeclSyntax.self) else {
return []
}

let parentName = identified.identifier

let registrar: DeclSyntax =
"""
let _registrar = ObservationRegistrar<\(parentName)>()
"""

let addObserver: DeclSyntax =
"""
public nonisolated func addObserver(_ observer: some Observer<\(parentName)>) {
_registrar.addObserver(observer)
}
"""

let removeObserver: DeclSyntax =
"""
public nonisolated func removeObserver(_ observer: some Observer<\(parentName)>) {
_registrar.removeObserver(observer)
}
"""

let withTransaction: DeclSyntax =
"""
private func withTransaction<T>(_ apply: () throws -> T) rethrows -> T {
_registrar.beginAccess()
defer { _registrar.endAccess() }
return try apply()
}
"""

let memberList = MemberDeclListSyntax(
declaration.members.members.filter {
$0.decl.isObservableStoredProperty
}
)

let storageStruct: DeclSyntax =
"""
private struct Storage {
\(memberList)
}
"""

let storage: DeclSyntax =
"""
private var _storage = Storage()
"""

return [
registrar,
addObserver,
removeObserver,
withTransaction,
storageStruct,
storage,
]
}

// MARK: - MemberAttributeMacro

public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: DeclSyntax,
in context: some MacroExpansionContext
) throws -> [SwiftSyntax.AttributeSyntax] {
guard member.isObservableStoredProperty else {
return []
}

return [
AttributeSyntax(
attributeName: SimpleTypeIdentifierSyntax(
name: .identifier("ObservableProperty")
)
)
]
}

}

public struct ObservablePropertyMacro: AccessorMacro {
public static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
guard let property = declaration.as(VariableDeclSyntax.self),
let binding = property.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
binding.accessor == nil
else {
return []
}

if identifier.text == "_registrar" || identifier.text == "_storage" { return [] }

let getAccessor: AccessorDeclSyntax =
"""
get {
_registrar.beginAccess(\\.\(identifier))
defer { _registrar.endAccess() }
return _storage.\(identifier)
}
"""

let setAccessor: AccessorDeclSyntax =
"""
set {
_registrar.beginAccess(\\.\(identifier))
_registrar.register(observable: self, willSet: \\.\(identifier), to: newValue)
defer {
_registrar.register(observable: self, didSet: \\.\(identifier))
_registrar.endAccess()
}
_storage.\(identifier) = newValue
}
"""

return [getAccessor, setAccessor]
}
}

0 comments on commit f5e6cb5

Please sign in to comment.