Files
swift-mirror/lib/Macros/Sources/ObservationMacros/ObservableMacro.swift

374 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.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 []
}
}