Skip to content

Commit 40a7396

Browse files
authored
Make the @Observable macro class only (#67033)
* Make ObservationRegistrar Codable/Hashable These conformances enable automatic Codable synthesis for Observable types, and smooth the runway for structs being supported by the Observable macro in the future. * Limit Observable macro to classes This removes the ability for the Observable macro to apply to structs, and adds diagnostic tests for the three disallowed declaration kinds.
1 parent b426c15 commit 40a7396

File tree

4 files changed

+61
-46
lines changed

4 files changed

+61
-46
lines changed

lib/Macros/Sources/ObservationMacros/Extensions.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,8 @@ extension DeclGroupSyntax {
275275
var isEnum: Bool {
276276
return self.is(EnumDeclSyntax.self)
277277
}
278+
279+
var isStruct: Bool {
280+
return self.is(StructDeclSyntax.self)
281+
}
278282
}

lib/Macros/Sources/ObservationMacros/ObservableMacro.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,15 @@ extension ObservableMacro: MemberMacro {
206206

207207
if declaration.isEnum {
208208
// enumerations cannot store properties
209-
throw DiagnosticsError(syntax: node, message: "@Observable cannot be applied to enumeration type \(observableType.text)", id: .invalidApplication)
209+
throw DiagnosticsError(syntax: node, message: "'@Observable' cannot be applied to enumeration type '\(observableType.text)'", id: .invalidApplication)
210+
}
211+
if declaration.isStruct {
212+
// structs are not yet supported; copying/mutation semantics tbd
213+
throw DiagnosticsError(syntax: node, message: "'@Observable' cannot be applied to struct type '\(observableType.text)'", id: .invalidApplication)
210214
}
211215
if declaration.isActor {
212216
// actors cannot yet be supported for their isolation
213-
throw DiagnosticsError(syntax: node, message: "@Observable cannot be applied to actor type \(observableType.text)", id: .invalidApplication)
217+
throw DiagnosticsError(syntax: node, message: "'@Observable' cannot be applied to actor type '\(observableType.text)'", id: .invalidApplication)
214218
}
215219

216220
var declarations = [DeclSyntax]()

stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,29 @@ public struct ObservationRegistrar: Sendable {
162162
defer { didSet(subject, keyPath: keyPath) }
163163
return try mutation()
164164
}
165-
}
165+
}
166+
167+
@available(SwiftStdlib 5.9, *)
168+
extension ObservationRegistrar: Codable {
169+
public init(from decoder: any Decoder) throws {
170+
self.init()
171+
}
172+
173+
public func encode(to encoder: any Encoder) {
174+
// Don't encode a registrar's transient state.
175+
}
176+
}
177+
178+
@available(SwiftStdlib 5.9, *)
179+
extension ObservationRegistrar: Hashable {
180+
public static func == (lhs: Self, rhs: Self) -> Bool {
181+
// A registrar should be ignored for the purposes of determining its
182+
// parent type's equality.
183+
return true
184+
}
185+
186+
public func hash(into hasher: inout Hasher) {
187+
// Don't include a registrar's transient state in its parent type's
188+
// hash value.
189+
}
190+
}

test/stdlib/Observation/Observable.swift

Lines changed: 25 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -24,29 +24,6 @@ func _blackHole<T>(_ value: T) { }
2424
@Observable
2525
class ContainsNothing { }
2626

27-
@Observable
28-
struct Structure {
29-
var field: Int = 0
30-
}
31-
32-
@Observable
33-
struct MemberwiseInitializers {
34-
var field: Int
35-
}
36-
37-
func validateMemberwiseInitializers() {
38-
_ = MemberwiseInitializers(field: 3)
39-
}
40-
41-
@Observable
42-
struct DefiniteInitialization {
43-
var field: Int
44-
45-
init(field: Int) {
46-
self.field = field
47-
}
48-
}
49-
5027
@Observable
5128
class ContainsWeak {
5229
weak var obj: AnyObject? = nil
@@ -92,6 +69,31 @@ struct NonObservableContainer {
9269
}
9370
}
9471

72+
@Observable
73+
final class SendableClass: Sendable {
74+
var field: Int = 3
75+
}
76+
77+
@Observable
78+
class CodableClass: Codable {
79+
var field: Int = 3
80+
}
81+
82+
@Observable
83+
final class HashableClass {
84+
var field: Int = 3
85+
}
86+
87+
extension HashableClass: Hashable {
88+
static func == (lhs: HashableClass, rhs: HashableClass) -> Bool {
89+
lhs.field == rhs.field
90+
}
91+
92+
func hash(into hasher: inout Hasher) {
93+
hasher.combine(field)
94+
}
95+
}
96+
9597
@Observable
9698
class ImplementsAccessAndMutation {
9799
var field = 3
@@ -158,9 +160,6 @@ class IsolatedInstance {
158160
var test = "hello"
159161
}
160162

161-
@Observable
162-
struct StructHasExistingConformance: Observable { }
163-
164163
@Observable
165164
class ClassHasExistingConformance: Observable { }
166165

@@ -209,23 +208,6 @@ struct Validator {
209208
expectEqual(changed.state, false)
210209
}
211210

212-
suite.test("tracking structure changes") {
213-
let changed = CapturedState(state: false)
214-
215-
var test = Structure()
216-
withObservationTracking {
217-
_blackHole(test.field)
218-
} onChange: {
219-
changed.state = true
220-
}
221-
222-
test.field = 4
223-
expectEqual(changed.state, true)
224-
changed.state = false
225-
test.field = 5
226-
expectEqual(changed.state, false)
227-
}
228-
229211
suite.test("conformance") {
230212
func testConformance<O: Observable>(_ o: O) -> Bool {
231213
return true

0 commit comments

Comments
 (0)