Search code examples
swiftmacros

Swift macro: unexpected error when parsing DeclSyntax node


I am trying to get my feet wet with Swift macros. The task is to get from

@RecordInterfacable
@Observable
class Model {
  let id: UUID
  var title: String

  init(
    id: UUID = UUID(),
    title: String
  ) {
    self.id = id
    self.title = title
  }
}

to

@Observable
class Model {
  let id: UUID
  var title: String

  init(
    id: UUID = UUID(),
    title: String
  ) {
    self.id = id
    self.title = title
  }

  convenience init(from record: ModelRecord) {
    self.init(id: record.id, title: record.title)
  }

  var record: ModelRecord {
    ModelRecord(id: self.id, title: self.title)
  }

  struct ModelRecord {
    let id: UUID
    var title: String
  }
}

Stubbing out the desired result compiles just fine, but when running my macro test, I am met with

Parsing a `DeclSyntax` node from string interpolation produced the following parsing errors.
Set a breakpoint in `SyntaxParseable.logStringInterpolationParsingError()` to debug the failure.
2 │   self.init(id: record.id, title: record.title)
3 │ }
4 │ struct ModelRecord: Codable {
  │ ╰─ error: unexpected code in initializer
5 │   let id: UUID
6 │ var title: String

I am aware that the macro is not fully implemented yet, but the convenience init() is giving me trouble already … I am surprised that this is the case, cause what I am returning in static expansion(of:providingMembersOf:conformingTo:in:) is trying to implement the exact same code that I have stubbed out above:

static expansion(of:providingMembersOf:conformingTo:in:) {
  // …
  return [
      DeclSyntax(
              extendedGraphemeClusterLiteral: """
      convenience init(from record: \(symbolName)Record) {
        //this needs to be implemented:
        self.init(id: record.id, title: record.title)
      }
      // var record is still missing
      struct \(symbolName)Record: Codable {
        \(memberStrings)
      }
      """
      )
    ]
}

… which according to the test console output produces:

Actual expanded source:
@Observable
class Model {
  let id: UUID
  var title: String
  init(
    id: UUID = UUID(),
    title: String
  ) {
    self.id = id
    self.title = title
  }

    convenience init(from record: ModelRecord) {
      self.init(id: record.id, title: record.title)
    }
    struct ModelRecord: Codable {
      let id: UUID
    var title: String
    }
}

The complete macro implementation looks as follows:

public struct RecordInterfacableMacro: MemberMacro {
  public static func expansion(
    of node: AttributeSyntax,
    providingMembersOf declaration: some DeclGroupSyntax,
    conformingTo protocols: [TypeSyntax],
    in context: some MacroExpansionContext
  ) throws -> [DeclSyntax] {
    guard let symbolName = declaration
      .as(ClassDeclSyntax.self)?
      .name
      .text
    else {
      fatalError("Could not extract symbol name.")
    }

    /// Extracts all the elements of the body of the given class.
    /// This includes all properties and functions.
    let membersDeclSyntax = declaration
      .as(ClassDeclSyntax.self)?
      .memberBlock
      .members
      .compactMap {
        $0
          .as(MemberBlockItemSyntax.self)?
          .decl
          .as(DeclSyntax.self)
      }

    /// Further extracts all variables
    let membersVariableDeclSyntax = membersDeclSyntax?
      .compactMap {
        $0
          .as(VariableDeclSyntax.self)
      }

    /// Create a string with the declaration of all members
    let memberStrings = membersVariableDeclSyntax?
      .map { member in
        guard let memberBindingSpecifier = member
          .bindingSpecifier
          .text
          .split(separator: ".")
          .last
        else { fatalError() }

        let identifierText = member
          .bindings
          .compactMap {
            $0
              .as(PatternBindingSyntax.self)?
              .pattern
              .as(IdentifierPatternSyntax.self)?
              .identifier
              .text
          }
          .first
        guard let identifierText else { fatalError() }

        let memberType = member
          .bindings
          .compactMap {
            $0
              .as(PatternBindingSyntax.self)?
              .typeAnnotation?
              .type
              .as(IdentifierTypeSyntax.self)?
              .name
              .text
          }
          .first
        guard let memberType else { fatalError() }

        return "\(memberBindingSpecifier) \(identifierText): \(memberType)"
      }
      .joined(separator: "\n")
    guard let memberStrings else{ fatalError() }

    return [
      DeclSyntax(
              extendedGraphemeClusterLiteral: """
      convenience init(from record: \(symbolName)Record) {
        //this needs to be implemented:
        self.init(id: record.id, title: record.title)
      }
      // var record is still missing
      struct \(symbolName)Record: Codable {
        \(memberStrings)
      }
      """
      )
    ]
  }
}

Solution

  • Turns out that I was holding it wrong–each DeclSyntax needs to be declared separately:

        return [
          DeclSyntax(
            extendedGraphemeClusterLiteral: """
          convenience init(from record: \(symbolName)) {
            self.init(id: record.id, title: record.title)
          }
          """
          ),
          DeclSyntax(
            stringLiteral: """
          struct \(symbolName)Record: Codable {
            \(memberStrings)
          }
          """
          )
        ]
    

    Now I can move on to complete my macro 😀