Skip to content

Commit eaebbed

Browse files
authored
Added Relayed macro (#9)
1 parent 5a8011d commit eaebbed

64 files changed

Lines changed: 5393 additions & 277 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Macros/RelayMacros/Combine/Common/ObservationIgnoredMacro.swift

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,3 @@ internal enum ObservationIgnoredMacro {
1212

1313
static let attribute: AttributeSyntax = "@ObservationIgnored"
1414
}
15-
16-
extension Property {
17-
18-
var isStoredObservationTracked: Bool {
19-
kind == .stored
20-
&& mutability == .mutable
21-
&& underlying.typeScopeSpecifier == nil
22-
&& underlying.overrideSpecifier == nil
23-
&& !underlying.attributes.contains(like: ObservationIgnoredMacro.attribute)
24-
}
25-
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// ObservationSuppressedMacro.swift
3+
// Relay
4+
//
5+
// Created by Kamil Strzelecki on 01/12/2025.
6+
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
7+
//
8+
9+
import SwiftSyntaxMacros
10+
11+
public enum ObservationSuppressedMacro {
12+
13+
static let attribute: AttributeSyntax = "@ObservationSuppressed"
14+
}
15+
16+
extension ObservationSuppressedMacro: PeerMacro {
17+
18+
public static func expansion(
19+
of _: AttributeSyntax,
20+
providingPeersOf _: some DeclSyntaxProtocol,
21+
in _: some MacroExpansionContext
22+
) -> [DeclSyntax] {
23+
[]
24+
}
25+
}
26+
27+
extension Property {
28+
29+
var isStoredObservationTracked: Bool {
30+
kind == .stored
31+
&& mutability == .mutable
32+
&& underlying.typeScopeSpecifier == nil
33+
&& underlying.overrideSpecifier == nil
34+
&& !underlying.attributes.contains(like: ObservationIgnoredMacro.attribute)
35+
&& !underlying.attributes.contains(like: ObservationSuppressedMacro.attribute)
36+
}
37+
}
38+
39+
extension FunctionDeclSyntax {
40+
41+
var isObservationTracked: Bool {
42+
!attributes.contains(like: ObservationIgnoredMacro.attribute)
43+
&& !attributes.contains(like: ObservationSuppressedMacro.attribute)
44+
}
45+
}

Macros/RelayMacros/Combine/Common/PropertyPublisherDeclBuilder.swift

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,7 @@ internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {
8989
private func storedPropertiesSubjectsFinishCalls() -> CodeBlockItemListSyntax {
9090
for property in properties.all where property.isStoredPublisherTracked {
9191
let call = storedPropertySubjectFinishCall(for: property)
92-
if let ifConfigCall = property.underlying.applyingEnclosingIfConfig(to: call) {
93-
ifConfigCall
94-
} else {
95-
call
96-
}
92+
call.withIfConfigIfPresent(from: property.underlying)
9793
}
9894
}
9995

@@ -105,11 +101,7 @@ internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {
105101
private func storedPropertiesPublishers() -> MemberBlockItemListSyntax {
106102
for property in properties.all where property.isStoredPublisherTracked {
107103
let publisher = storedPropertyPublisher(for: property)
108-
if let ifConfigPublisher = property.underlying.applyingEnclosingIfConfig(to: publisher) {
109-
ifConfigPublisher
110-
} else {
111-
publisher
112-
}
104+
publisher.withIfConfigIfPresent(from: property.underlying)
113105
}
114106
}
115107

@@ -131,11 +123,7 @@ internal struct PropertyPublisherDeclBuilder: ClassDeclBuilder, MemberBuilding {
131123
private func computedPropertiesPublishers() -> MemberBlockItemListSyntax {
132124
for property in properties.all where property.isComputedPublisherTracked {
133125
let publisher = computedPropertyPublisher(for: property)
134-
if let ifConfigPublisher = property.underlying.applyingEnclosingIfConfig(to: publisher) {
135-
ifConfigPublisher
136-
} else {
137-
publisher
138-
}
126+
publisher.withIfConfigIfPresent(from: property.underlying)
139127
}
140128
}
141129

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

155143
@MemberBlockItemListBuilder
156144
private func memoizedPropertiesPublishers() -> MemberBlockItemListSyntax {
157-
for member in declaration.memberBlock.members {
145+
for member in declaration.memberBlock.members.flattened {
158146
if let extractionResult = MemoizedMacro.extract(from: member.decl) {
159147
let declaration = extractionResult.declaration
160148

161-
if !declaration.attributes.contains(like: PublisherIgnoredMacro.attribute) {
149+
if declaration.isPublisherTracked {
162150
let publisher = memoizedPropertyPublisher(for: extractionResult)
163-
if let ifConfigPublisher = declaration.applyingEnclosingIfConfig(to: publisher) {
164-
ifConfigPublisher
165-
} else {
166-
publisher
167-
}
151+
publisher.withIfConfigIfPresent(from: extractionResult.declaration)
168152
}
169153
}
170154
}

Macros/RelayMacros/Combine/Common/PublisherIgnoredMacro.swift renamed to Macros/RelayMacros/Combine/Common/PublisherSuppressedMacro.swift

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// PublisherIgnoredMacro.swift
2+
// PublisherSuppressedMacro.swift
33
// Relay
44
//
55
// Created by Kamil Strzelecki on 22/11/2025.
@@ -8,12 +8,12 @@
88

99
import SwiftSyntaxMacros
1010

11-
public enum PublisherIgnoredMacro {
11+
public enum PublisherSuppressedMacro {
1212

13-
static let attribute: AttributeSyntax = "@PublisherIgnored"
13+
static let attribute: AttributeSyntax = "@PublisherSuppressed"
1414
}
1515

16-
extension PublisherIgnoredMacro: PeerMacro {
16+
extension PublisherSuppressedMacro: PeerMacro {
1717

1818
public static func expansion(
1919
of _: AttributeSyntax,
@@ -31,13 +31,20 @@ extension Property {
3131
&& mutability == .mutable
3232
&& underlying.typeScopeSpecifier == nil
3333
&& underlying.overrideSpecifier == nil
34-
&& !underlying.attributes.contains(like: PublisherIgnoredMacro.attribute)
34+
&& !underlying.attributes.contains(like: PublisherSuppressedMacro.attribute)
3535
}
3636

3737
var isComputedPublisherTracked: Bool {
3838
kind == .computed
3939
&& underlying.typeScopeSpecifier == nil
4040
&& underlying.overrideSpecifier == nil
41-
&& !underlying.attributes.contains(like: PublisherIgnoredMacro.attribute)
41+
&& !underlying.attributes.contains(like: PublisherSuppressedMacro.attribute)
42+
}
43+
}
44+
45+
extension FunctionDeclSyntax {
46+
47+
var isPublisherTracked: Bool {
48+
!attributes.contains(like: PublisherSuppressedMacro.attribute)
4249
}
4350
}

Macros/RelayMacros/Combine/Publishable/ObservationRegistrarDeclBuilder.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,7 @@ internal struct ObservationRegistrarDeclBuilder: ClassDeclBuilder, MemberBuildin
6464
private func publishKeyPathLookups() -> CodeBlockItemListSyntax {
6565
for property in trackedProperties {
6666
let lookup = publishKeyPathLookup(for: property)
67-
if let ifConfigLookup = property.underlying.applyingEnclosingIfConfig(to: lookup) {
68-
ifConfigLookup
69-
} else {
70-
lookup
71-
}
67+
lookup.withIfConfigIfPresent(from: property.underlying)
7268
}
7369
}
7470

Macros/RelayMacros/Combine/Publishable/PublishableMacro.swift

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public enum PublishableMacro {
1212

1313
static let attribute: AttributeSyntax = "@Publishable"
1414

15-
private static func validate(
15+
private static func validateNode(
1616
_ node: AttributeSyntax,
1717
attachedTo declaration: some DeclGroupSyntax,
1818
in context: some MacroExpansionContext
@@ -24,6 +24,23 @@ public enum PublishableMacro {
2424
)
2525
}
2626

27+
if declaration.attributes.contains(like: ObservableMacro.attribute) {
28+
context.diagnose(
29+
node: declaration,
30+
warningMessage: """
31+
@Publishable macro should be used with macros other than @Observable, \
32+
that supply their own Observable protocol conformance
33+
""",
34+
fixIts: [
35+
.replace(
36+
message: MacroExpansionFixItMessage("Apply @Relayed macro"),
37+
oldNode: node.attributeName,
38+
newNode: RelayedMacro.attribute.attributeName.withTrivia(from: node.attributeName)
39+
)
40+
]
41+
)
42+
}
43+
2744
if declaration.attributes.contains(like: SwiftDataModelMacro.attribute) {
2845
context.diagnose(
2946
node: declaration,
@@ -32,10 +49,9 @@ public enum PublishableMacro {
3249
but internals of SwiftData are incompatible with custom ObservationRegistrar
3350
""",
3451
fixIts: [
35-
.replace(
36-
message: MacroExpansionFixItMessage("Remove @Publishable macro"),
37-
oldNode: node,
38-
newNode: "\(node.leadingTrivia)" as TokenSyntax
52+
.remove(
53+
message: "Remove @Publishable macro",
54+
oldNode: node
3955
)
4056
]
4157
)
@@ -53,14 +69,14 @@ extension PublishableMacro: MemberMacro {
5369
conformingTo protocols: [TypeSyntax],
5470
in context: some MacroExpansionContext
5571
) throws -> [DeclSyntax] {
56-
let declaration = try validate(node, attachedTo: declaration, in: context)
57-
let properties = try PropertiesParser.parse(memberBlock: declaration.memberBlock)
72+
let declaration = try validateNode(node, attachedTo: declaration, in: context)
73+
let properties = try PropertiesParser.parse(declarationGroup: declaration)
5874
let parameters = try Parameters(from: node)
5975

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

63-
let builderTypes: [any ClassDeclBuilder] = [
79+
let builders: [any ClassDeclBuilder] = [
6480
PublisherDeclBuilder(
6581
declaration: declaration,
6682
trimmedSuperclassType: trimmedSuperclassType
@@ -79,8 +95,8 @@ extension PublishableMacro: MemberMacro {
7995
)
8096
]
8197

82-
return try builderTypes.flatMap { builderType in
83-
try builderType.build()
98+
return try builders.flatMap { builder in
99+
try builder.build()
84100
}
85101
}
86102
}
@@ -98,7 +114,7 @@ extension PublishableMacro: ExtensionMacro {
98114
return []
99115
}
100116

101-
let declaration = try validate(node, attachedTo: declaration, in: context)
117+
let declaration = try validateNode(node, attachedTo: declaration, in: context)
102118
let parameters = try Parameters(from: node)
103119

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

109125
return [
110-
.init(
126+
ExtensionDeclSyntax(
111127
attributes: declaration.availability ?? [],
112128
extendedType: type,
113-
inheritanceClause: .init(
129+
inheritanceClause: InheritanceClauseSyntax(
114130
inheritedTypes: [
115131
InheritedTypeSyntax(
116132
type: AttributedTypeSyntax(
117133
globalActorIsolation: globalActorIsolation,
118-
baseType: IdentifierTypeSyntax(name: "Publishable")
134+
baseType: "Relay.Publishable"
119135
)
120136
)
121137
]
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
//
2+
// ObservableDeclBuilder.swift
3+
// Relay
4+
//
5+
// Created by Kamil Strzelecki on 28/11/2025.
6+
// Copyright © 2025 Kamil Strzelecki. All rights reserved.
7+
//
8+
9+
import SwiftSyntaxMacros
10+
11+
internal struct ObservableDeclBuilder: ClassDeclBuilder, MemberBuilding {
12+
13+
let declaration: ClassDeclSyntax
14+
private let genericParameter: TokenSyntax
15+
16+
init(
17+
declaration: ClassDeclSyntax,
18+
context: some MacroExpansionContext
19+
) {
20+
self.declaration = declaration
21+
self.genericParameter = context.makeUniqueName("T")
22+
}
23+
24+
func build() -> [DeclSyntax] {
25+
[
26+
observationRegistrarProperty(),
27+
shouldNotifyObserversFunction(),
28+
shouldNotifyObserversEquatableFunction(),
29+
shouldNotifyObserversAnyObjectFunction(),
30+
shouldNotifyObserversAnyObjectEquatableFunction()
31+
]
32+
}
33+
34+
private func observationRegistrarProperty() -> DeclSyntax {
35+
"private let _$observationRegistrar = Observation.ObservationRegistrar()"
36+
}
37+
38+
private func shouldNotifyObserversFunction() -> DeclSyntax {
39+
"""
40+
private nonisolated func shouldNotifyObservers<\(genericParameter)>(
41+
_ lhs: \(genericParameter),
42+
_ rhs: \(genericParameter)
43+
) -> Bool {
44+
true
45+
}
46+
"""
47+
}
48+
49+
private func shouldNotifyObserversEquatableFunction() -> DeclSyntax {
50+
"""
51+
private nonisolated func shouldNotifyObservers<\(genericParameter): Equatable>(
52+
_ lhs: \(genericParameter),
53+
_ rhs: \(genericParameter)
54+
) -> Bool {
55+
lhs != rhs
56+
}
57+
"""
58+
}
59+
60+
private func shouldNotifyObserversAnyObjectFunction() -> DeclSyntax {
61+
"""
62+
private nonisolated func shouldNotifyObservers<\(genericParameter): AnyObject>(
63+
_ lhs: \(genericParameter),
64+
_ rhs: \(genericParameter)
65+
) -> Bool {
66+
lhs !== rhs
67+
}
68+
"""
69+
}
70+
71+
private func shouldNotifyObserversAnyObjectEquatableFunction() -> DeclSyntax {
72+
"""
73+
private nonisolated func shouldNotifyObservers<\(genericParameter): AnyObject & Equatable>(
74+
_ lhs: \(genericParameter),
75+
_ rhs: \(genericParameter)
76+
) -> Bool {
77+
lhs != rhs
78+
}
79+
"""
80+
}
81+
}

0 commit comments

Comments
 (0)