Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions Macros/RelayMacros/Combine/Common/ObservationIgnoredMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,3 @@ internal enum ObservationIgnoredMacro {

static let attribute: AttributeSyntax = "@ObservationIgnored"
}

extension Property {

var isStoredObservationTracked: Bool {
kind == .stored
&& mutability == .mutable
&& underlying.typeScopeSpecifier == nil
&& underlying.overrideSpecifier == nil
&& !underlying.attributes.contains(like: ObservationIgnoredMacro.attribute)
}
}
45 changes: 45 additions & 0 deletions Macros/RelayMacros/Combine/Common/ObservationSuppressedMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// ObservationSuppressedMacro.swift
// Relay
//
// Created by Kamil Strzelecki on 01/12/2025.
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
//

import SwiftSyntaxMacros

public enum ObservationSuppressedMacro {

static let attribute: AttributeSyntax = "@ObservationSuppressed"
}

extension ObservationSuppressedMacro: PeerMacro {

public static func expansion(
of _: AttributeSyntax,
providingPeersOf _: some DeclSyntaxProtocol,
in _: some MacroExpansionContext
) -> [DeclSyntax] {
[]
}
}

extension Property {

var isStoredObservationTracked: Bool {
kind == .stored
&& mutability == .mutable
&& underlying.typeScopeSpecifier == nil
&& underlying.overrideSpecifier == nil
&& !underlying.attributes.contains(like: ObservationIgnoredMacro.attribute)
&& !underlying.attributes.contains(like: ObservationSuppressedMacro.attribute)
}
}

extension FunctionDeclSyntax {

var isObservationTracked: Bool {
!attributes.contains(like: ObservationIgnoredMacro.attribute)
&& !attributes.contains(like: ObservationSuppressedMacro.attribute)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,7 @@ internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {
private func storedPropertiesSubjectsFinishCalls() -> CodeBlockItemListSyntax {
for property in properties.all where property.isStoredPublisherTracked {
let call = storedPropertySubjectFinishCall(for: property)
if let ifConfigCall = property.underlying.applyingEnclosingIfConfig(to: call) {
ifConfigCall
} else {
call
}
call.withIfConfigIfPresent(from: property.underlying)
}
}

Expand All @@ -105,11 +101,7 @@ internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {
private func storedPropertiesPublishers() -> MemberBlockItemListSyntax {
for property in properties.all where property.isStoredPublisherTracked {
let publisher = storedPropertyPublisher(for: property)
if let ifConfigPublisher = property.underlying.applyingEnclosingIfConfig(to: publisher) {
ifConfigPublisher
} else {
publisher
}
publisher.withIfConfigIfPresent(from: property.underlying)
}
}

Expand All @@ -131,11 +123,7 @@ internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {
private func computedPropertiesPublishers() -> MemberBlockItemListSyntax {
for property in properties.all where property.isComputedPublisherTracked {
let publisher = computedPropertyPublisher(for: property)
if let ifConfigPublisher = property.underlying.applyingEnclosingIfConfig(to: publisher) {
ifConfigPublisher
} else {
publisher
}
publisher.withIfConfigIfPresent(from: property.underlying)
}
}

Expand All @@ -154,17 +142,13 @@ internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {

@MemberBlockItemListBuilder
private func memoizedPropertiesPublishers() -> MemberBlockItemListSyntax {
for member in declaration.memberBlock.members {
for member in declaration.memberBlock.members.flattened {
if let extractionResult = MemoizedMacro.extract(from: member.decl) {
let declaration = extractionResult.declaration

if !declaration.attributes.contains(like: PublisherIgnoredMacro.attribute) {
if declaration.isPublisherTracked {
let publisher = memoizedPropertyPublisher(for: extractionResult)
if let ifConfigPublisher = declaration.applyingEnclosingIfConfig(to: publisher) {
ifConfigPublisher
} else {
publisher
}
publisher.withIfConfigIfPresent(from: extractionResult.declaration)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// PublisherIgnoredMacro.swift
// PublisherSuppressedMacro.swift
// Relay
//
// Created by Kamil Strzelecki on 22/11/2025.
Expand All @@ -8,12 +8,12 @@

import SwiftSyntaxMacros

public enum PublisherIgnoredMacro {
public enum PublisherSuppressedMacro {

static let attribute: AttributeSyntax = "@PublisherIgnored"
static let attribute: AttributeSyntax = "@PublisherSuppressed"
}

extension PublisherIgnoredMacro: PeerMacro {
extension PublisherSuppressedMacro: PeerMacro {

public static func expansion(
of _: AttributeSyntax,
Expand All @@ -31,13 +31,20 @@ extension Property {
&& mutability == .mutable
&& underlying.typeScopeSpecifier == nil
&& underlying.overrideSpecifier == nil
&& !underlying.attributes.contains(like: PublisherIgnoredMacro.attribute)
&& !underlying.attributes.contains(like: PublisherSuppressedMacro.attribute)
}

var isComputedPublisherTracked: Bool {
kind == .computed
&& underlying.typeScopeSpecifier == nil
&& underlying.overrideSpecifier == nil
&& !underlying.attributes.contains(like: PublisherIgnoredMacro.attribute)
&& !underlying.attributes.contains(like: PublisherSuppressedMacro.attribute)
}
}

extension FunctionDeclSyntax {

var isPublisherTracked: Bool {
!attributes.contains(like: PublisherSuppressedMacro.attribute)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,7 @@ internal struct ObservationRegistrarDeclBuilder: ClassDeclBuilder, MemberBuildin
private func publishKeyPathLookups() -> CodeBlockItemListSyntax {
for property in trackedProperties {
let lookup = publishKeyPathLookup(for: property)
if let ifConfigLookup = property.underlying.applyingEnclosingIfConfig(to: lookup) {
ifConfigLookup
} else {
lookup
}
lookup.withIfConfigIfPresent(from: property.underlying)
}
}

Expand Down
44 changes: 30 additions & 14 deletions Macros/RelayMacros/Combine/Publishable/PublishableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public enum PublishableMacro {

static let attribute: AttributeSyntax = "@Publishable"

private static func validate(
private static func validateNode(
_ node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
Expand All @@ -24,6 +24,23 @@ public enum PublishableMacro {
)
}

if declaration.attributes.contains(like: ObservableMacro.attribute) {
context.diagnose(
node: declaration,
warningMessage: """
@Publishable macro should be used with macros other than @Observable, \
that supply their own Observable protocol conformance
""",
fixIts: [
.replace(
message: MacroExpansionFixItMessage("Apply @Relayed macro"),
oldNode: node.attributeName,
newNode: RelayedMacro.attribute.attributeName.withTrivia(from: node.attributeName)
)
]
)
}

if declaration.attributes.contains(like: SwiftDataModelMacro.attribute) {
context.diagnose(
node: declaration,
Expand All @@ -32,10 +49,9 @@ public enum PublishableMacro {
but internals of SwiftData are incompatible with custom ObservationRegistrar
""",
fixIts: [
.replace(
message: MacroExpansionFixItMessage("Remove @Publishable macro"),
oldNode: node,
newNode: "\(node.leadingTrivia)" as TokenSyntax
.remove(
message: "Remove @Publishable macro",
oldNode: node
)
]
)
Expand All @@ -53,14 +69,14 @@ extension PublishableMacro: MemberMacro {
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let declaration = try validate(node, attachedTo: declaration, in: context)
let properties = try PropertiesParser.parse(memberBlock: declaration.memberBlock)
let declaration = try validateNode(node, attachedTo: declaration, in: context)
let properties = try PropertiesParser.parse(declarationGroup: declaration)
let parameters = try Parameters(from: node)

let hasPublishableSuperclass = protocols.isEmpty
let trimmedSuperclassType = hasPublishableSuperclass ? declaration.possibleSuperclassType : nil

let builderTypes: [any ClassDeclBuilder] = [
let builders: [any ClassDeclBuilder] = [
PublisherDeclBuilder(
declaration: declaration,
trimmedSuperclassType: trimmedSuperclassType
Expand All @@ -79,8 +95,8 @@ extension PublishableMacro: MemberMacro {
)
]

return try builderTypes.flatMap { builderType in
try builderType.build()
return try builders.flatMap { builder in
try builder.build()
}
}
}
Expand All @@ -98,7 +114,7 @@ extension PublishableMacro: ExtensionMacro {
return []
}

let declaration = try validate(node, attachedTo: declaration, in: context)
let declaration = try validateNode(node, attachedTo: declaration, in: context)
let parameters = try Parameters(from: node)

let globalActorIsolation = GlobalActorIsolation.resolved(
Expand All @@ -107,15 +123,15 @@ extension PublishableMacro: ExtensionMacro {
)

return [
.init(
ExtensionDeclSyntax(
attributes: declaration.availability ?? [],
extendedType: type,
inheritanceClause: .init(
inheritanceClause: InheritanceClauseSyntax(
inheritedTypes: [
InheritedTypeSyntax(
type: AttributedTypeSyntax(
globalActorIsolation: globalActorIsolation,
baseType: IdentifierTypeSyntax(name: "Publishable")
baseType: "Relay.Publishable"
)
)
]
Expand Down
81 changes: 81 additions & 0 deletions Macros/RelayMacros/Combine/Relayed/ObservableDeclBuilder.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//
// ObservableDeclBuilder.swift
// Relay
//
// Created by Kamil Strzelecki on 28/11/2025.
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
//

import SwiftSyntaxMacros

internal struct ObservableDeclBuilder: ClassDeclBuilder, MemberBuilding {

let declaration: ClassDeclSyntax
private let genericParameter: TokenSyntax

init(
declaration: ClassDeclSyntax,
context: some MacroExpansionContext
) {
self.declaration = declaration
self.genericParameter = context.makeUniqueName("T")
}

func build() -> [DeclSyntax] {
[
observationRegistrarProperty(),
shouldNotifyObserversFunction(),
shouldNotifyObserversEquatableFunction(),
shouldNotifyObserversAnyObjectFunction(),
shouldNotifyObserversAnyObjectEquatableFunction()
]
}

private func observationRegistrarProperty() -> DeclSyntax {
"private let _$observationRegistrar = Observation.ObservationRegistrar()"
}

private func shouldNotifyObserversFunction() -> DeclSyntax {
"""
private nonisolated func shouldNotifyObservers<\(genericParameter)>(
_ lhs: \(genericParameter),
_ rhs: \(genericParameter)
) -> Bool {
true
}
"""
}

private func shouldNotifyObserversEquatableFunction() -> DeclSyntax {
"""
private nonisolated func shouldNotifyObservers<\(genericParameter): Equatable>(
_ lhs: \(genericParameter),
_ rhs: \(genericParameter)
) -> Bool {
lhs != rhs
}
"""
}

private func shouldNotifyObserversAnyObjectFunction() -> DeclSyntax {
"""
private nonisolated func shouldNotifyObservers<\(genericParameter): AnyObject>(
_ lhs: \(genericParameter),
_ rhs: \(genericParameter)
) -> Bool {
lhs !== rhs
}
"""
}

private func shouldNotifyObserversAnyObjectEquatableFunction() -> DeclSyntax {
"""
private nonisolated func shouldNotifyObservers<\(genericParameter): AnyObject & Equatable>(
_ lhs: \(genericParameter),
_ rhs: \(genericParameter)
) -> Bool {
lhs != rhs
}
"""
}
}
Loading
Loading