Files
element-x-ios/Components/BuildExtensions/Sources/MacrosImplementation/AppHookMacro.swift
Stefan Ceriu c0e19454e3 Move BuildExtensions into the Components folder
Complete grouping the standalone modules under Components. Updates the local
Swift package path in project.yml.
2026-08-12 16:52:26 +03:00

99 lines
3.4 KiB
Swift

//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import SwiftSyntax
import SwiftSyntaxMacros
/// Implements `@AppHook`. The generated property reads through a `Mutex`-backed store, and a
/// generated `register<Name>` method swaps in a replacement hook.
enum AppHookMacro {
enum DiagnosticError: Error, CustomStringConvertible {
case notAStoredProperty
case missingTypeAnnotation
case missingDefault
var description: String {
switch self {
case .notAStoredProperty:
"@AppHook can only be applied to a stored property."
case .missingTypeAnnotation:
"@AppHook requires an explicit type annotation."
case .missingDefault:
"@AppHook requires a default value."
}
}
}
private struct AppHook {
let name: String
let type: TypeSyntax
let defaultValue: ExprSyntax
var storageName: TokenSyntax {
"_\(raw: name)"
}
var registerMethodName: TokenSyntax {
"register\(raw: name.prefix(1).uppercased() + name.dropFirst())"
}
}
private static func appHook(for declaration: some DeclSyntaxProtocol, node: AttributeSyntax) throws -> AppHook {
guard let variable = declaration.as(VariableDeclSyntax.self),
let binding = variable.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw DiagnosticError.notAStoredProperty
}
guard let type = binding.typeAnnotation?.type.trimmed else {
throw DiagnosticError.missingTypeAnnotation
}
var defaultValue: ExprSyntax?
if let list = node.arguments?.as(LabeledExprListSyntax.self) {
for argument in list where argument.label?.text == "default" {
defaultValue = argument.expression
}
}
guard let defaultValue else {
throw DiagnosticError.missingDefault
}
return AppHook(name: identifier.text, type: type, defaultValue: defaultValue)
}
}
extension AppHookMacro: AccessorMacro {
static func expansion(of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [AccessorDeclSyntax] {
let appHook = try appHook(for: declaration, node: node)
return [
"""
get {
\(appHook.storageName).withLock { $0 }
}
"""
]
}
}
extension AppHookMacro: PeerMacro {
static func expansion(of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext) throws -> [DeclSyntax] {
let appHook = try appHook(for: declaration, node: node)
return [
"private let \(appHook.storageName) = Mutex<\(appHook.type)>(\(appHook.defaultValue))",
"""
func \(appHook.registerMethodName)(_ hook: \(appHook.type)) {
\(appHook.storageName).withLock { $0 = hook }
}
"""
]
}
}