I am trying to create a Swift macro that conditionally adds the @available(*, deprecated) attribute to all properties of a struct when the macro is applied. I want to control this behavior globally using a custom compile-time flag, so that I can toggle between showing and hiding the @available annotation across my project at build time.
I would like to pass a compile-time flag globally within the project and have it conditionally affect the Swift macro behavior.
Example:
I want to use the macro like this:
@MyMacro()
struct X {
var a: String
var b: Int
}
And depending on whether a global compile-time flag is set, the result should either look like:
struct X {
@available(*, deprecated) var a: String
@available(*, deprecated) var b: Int
}
With Flag Enabled
struct X {
@available(*, deprecated) var a: String
@available(*, deprecated) var b: Int
}
With Flag Disabled:
struct X {
var a: String
var b: Int
}
Macros cannot read the compiler flags during expansion, so your macro has to a peer macro that generates a #if ... #else ... #endif
block instead. e.g.
#if SOME_FLAG
struct X {
@available(*, deprecated) var a: String
@available(*, deprecated) var b: Int
}
#else
struct X {
var a: String
var b: Int
}
#endif
However, macros cannot remove the type to which they are attached. This means that the types that the macro generates must have a different name from the type that the macro is attached to (add a prefix/suffix). You can make sure no one uses the original type by marking it as unavailable
, or private
. Example usage:
@MyMacro
@available(*, unavailable, message: "Use PrefixSomething instead!")
struct Something {
var a: String
var b: Int
}
A second design is to use a declaration macro that takes a closure. It will generate a #if ... #else ... #endif
block for every declaration in that closure. e.g.
#MyMacro {
struct Something {
var a: String
var b: Int
}
}
The downside to this approach is that it cannot be used at the top level, because it is a macro that creates arbitrary
names. See my answer here for an example.
Here is an example implementation of the first approach (the peer macro).
// declaration:
@attached(peer, names: prefixed(XX)) // choose a prefix/suffix here!
public macro MyMacro() = #externalMacro(module: "MyMacroMacros", type: "MyMacro")
// implementation:
enum MyMacro: PeerMacro {
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard var declGroup = declaration.asProtocol(DeclGroupSyntax.self),
declaration.isProtocol(NamedDeclSyntax.self) else {
return []
}
removeUnavailableAndMacro(&declGroup)
let nonDeprecatedDecl = prefixName(declGroup.asProtocol(NamedDeclSyntax.self)!)
let deprecatedDecl = prefixName(
copyWithDeprecation(declGroup).asProtocol(NamedDeclSyntax.self)!
)
return [
"""
#if SOME_FLAG
\(deprecatedDecl)
#else
\(nonDeprecatedDecl)
#endif
"""
]
}
}
func prefixName(_ declaration: NamedDeclSyntax) -> DeclSyntax {
// choose a prefix here!
declaration.with(\.name, TokenSyntax(stringLiteral: "XX" + declaration.name.text)).cast(DeclSyntax.self)
}
func removeUnavailableAndMacro(_ declaration: inout some DeclGroupSyntax) {
let unavailableIndex = declaration.attributes.firstIndex {
guard case let .attribute(attr) = $0,
case let .availability(args) = attr.arguments else {
return false
}
guard case let .token(token1) = args.first?.argument,
case let .token(token2) = args.dropFirst().first?.argument else {
return false
}
guard token1.text == "*", token2.text == "unavailable" else {
return false
}
return true
}
if let unavailableIndex {
declaration.attributes.remove(at: unavailableIndex)
}
let macroIndex = declaration.attributes.firstIndex {
guard case let .attribute(attr) = $0 else {
return false
}
return attr.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "MyMacro"
}
if let macroIndex {
declaration.attributes.remove(at: macroIndex)
}
}
func copyWithDeprecation<T: DeclGroupSyntax>(_ declaration: T) -> T {
var copy = declaration
for i in copy.memberBlock.members.indices {
let memberDecl = copy.memberBlock.members[i].decl
if var varDecl = memberDecl.as(VariableDeclSyntax.self) {
varDecl.attributes.append(.attribute("@available(*, deprecated)"))
copy.memberBlock.members[i].decl = DeclSyntax(varDecl)
} else if var funcDecl = memberDecl.as(FunctionDeclSyntax.self) {
funcDecl.attributes.append(.attribute("@available(*, deprecated)"))
copy.memberBlock.members[i].decl = DeclSyntax(funcDecl)
}
}
return copy
}