Hello,
While developing a macros package, I have encountered an error that I'm still trying to fully understand. The problem appears to affect only MemberAttributeMacro
, although it might also stem from conditional conformance to required protocol.
A minimal reproduction example requires two macros: MemberAttributeMacro
and BodyMacro
, where the former only forwards its argument to the latter.
// MemberAttributeMacro implementation
struct LoggedMacro: MemberAttributeMacro {
static func expansion( ... ) throws -> [SwiftSyntax.AttributeSyntax] {
return introduce(for: node)
}
// Forwards passed argument to @Log macro
static func introduce(for node: AttributeSyntax) -> [AttributeSyntax] {
var syntax = node
syntax.attributeName = TypeSyntax(
IdentifierTypeSyntax(
name: .identifier("Log")
)
)
return [syntax]
}
}
struct LogMacro: BodyMacro {
// Returns empty CodeBlockItemSyntax array
static func expansion( ... ) throws -> [CodeBlockItemSyntax] { [] }
}
Where the following represents their signatures:
public protocol Loggable {}
@attached(body)
public macro Log(using loggable: any Loggable) = #externalMacro(
module: "swift_loggable_exampleMacros",
type: "LogMacro"
)
@attached(memberAttribute)
public macro Logged(using loggable: any Loggable) = #externalMacro(
module: "swift_loggable_exampleMacros",
type: "LoggedMacro"
)
Now here is the catch, if we create a concrete instance of the Loggable protocol and create conditional conformance to it, just like that:
// Some concrete implementation
struct Logger: Loggable {}
extension Loggable where Self == Logger {
// Works both with @Logged(using:) and @Log(using:)
static var worksOnLoggedAndLog: any Loggable {
Logger()
}
// Works only in @Log(using:),
static var worskOnLogOnly: Self {
Logger()
}
}
The first one will work like a charm, whereas the second one, when passed to @Logged(using:)
, will result in Command SwiftCompile failed with a nonzero exit code
. Consider the following:
@Logged(using: .worksOnLoggedAndLog)
class Foo {
func method() {}
}
@Logged(using: .worksOnLogOnly) // 💥 Command SwiftCompile failed with a nonzero exit code
class Bar {
func method() {}
}
class Baz {
@Log(using: .worksOnLoggedAndLog)
func method() {}
}
class Qux {
@Log(using: .worksOnLogOnly)
func method() {}
}
That made me wonder, is this a known limitation or a subtle difference with conditional conformance that I’m missing, or perhaps I’m misusing this API? Here is a minimal "compiling" example