From 15cf156cd5c25310ea39f45bfdeffea53b6f4a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20B=C4=85k?= Date: Sun, 22 Oct 2023 08:57:16 +0200 Subject: [PATCH] Incorporate ObservableMacro --- .../MacroExamples/ObservableMacro.swift | 173 ++++++++++++++++++ .../ObservableMacroTests.swift | 132 +++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift create mode 100644 Tests/MacroTestingTests/ObservableMacroTests.swift diff --git a/Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift b/Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift new file mode 100644 index 0000000..ec4669f --- /dev/null +++ b/Tests/MacroTestingTests/MacroExamples/ObservableMacro.swift @@ -0,0 +1,173 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +import SwiftSyntaxMacros + +private extension DeclSyntaxProtocol { + var isObservableStoredProperty: Bool { + if let property = self.as(VariableDeclSyntax.self), + let binding = property.bindings.first, + let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier, + identifier.text != "_registrar", identifier.text != "_storage", + binding.accessorBlock == nil + { + return true + } + + return false + } +} + +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(NamedDeclSyntax.self) else { + return [] + } + + let parentName = identified.name + + 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(_ apply: () throws -> T) rethrows -> T { + _registrar.beginAccess() + defer { _registrar.endAccess() } + return try apply() + } + """ + + let memberList = declaration.memberBlock.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: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [SwiftSyntax.AttributeSyntax] { + guard member.isObservableStoredProperty else { + return [] + } + + return [ + AttributeSyntax( + attributeName: IdentifierTypeSyntax( + name: .identifier("ObservableProperty") + ) + ) + ] + } + +} + +extension ObservableMacro: ExtensionMacro { + public static func expansion( + of node: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo protocols: [TypeSyntax], + in context: some MacroExpansionContext + ) throws -> [ExtensionDeclSyntax] { + [try ExtensionDeclSyntax("extension \(type): Observable {}")] + } +} + +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.accessorBlock == nil + else { + 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] + } +} diff --git a/Tests/MacroTestingTests/ObservableMacroTests.swift b/Tests/MacroTestingTests/ObservableMacroTests.swift new file mode 100644 index 0000000..f05027f --- /dev/null +++ b/Tests/MacroTestingTests/ObservableMacroTests.swift @@ -0,0 +1,132 @@ +import MacroTesting +import XCTest + +final class ObservableMacroTests: XCTestCase { + override func invokeTest() { + withMacroTesting( + macros: [ + "Observable": ObservableMacro.self, + "ObservableProperty": ObservablePropertyMacro.self + ] + ) { + super.invokeTest() + } + } + + func testExpansion() { + assertMacro { + """ + @Observable + final class Dog { + var name: String? + var treat: Treat? + + var isHappy: Bool = true + + init() {} + + func bark() { + print("bork bork") + } + } + """ + } expansion: { + #""" + final class Dog { + var name: String? { + get { + _registrar.beginAccess(\.name) + defer { + _registrar.endAccess() + } + return _storage.name + } + set { + _registrar.beginAccess(\.name) + _registrar.register(observable: self, willSet: \.name, to: newValue) + defer { + _registrar.register(observable: self, didSet: \.name) + _registrar.endAccess() + } + _storage.name = newValue + } + } + var treat: Treat? { + get { + _registrar.beginAccess(\.treat) + defer { + _registrar.endAccess() + } + return _storage.treat + } + set { + _registrar.beginAccess(\.treat) + _registrar.register(observable: self, willSet: \.treat, to: newValue) + defer { + _registrar.register(observable: self, didSet: \.treat) + _registrar.endAccess() + } + _storage.treat = newValue + } + } + + var isHappy: Bool = true { + get { + _registrar.beginAccess(\.isHappy) + defer { + _registrar.endAccess() + } + return _storage.isHappy + } + set { + _registrar.beginAccess(\.isHappy) + _registrar.register(observable: self, willSet: \.isHappy, to: newValue) + defer { + _registrar.register(observable: self, didSet: \.isHappy) + _registrar.endAccess() + } + _storage.isHappy = newValue + } + } + + init() {} + + func bark() { + print("bork bork") + } + + let _registrar = ObservationRegistrar() + + public nonisolated func addObserver(_ observer: some Observer) { + _registrar.addObserver(observer) + } + + public nonisolated func removeObserver(_ observer: some Observer) { + _registrar.removeObserver(observer) + } + + private func withTransaction(_ apply: () throws -> T) rethrows -> T { + _registrar.beginAccess() + defer { + _registrar.endAccess() + } + return try apply() + } + + private struct Storage { + + var name: String? + var treat: Treat? + + var isHappy: Bool = true + } + + private var _storage = Storage() + } + + extension Dog: Observable { + } + """# + } + } +}