# \[Pitch\] Attached macros

**URL:** <https://forums.swift.org/t/pitch-attached-macros/62812>\
**Category:** Pitches\
**Created:** [January 27, 2023, 1:05am UTC](https://forums.swift.org/t/pitch-attached-macros/62812 "2023-01-27T01:05:59Z")\
**Posts on this page:** 1\
**Showing post:** 8

<div class="post-metadata">

**Author:** ![s-k](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/s-k/32/6796_2.png) [@s-k](https://forums.swift.org/u/s-k)\
**Post date:** [February 10, 2023, 9:14am UTC](https://forums.swift.org/t/pitch-attached-macros/62812/8 "2023-02-10T09:14:46Z")

</div>

I have played a little with the attached macros implementation. My goal was to see if it is feasible to implement `Codable` as a macro. I am aware that witness macros are a better fit for this job, but those are not implemented yet, so I used an attached macro. Here is the result:

```swift
@CustomCodable
struct Party: Codable {
  enum Theme: Codable {
    case halloween
    case birthday
    case piratesAndMonstersAndUnicornsAndEverythingElse
  }
  
  @CustomKey("host_name")
  var hostName: String
  var theme: Theme
  var numberOfGuests: Int = 0 {
    didSet {
      print("Someone came! Or went, I don't know...")
    }
  }
  
  var description: String {
    "\(hostName) has invited \(numberOfGuests) \(theme == .halloween ? "ghosts" : "guests")!"
  }
}

let party = Party(
  hostName: "Jim",
  theme: .halloween,
  numberOfGuests: 2
)
let encoder = JSONEncoder()
let partyData = try encoder.encode(party)
print(String(data: partyData, encoding: .utf8)!)
// Prints {"theme":{"halloween":{}},"host_name":"Jim","numberOfGuests":2}

let decoder = JSONDecoder()
let partyCopy = try decoder.decode(Party.self, from: partyData)
print(partyCopy.description) // Prints Jim has invited 2 ghosts!

```

> **Full implementation**
>
> In `MacroExamplesLib`:
> 
> ```swift
> @attached(member, names: named(encode), named(CodingKeys))
> public macro CustomCodable() = #externalMacro(module: "MacroExamplesPlugin", type: "CustomCodableMacro")
> 
> /// This macro does nothing. It is only used as a hint for `@CustomCodable`.
> @attached(accessor)
> public macro CustomKey(_ key: String) = #externalMacro(module: "MacroExamplesPlugin", type: "CustomKeyDummyMacro")
> 
> public extension KeyedDecodingContainer {
> func _decodeInferredType<Value: Decodable>(forKey key: KeyedDecodingContainer<K>.Key) throws -> Value {
> try self.decode(Value.self, forKey: key)
> }
> }
> 
> ```
> 
> In `MacroExamplesPlugin`:
> 
> ```swift
> public enum CustomCodableMacro {}
> 
> extension CustomCodableMacro: MemberMacro {
> public static func expansion(
> of node: AttributeSyntax,
> providingMembersOf declaration: some DeclGroupSyntax,
> in context: some MacroExpansionContext
> ) throws -> [DeclSyntax] {
>     
> let storedProperties = declaration.members.members.compactMap {
> StoredProperty(potentialPropertyDeclaration: $0.decl, diagnosticHandler: context.diagnose)
> }
>     
> let codingKeyDefinitions = storedProperties.map { property in
> "\(property.name) = \(property.keyLiteral)"
> }
>     
> let codingKeysSyntax: DeclSyntax = """
> enum CodingKeys: String, CodingKey {
> case \(raw: codingKeyDefinitions.joined(separator: ", "))
> }
> """
>     
> let encodingCode = storedProperties
> .map { property in
> " try container.encode(\(property.name), forKey: .\(property.name))"
> }
> .joined(separator: "\n")
>     
> let encodeFunctionSyntax: DeclSyntax = """
> func encode(to encoder: Encoder) throws {
> var container = encoder.container(keyedBy: CodingKeys.self)
> \(raw: encodingCode)
> }
> """
>     
> let decodingCode = storedProperties
> .map { property in
> " self.\(property.name) = try container._decodeInferredType(forKey: .\(property.name))"
> }
> .joined(separator: "\n")
>     
> let decodingInitializerSyntax: DeclSyntax = """
> init(from decoder: Decoder) throws {
> let container = try decoder.container(keyedBy: CodingKeys.self)
> \(raw: decodingCode)
> }
> """
>     
> let propertiesWithTypeAnnotations = storedProperties.filter { $0.typeName != nil }
>     
> let parameterList = propertiesWithTypeAnnotations
> .map { property in
> "\(property.name): \(property.typeName!)\(property.initializer.map { " " + $0 } ?? "")"
> }
> .joined(separator: ", ")
>     
> let propertyAssignmentList = propertiesWithTypeAnnotations
> .map { property in
> "self.\(property.name) = \(property.name)"
> }
> .joined(separator: "\n ")
>     
> let memberwiseInitializer: DeclSyntax = """
> init(
> \(raw: parameterList)
> ) {
> \(raw: propertyAssignmentList)
> }
> """
>     
> return [codingKeysSyntax, encodeFunctionSyntax, decodingInitializerSyntax, memberwiseInitializer]
> }
> }
> 
> // MARK: - StoredProperty
> 
> fileprivate struct StoredProperty {
> var name: String
> var typeName: String?
> var keyLiteral: String
> var initializer: String?
> }
> 
> extension StoredProperty {
> init?(potentialPropertyDeclaration: DeclSyntax, diagnosticHandler: (Diagnostic) -> Void) {
> guard let property = potentialPropertyDeclaration.as(VariableDeclSyntax.self) else {
> return nil
> }
>     
> self.init(property, diagnosticHandler: diagnosticHandler)
> }
>   
> init?(_ property: VariableDeclSyntax, diagnosticHandler: (Diagnostic) -> Void) {
> guard
> property.isStoredProperty,
> let binding = property.bindings.first,
> let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier
> else {
> return nil
> }
>     
> let attributes = property.attributes ?? []
> let key = attributes
> .compactMap { syntax -> String? in
> guard
> let attributeSyntax = syntax.as(AttributeSyntax.self),
> attributeSyntax.attributeName.trimmedDescription == "CustomKey",
> let argument = attributeSyntax.argument
> else {
> return nil
> }
>         
> return argument.trimmedDescription
> }
> .first
>     
> self.name = identifier.text
> self.typeName = binding.typeAnnotation?.type.trimmedDescription
> self.keyLiteral = key ?? "\"\(identifier.text)\""
> self.initializer = binding.initializer?.trimmedDescription
>     
> if typeName == nil {
> let message = SimpleDiagnosticMessage(
> message: "@CustomCodable requires that all properties have an explicit type annotation",
> diagnosticID: .init(domain: "test", id: "error"),
> severity: .warning
> )
> diagnosticHandler(.init(node: binding.initializer?.as(Syntax.self) ?? property.as(Syntax.self)!, message: message))
> }
> }
> }
> 
> // MARK: - CustomKeyDummyMacro
> 
> /// This macro does nothing. It is only used as a hint for `CustomCodableMacro`.
> public enum CustomKeyDummyMacro {}
> 
> extension CustomKeyDummyMacro: AccessorMacro {
> public static func expansion(
> of node: AttributeSyntax,
> providingAccessorsOf declaration: some DeclSyntaxProtocol,
> in context: some MacroExpansionContext
> ) throws -> [AccessorDeclSyntax] {
> return []
> }
> }
> 
> ```

The macro only replaces `CodingKeys`, `encode(to:)` and `init(from:)` and uses the other parts of `Codable` as is. However, I think this is enough to see if macros are feasible for the job. As a test case, I have added the ability to use a custom key using `@CustomKey` (see `hostName` in the example), which I believe is not possible with the default `Codable` implementation.

I have learned several things. First, the positive:

- It works!

- Implementing `CodingKeys` and `encode(to:)` was straightforward (apart from finding the types and properties I needed in SwiftSyntax).

- When using the current snapshot, if there is a compilation error or crash, Xcode directly points to the expanded code in a buffer. This is really great for debugging!

Now the points that could be improved:

- When generating `init(from:)`, the macro can't just use `KeyedDecodingContainer.decode(_:forKey:)` because it does not know the type of properties that have a default value and use type inference. I have circumvented the issue by defining a helper method and (mis)using type inference:

- Because the macro has to generate `init(from:)`, the default memberwise initializer is no longer automatically generated. I think this is a problem. My macro could have used a peer macro for this if they had already been implemented. However, the canonical macro type for `Codable` would be witness macros, which are from my understanding not able to generate a peer. We definitely need a solution for this problem!

Implementing this macro reinforced my impressions that the proposed macro system is very powerful but what it misses most is type information. For this macro, it would have sufficed to provide a list of stored properties to the macro as proposed for witness macros. However, other macros may need other types of type information.

---

_[View the full topic](https://forums.swift.org/t/pitch-attached-macros/62812)._
