mirror of
https://github.com/apple/swift.git
synced 2025-12-21 12:14:44 +01:00
* 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.
378 lines
12 KiB
Swift
378 lines
12 KiB
Swift
//===----------------------------------------------------------------------===//
|
|
//
|
|
// This source file is part of the Swift.org open source project
|
|
//
|
|
// Copyright (c) 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
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
import SwiftSyntax
|
|
import SwiftSyntaxMacros
|
|
|
|
@_implementationOnly import SwiftDiagnostics
|
|
@_implementationOnly import SwiftOperators
|
|
@_implementationOnly import SwiftSyntaxBuilder
|
|
|
|
public struct ObservableMacro {
|
|
static let moduleName = "Observation"
|
|
|
|
static let conformanceName = "Observable"
|
|
static var qualifiedConformanceName: String {
|
|
return "\(moduleName).\(conformanceName)"
|
|
}
|
|
|
|
static var observableConformanceType: TypeSyntax {
|
|
"\(raw: qualifiedConformanceName)"
|
|
}
|
|
|
|
static let registrarTypeName = "ObservationRegistrar"
|
|
static var qualifiedRegistrarTypeName: String {
|
|
return "\(moduleName).\(registrarTypeName)"
|
|
}
|
|
|
|
static let trackedMacroName = "ObservationTracked"
|
|
static let ignoredMacroName = "ObservationIgnored"
|
|
|
|
static let registrarVariableName = "_$observationRegistrar"
|
|
|
|
static func registrarVariable(_ observableType: TokenSyntax) -> DeclSyntax {
|
|
return
|
|
"""
|
|
@\(raw: ignoredMacroName) private let \(raw: registrarVariableName) = \(raw: qualifiedRegistrarTypeName)()
|
|
"""
|
|
}
|
|
|
|
static func accessFunction(_ observableType: TokenSyntax) -> DeclSyntax {
|
|
return
|
|
"""
|
|
internal nonisolated func access<Member>(
|
|
keyPath: KeyPath<\(observableType), Member>
|
|
) {
|
|
\(raw: registrarVariableName).access(self, keyPath: keyPath)
|
|
}
|
|
"""
|
|
}
|
|
|
|
static func withMutationFunction(_ observableType: TokenSyntax) -> DeclSyntax {
|
|
return
|
|
"""
|
|
internal nonisolated func withMutation<Member, T>(
|
|
keyPath: KeyPath<\(observableType), Member>,
|
|
_ mutation: () throws -> T
|
|
) rethrows -> T {
|
|
try \(raw: registrarVariableName).withMutation(of: self, keyPath: keyPath, mutation)
|
|
}
|
|
"""
|
|
}
|
|
|
|
static var ignoredAttribute: AttributeSyntax {
|
|
AttributeSyntax(
|
|
leadingTrivia: .space,
|
|
atSignToken: .atSignToken(),
|
|
attributeName: SimpleTypeIdentifierSyntax(name: .identifier(ignoredMacroName)),
|
|
trailingTrivia: .space
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ObservationDiagnostic: DiagnosticMessage {
|
|
enum ID: String {
|
|
case invalidApplication = "invalid type"
|
|
case missingInitializer = "missing initializer"
|
|
}
|
|
|
|
var message: String
|
|
var diagnosticID: MessageID
|
|
var severity: DiagnosticSeverity
|
|
|
|
init(message: String, diagnosticID: SwiftDiagnostics.MessageID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
|
|
self.message = message
|
|
self.diagnosticID = diagnosticID
|
|
self.severity = severity
|
|
}
|
|
|
|
init(message: String, domain: String, id: ID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
|
|
self.message = message
|
|
self.diagnosticID = MessageID(domain: domain, id: id.rawValue)
|
|
self.severity = severity
|
|
}
|
|
}
|
|
|
|
extension DiagnosticsError {
|
|
init<S: SyntaxProtocol>(syntax: S, message: String, domain: String = "Observation", id: ObservationDiagnostic.ID, severity: SwiftDiagnostics.DiagnosticSeverity = .error) {
|
|
self.init(diagnostics: [
|
|
Diagnostic(node: Syntax(syntax), message: ObservationDiagnostic(message: message, domain: domain, id: id, severity: severity))
|
|
])
|
|
}
|
|
}
|
|
|
|
extension ModifierListSyntax {
|
|
func privatePrefixed(_ prefix: String) -> ModifierListSyntax {
|
|
let modifier: DeclModifierSyntax = DeclModifierSyntax(name: "private", trailingTrivia: .space)
|
|
return ModifierListSyntax([modifier] + filter {
|
|
switch $0.name.tokenKind {
|
|
case .keyword(let keyword):
|
|
switch keyword {
|
|
case .fileprivate: fallthrough
|
|
case .private: fallthrough
|
|
case .internal: fallthrough
|
|
case .public:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
default:
|
|
return true
|
|
}
|
|
})
|
|
}
|
|
|
|
init(keyword: Keyword) {
|
|
self.init([DeclModifierSyntax(name: .keyword(keyword))])
|
|
}
|
|
}
|
|
|
|
extension TokenSyntax {
|
|
func privatePrefixed(_ prefix: String) -> TokenSyntax {
|
|
switch tokenKind {
|
|
case .identifier(let identifier):
|
|
return TokenSyntax(.identifier(prefix + identifier), leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia, presence: presence)
|
|
default:
|
|
return self
|
|
}
|
|
}
|
|
}
|
|
|
|
extension PatternBindingListSyntax {
|
|
func privatePrefixed(_ prefix: String) -> PatternBindingListSyntax {
|
|
var bindings = self.map { $0 }
|
|
for index in 0..<bindings.count {
|
|
let binding = bindings[index]
|
|
if let identifier = binding.pattern.as(IdentifierPatternSyntax.self) {
|
|
bindings[index] = PatternBindingSyntax(
|
|
leadingTrivia: binding.leadingTrivia,
|
|
pattern: IdentifierPatternSyntax(
|
|
leadingTrivia: identifier.leadingTrivia,
|
|
identifier: identifier.identifier.privatePrefixed(prefix),
|
|
trailingTrivia: identifier.trailingTrivia
|
|
),
|
|
typeAnnotation: binding.typeAnnotation,
|
|
initializer: binding.initializer,
|
|
accessor: binding.accessor,
|
|
trailingComma: binding.trailingComma,
|
|
trailingTrivia: binding.trailingTrivia)
|
|
|
|
}
|
|
}
|
|
|
|
return PatternBindingListSyntax(bindings)
|
|
}
|
|
}
|
|
|
|
extension VariableDeclSyntax {
|
|
func privatePrefixed(_ prefix: String, addingAttribute attribute: AttributeSyntax) -> VariableDeclSyntax {
|
|
VariableDeclSyntax(
|
|
leadingTrivia: leadingTrivia,
|
|
attributes: attributes?.appending(.attribute(attribute)) ?? [.attribute(attribute)],
|
|
modifiers: modifiers?.privatePrefixed(prefix) ?? ModifierListSyntax(keyword: .private),
|
|
bindingKeyword: TokenSyntax(bindingKeyword.tokenKind, leadingTrivia: .space, trailingTrivia: .space, presence: .present),
|
|
bindings: bindings.privatePrefixed(prefix),
|
|
trailingTrivia: trailingTrivia
|
|
)
|
|
}
|
|
|
|
var isValidForObservation: Bool {
|
|
!isComputed && isInstance && !isImmutable && identifier != nil
|
|
}
|
|
}
|
|
|
|
extension ObservableMacro: MemberMacro {
|
|
public static func expansion<
|
|
Declaration: DeclGroupSyntax,
|
|
Context: MacroExpansionContext
|
|
>(
|
|
of node: AttributeSyntax,
|
|
providingMembersOf declaration: Declaration,
|
|
in context: Context
|
|
) throws -> [DeclSyntax] {
|
|
guard let identified = declaration.asProtocol(IdentifiedDeclSyntax.self) else {
|
|
return []
|
|
}
|
|
|
|
let observableType = identified.identifier
|
|
|
|
if declaration.isEnum {
|
|
// enumerations cannot store properties
|
|
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)
|
|
}
|
|
|
|
var declarations = [DeclSyntax]()
|
|
|
|
declaration.addIfNeeded(ObservableMacro.registrarVariable(observableType), to: &declarations)
|
|
declaration.addIfNeeded(ObservableMacro.accessFunction(observableType), to: &declarations)
|
|
declaration.addIfNeeded(ObservableMacro.withMutationFunction(observableType), to: &declarations)
|
|
|
|
#if !OBSERVATION_SUPPORTS_PEER_MACROS
|
|
let storedInstanceVariables = declaration.definedVariables.filter { $0.isValidForObservation }
|
|
for property in storedInstanceVariables {
|
|
if property.hasMacroApplication(ObservableMacro.ignoredMacroName) { continue }
|
|
let storage = DeclSyntax(property.privatePrefixed("_", addingAttribute: ObservableMacro.ignoredAttribute))
|
|
declaration.addIfNeeded(storage, to: &declarations)
|
|
}
|
|
#endif
|
|
|
|
return declarations
|
|
}
|
|
}
|
|
|
|
extension ObservableMacro: MemberAttributeMacro {
|
|
public static func expansion<
|
|
Declaration: DeclGroupSyntax,
|
|
MemberDeclaration: DeclSyntaxProtocol,
|
|
Context: MacroExpansionContext
|
|
>(
|
|
of node: AttributeSyntax,
|
|
attachedTo declaration: Declaration,
|
|
providingAttributesFor member: MemberDeclaration,
|
|
in context: Context
|
|
) throws -> [AttributeSyntax] {
|
|
guard let property = member.as(VariableDeclSyntax.self), property.isValidForObservation,
|
|
property.identifier != nil else {
|
|
return []
|
|
}
|
|
|
|
// dont apply to ignored properties or properties that are already flaged as tracked
|
|
if property.hasMacroApplication(ObservableMacro.ignoredMacroName) ||
|
|
property.hasMacroApplication(ObservableMacro.trackedMacroName) {
|
|
return []
|
|
}
|
|
|
|
|
|
return [
|
|
AttributeSyntax(attributeName: SimpleTypeIdentifierSyntax(name: .identifier(ObservableMacro.trackedMacroName)))
|
|
]
|
|
}
|
|
}
|
|
|
|
extension ObservableMacro: ConformanceMacro {
|
|
public static func expansion<Declaration: DeclGroupSyntax, Context: MacroExpansionContext>(
|
|
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 []
|
|
}
|
|
}
|
|
}
|
|
|
|
return [(ObservableMacro.observableConformanceType, nil)]
|
|
}
|
|
}
|
|
|
|
public struct ObservationTrackedMacro: AccessorMacro {
|
|
public static func expansion<
|
|
Context: MacroExpansionContext,
|
|
Declaration: DeclSyntaxProtocol
|
|
>(
|
|
of node: AttributeSyntax,
|
|
providingAccessorsOf declaration: Declaration,
|
|
in context: Context
|
|
) throws -> [AccessorDeclSyntax] {
|
|
guard let property = declaration.as(VariableDeclSyntax.self),
|
|
property.isValidForObservation,
|
|
let identifier = property.identifier else {
|
|
return []
|
|
}
|
|
|
|
if property.hasMacroApplication(ObservableMacro.ignoredMacroName) {
|
|
return []
|
|
}
|
|
|
|
let initAccessor: AccessorDeclSyntax =
|
|
"""
|
|
init(initialValue) initializes(_\(identifier)) {
|
|
_\(identifier) = initialValue
|
|
}
|
|
"""
|
|
|
|
let getAccessor: AccessorDeclSyntax =
|
|
"""
|
|
get {
|
|
access(keyPath: \\.\(identifier))
|
|
return _\(identifier)
|
|
}
|
|
"""
|
|
|
|
let setAccessor: AccessorDeclSyntax =
|
|
"""
|
|
set {
|
|
withMutation(keyPath: \\.\(identifier)) {
|
|
_\(identifier) = newValue
|
|
}
|
|
}
|
|
"""
|
|
|
|
return [initAccessor, getAccessor, setAccessor]
|
|
}
|
|
}
|
|
|
|
extension ObservationTrackedMacro: PeerMacro {
|
|
public static func expansion<
|
|
Context: MacroExpansionContext,
|
|
Declaration: DeclSyntaxProtocol
|
|
>(
|
|
of node: SwiftSyntax.AttributeSyntax,
|
|
providingPeersOf declaration: Declaration,
|
|
in context: Context
|
|
) throws -> [DeclSyntax] {
|
|
guard let property = declaration.as(VariableDeclSyntax.self),
|
|
property.isValidForObservation else {
|
|
return []
|
|
}
|
|
|
|
if property.hasMacroApplication(ObservableMacro.ignoredMacroName) ||
|
|
property.hasMacroApplication(ObservableMacro.trackedMacroName) {
|
|
return []
|
|
}
|
|
|
|
let storage = DeclSyntax(property.privatePrefixed("_", addingAttribute: ObservableMacro.ignoredAttribute))
|
|
return [storage]
|
|
}
|
|
}
|
|
|
|
public struct ObservationIgnoredMacro: AccessorMacro {
|
|
public static func expansion<
|
|
Context: MacroExpansionContext,
|
|
Declaration: DeclSyntaxProtocol
|
|
>(
|
|
of node: AttributeSyntax,
|
|
providingAccessorsOf declaration: Declaration,
|
|
in context: Context
|
|
) throws -> [AccessorDeclSyntax] {
|
|
return []
|
|
}
|
|
}
|