The move from conformance macros to extension macros included the ability for extension macros to learn about which protocols the type already conformed to (e.g., because a superclass conformed), so that one could avoid adding declarations that aren't needed. It also meant that any new declarations added are part of an extension---not the original type---which is generally a good thing, because it means that (e.g.) a new initializer doesn't suppress the memberwise initializer, and it's usually considered good form to split protocol conformances out into their own extensions.
However, there are some times when the member used for the conformance really needs to be part of the original type definition. For example:
- An initializer in a non-final class needs to be a
required initto satisfy a protocol requirement
- An overridable member of a non-final class
- A stored property or case can only be in the primary type definition
Codable-synthesizing macro is the most obvious case for this, because it hits both of the first two bullets: when we're in a non-final class, we need to make the
required, and also call the superclass initializer, and we also want
encode(to:) to be overridable.
My proposal is that member macros can be declared with a
conformances argument that has the same meaning as the one for extension macros, e.g.,
@attached(member, conformances: Decodable, Encodable, names: named(init(from:), encode(to:))) @attached(extension, conformances: Decodable, Encodable, names: named(init(from:), encode(to:))) macro Codable() = ...
and that member macro implementations will use this new requirement to get the list of conformances that are "missing":
/// Expand an attached declaration macro to produce a set of members. /// /// - Parameters: /// - node: The custom attribute describing the attached macro. /// - declaration: The declaration the macro attribute is attached to. /// - missingConformancesTo: The set of protocols that were declared /// in the set of conformances for the macro and to which the declaration /// does not explicitly conform. The member macro itself cannot declare /// conformances to these protocols (only an extension macro can do that), /// but can provide supporting declarations, such as a required /// initializer or stored property, that cannot be written in an /// extension. /// - context: The context in which to perform the macro expansion. /// /// - Returns: the set of member declarations introduced by this macro, which /// are nested inside the `attachedTo` declaration. static func expansion( of node: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, missingConformancesTo protocols: [TypeSyntax], in context: some MacroExpansionContext ) throws -> [DeclSyntax]
Member macros still cannot produce conformances themselves, but at least they can react to whether conformances are already there vs. could be synthesized by an extension macro.
I have cobbled together an implementation here.