Skip to content

[5.9] Observable protocol non-marker, @Observable class only #67196

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 11, 2023
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
4 changes: 4 additions & 0 deletions lib/Macros/Sources/ObservationMacros/Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,4 +275,8 @@ extension DeclGroupSyntax {
var isEnum: Bool {
return self.is(EnumDeclSyntax.self)
}

var isStruct: Bool {
return self.is(StructDeclSyntax.self)
}
}
57 changes: 30 additions & 27 deletions lib/Macros/Sources/ObservationMacros/ObservableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,12 @@ extension PatternBindingListSyntax {
}

extension VariableDeclSyntax {
func privatePrefixed(_ prefix: String, addingAttribute attribute: AttributeSyntax) -> VariableDeclSyntax {
VariableDeclSyntax(
func privatePrefixed(_ prefix: String, addingAttribute attribute: AttributeSyntax) -> VariableDeclSyntax {
let newAttributes = AttributeListSyntax(
(attributes.map(Array.init) ?? []) + [.attribute(attribute)])
return VariableDeclSyntax(
leadingTrivia: leadingTrivia,
attributes: attributes?.appending(.attribute(attribute)) ?? [.attribute(attribute)],
attributes: newAttributes,
modifiers: modifiers?.privatePrefixed(prefix) ?? ModifierListSyntax(keyword: .private),
bindingKeyword: TokenSyntax(bindingKeyword.tokenKind, leadingTrivia: .space, trailingTrivia: .space, presence: .present),
bindings: bindings.privatePrefixed(prefix),
Expand Down Expand Up @@ -206,11 +208,15 @@ extension ObservableMacro: MemberMacro {

if declaration.isEnum {
// enumerations cannot store properties
throw DiagnosticsError(syntax: node, message: "@Observable cannot be applied to enumeration type \(observableType.text)", id: .invalidApplication)
throw DiagnosticsError(syntax: node, message: "'@Observable' cannot be applied to enumeration type '\(observableType.text)'", id: .invalidApplication)
}
if declaration.isStruct {
// structs are not yet supported; copying/mutation semantics tbd
throw DiagnosticsError(syntax: node, message: "'@Observable' cannot be applied to struct type '\(observableType.text)'", id: .invalidApplication)
}
if declaration.isActor {
// actors cannot yet be supported for their isolation
throw DiagnosticsError(syntax: node, message: "@Observable cannot be applied to actor type \(observableType.text)", id: .invalidApplication)
throw DiagnosticsError(syntax: node, message: "'@Observable' cannot be applied to actor type '\(observableType.text)'", id: .invalidApplication)
}

var declarations = [DeclSyntax]()
Expand Down Expand Up @@ -261,30 +267,27 @@ extension ObservableMacro: MemberAttributeMacro {
}
}

extension ObservableMacro: ConformanceMacro {
public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>(
extension ObservableMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
providingConformancesOf declaration: Declaration,
in context: Context
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
let inheritanceList: InheritedTypeListSyntax?
if let classDecl = declaration.as(ClassDeclSyntax.self) {
inheritanceList = classDecl.inheritanceClause?.inheritedTypeCollection
} else if let structDecl = declaration.as(StructDeclSyntax.self) {
inheritanceList = structDecl.inheritanceClause?.inheritedTypeCollection
} else {
inheritanceList = nil
}

if let inheritanceList {
for inheritance in inheritanceList {
if inheritance.typeName.identifier == ObservableMacro.conformanceName {
return []
}
}
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
// This method can be called twice - first with an empty `protocols` when
// no conformance is needed, and second with a `MissingTypeSyntax` instance.
if protocols.isEmpty {
return []
}

return [(ObservableMacro.observableConformanceType, nil)]

let decl: DeclSyntax = """
extension \(raw: type.trimmedDescription): \(raw: qualifiedConformanceName) {}
"""

return [
decl.cast(ExtensionDeclSyntax.self)
]
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ add_swift_target_library(swiftObservation ${SWIFT_STDLIB_LIBRARY_BUILD_TYPES} IS
SWIFT_COMPILE_FLAGS
${SWIFT_STANDARD_LIBRARY_SWIFT_FLAGS}
"-enable-experimental-feature" "Macros"
"-enable-experimental-feature" "ExtensionMacros"
-Xfrontend -disable-implicit-string-processing-module-import

C_COMPILE_FLAGS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


@available(SwiftStdlib 5.9, *)
@_marker public protocol Observable { }
public protocol Observable { }

#if $Macros && hasAttribute(attached)

Expand All @@ -22,7 +22,7 @@
@attached(member, names: named(_$observationRegistrar), named(access), named(withMutation), arbitrary)
#endif
@attached(memberAttribute)
@attached(conformance)
@attached(extension, conformances: Observable)
public macro Observable() =
#externalMacro(module: "ObservationMacros", type: "ObservableMacro")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,29 @@ public struct ObservationRegistrar: Sendable {
defer { didSet(subject, keyPath: keyPath) }
return try mutation()
}
}
}

@available(SwiftStdlib 5.9, *)
extension ObservationRegistrar: Codable {
public init(from decoder: any Decoder) throws {
self.init()
}

public func encode(to encoder: any Encoder) {
// Don't encode a registrar's transient state.
}
}

@available(SwiftStdlib 5.9, *)
extension ObservationRegistrar: Hashable {
public static func == (lhs: Self, rhs: Self) -> Bool {
// A registrar should be ignored for the purposes of determining its
// parent type's equality.
return true
}

public func hash(into hasher: inout Hasher) {
// Don't include a registrar's transient state in its parent type's
// hash value.
}
}
73 changes: 29 additions & 44 deletions test/stdlib/Observation/Observable.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// REQUIRES: swift_swift_parser, executable_test

// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -enable-experimental-feature InitAccessors -enable-experimental-feature Macros -Xfrontend -plugin-path -Xfrontend %swift-host-lib-dir/plugins)
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -enable-experimental-feature InitAccessors -enable-experimental-feature Macros -enable-experimental-feature ExtensionMacros -Xfrontend -plugin-path -Xfrontend %swift-host-lib-dir/plugins)

// Run this test via the swift-plugin-server
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -parse-as-library -enable-experimental-feature InitAccessors -enable-experimental-feature Macros -enable-experimental-feature ExtensionMacros -Xfrontend -external-plugin-path -Xfrontend %swift-host-lib-dir/plugins#%swift-plugin-server)

// Asserts is required for '-enable-experimental-feature InitAccessors'.
// REQUIRES: asserts
Expand All @@ -19,29 +22,6 @@ func _blackHole<T>(_ value: T) { }
@Observable
class ContainsNothing { }

@Observable
struct Structure {
var field: Int = 0
}

@Observable
struct MemberwiseInitializers {
var field: Int
}

func validateMemberwiseInitializers() {
_ = MemberwiseInitializers(field: 3)
}

@Observable
struct DefiniteInitialization {
var field: Int

init(field: Int) {
self.field = field
}
}

@Observable
class ContainsWeak {
weak var obj: AnyObject? = nil
Expand Down Expand Up @@ -87,6 +67,31 @@ struct NonObservableContainer {
}
}

@Observable
final class SendableClass: Sendable {
var field: Int = 3
}

@Observable
class CodableClass: Codable {
var field: Int = 3
}

@Observable
final class HashableClass {
var field: Int = 3
}

extension HashableClass: Hashable {
static func == (lhs: HashableClass, rhs: HashableClass) -> Bool {
lhs.field == rhs.field
}

func hash(into hasher: inout Hasher) {
hasher.combine(field)
}
}

@Observable
class ImplementsAccessAndMutation {
var field = 3
Expand Down Expand Up @@ -153,9 +158,6 @@ class IsolatedInstance {
var test = "hello"
}

@Observable
struct StructHasExistingConformance: Observable { }

@Observable
class ClassHasExistingConformance: Observable { }

Expand Down Expand Up @@ -204,23 +206,6 @@ struct Validator {
expectEqual(changed.state, false)
}

suite.test("tracking structure changes") {
let changed = CapturedState(state: false)

var test = Structure()
withObservationTracking {
_blackHole(test.field)
} onChange: {
changed.state = true
}

test.field = 4
expectEqual(changed.state, true)
changed.state = false
test.field = 5
expectEqual(changed.state, false)
}

suite.test("conformance") {
func testConformance<O: Observable>(_ o: O) -> Bool {
return true
Expand Down
2 changes: 1 addition & 1 deletion test/stdlib/Observation/ObservableDidSetWillSet.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// REQUIRES: swift_swift_parser, executable_test

// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -enable-experimental-feature InitAccessors -enable-experimental-feature Macros -Xfrontend -plugin-path -Xfrontend %swift-host-lib-dir/plugins) | %FileCheck %s
// RUN: %target-run-simple-swift( -Xfrontend -disable-availability-checking -enable-experimental-feature InitAccessors -enable-experimental-feature Macros -enable-experimental-feature ExtensionMacros -Xfrontend -plugin-path -Xfrontend %swift-host-lib-dir/plugins) | %FileCheck %s

// Asserts is required for '-enable-experimental-feature InitAccessors'.
// REQUIRES: asserts
Expand Down