Я пытаюсь освоить макросы Swift. Задача состоит в том, чтобы получить от
@RecordInterfacable
@Observable
class Model {
let id: UUID
var title: String
init(
id: UUID = UUID(),
title: String
) {
self.id = id
self.title = title
}
}
к
@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
}
}
Заглушка желаемого результата компилируется нормально, но при запуске макротеста я встречаюсь с
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
Я знаю, что макрос еще не полностью реализован, но convenience init() уже доставляет мне неприятности… Я удивлен, что это так, потому что то, что я возвращаю static expansion(of:providingMembersOf:conformingTo:in:), пытается реализовать тот же самый код, который я заглушил. выше:
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)
}
"""
)
]
}
… что согласно выводу тестовой консоли выдает:
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
}
}
Полная реализация макроса выглядит следующим образом:
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)
}
"""
)
]
}
}
Да, я только что узнал об этом – буквально в тот момент, когда вы написали комментарий. Я держал это неправильно (ТМ). Спасибо!





Оказывается, я неправильно понял — каждый DeclSyntax нужно объявлять отдельно:
return [
DeclSyntax(
extendedGraphemeClusterLiteral: """
convenience init(from record: \(symbolName)) {
self.init(id: record.id, title: record.title)
}
"""
),
DeclSyntax(
stringLiteral: """
struct \(symbolName)Record: Codable {
\(memberStrings)
}
"""
)
]
Теперь я могу перейти к завершению макроса 😀
Моему будущему и всем, кому это может быть интересно: github.com/appfrosch/RecordInterfacable.git
Вы пишете два объявления в одном строковом литерале. Разделите их. Вы должны возвращать один
DeclSyntaxдля каждого создаваемого вами объявления.