A Swift library that provides ergonomic wrappers around SwiftSyntax for both reading (parsing/inspecting) and writing (building/generating) Swift syntax in macro implementations.
MacroSyn eliminates the boilerplate of working with SwiftSyntax directly, letting you focus on your macro's logic rather than syntax tree navigation.
Add MacroSyn as a dependency in your Package.swift:
.package(url: "https://github.com/Ahmed-Ali/MacroSyn.git", from: "0.0.1")Then add it to your macro target:
.macro(
name: "MyMacros",
dependencies: ["MacroSyn"]
)MacroSyn has two sides:
| Side | Purpose | Key Types |
|---|---|---|
| Reader | Parse and inspect existing Swift syntax | StructDecl, EnumDecl, Function, Variable, Property |
| Builder | Generate new Swift syntax | Func(), Init(), Struct(), Enum(), If(), Switch() |
Both sides are designed to work together — read declarations from user code, then generate new declarations using the builder DSL.
The Reader side wraps SwiftSyntax node types with ergonomic accessors. All reader types conform to the SyntaxReader protocol.
// Wrap any DeclGroupSyntax node
let structDecl = StructDecl(node) // wraps StructDeclSyntax
let classDecl = ClassDecl(node) // wraps ClassDeclSyntax
let enumDecl = EnumDecl(node) // wraps EnumDeclSyntax
let actorDecl = ActorDecl(node) // wraps ActorDeclSyntax
let protoDecl = ProtocolDecl(node) // wraps ProtocolDeclSyntaxAll declaration group readers share these properties:
structDecl.name // "User"
structDecl.accessLevel // .public, .private, etc.
structDecl.properties // [Property] — all member variables
structDecl.functions // [Function] — all member functions
structDecl.initializers // [Initializer] — all init declarations
structDecl.inheritedTypes // ["Codable", "Equatable"]
structDecl.genericParameters // [GenericParameter]let fn = Function(funcDeclSyntax)
fn.name // "fetch"
fn.accessLevel // .public
fn.returnType // "Data"
fn.isAsync // true
fn.isThrowing // true
fn.throwingErrorType // "NetworkError" (typed throws)
fn.parameters // [Parameter]
let param = fn.parameters[0]
param.label // "for" (external name)
param.secondName // "value" (internal name)
param.localName // "value" (effective name in body)
param.type // "Int"
param.isInout // false
param.isVariadic // false
param.defaultValue // nillet initializer = Initializer(initDeclSyntax)
initializer.accessLevel // .public
initializer.parameters // [Parameter]
initializer.isFailable // true (init?)
initializer.isImplicitlyUnwrapped // true (init!)
initializer.isAsync // false
initializer.isThrowing // falselet variables = Variable.from(variableDeclSyntax)
let v = variables[0]
v.name // "count"
v.type // "Int"
v.value // "0" (initializer value)
v.mutable // true (var with setter)
v.isLet // false
v.isVar // true
v.accessLevel // .public
v.setterAccessLevel // .private (for `public private(set) var`)
v.isStatic // false
v.isLazy // false
v.isWeak // falseProperty is a typealias for Variable with an additional stored property.
let enumDecl = EnumDecl(enumDeclSyntax)
for c in enumDecl.cases {
c.name // "success"
c.rawValue // "\"ok\"" (if raw-value enum)
c.hasAssociatedValues // true
c.associatedValues // [AssociatedValue]
for av in c.associatedValues {
av.label // "value" (nil if unlabeled)
av.type // "Int"
av.defaultValue // nil
}
}let proto = ProtocolDecl(protocolDeclSyntax)
proto.associatedTypes // [AssociatedType]
let at = proto.associatedTypes[0]
at.name // "Element"
at.inheritedType // "Equatable"
at.defaultType // "Int"
at.whereClause // "where Element: Comparable"All readers on types conforming to WithModifiersSyntax expose:
reader.isStatic reader.isClass
reader.isFinal reader.isLazy
reader.isWeak reader.isUnowned
reader.isMutating reader.isNonmutating
reader.isOptional reader.isNonisolated
reader.isOverride reader.isRequired
reader.isConvenience reader.isDynamic
reader.isIndirect reader.isDistributed
reader.isConsuming reader.isBorrowinglet attrs = reader.attributes // [Attribute]
attrs[0].name // "available"
attrs[0].arguments // "*, deprecated"reader.hasGenericParameters // true
reader.genericParameters // [GenericParameter]
let gp = reader.genericParameters[0]
gp.name // "T"
gp.inheritedType // "Equatable"
gp.isParameterPack // true (for `each T`)Every reader type can create diagnostics with an ergonomic builder:
// Simple error
let diag = variable.error("Must be mutable").build()
// Error on a specific token with fix-it
let diag = variable
.error("Expected 'var'", on: variable.bindingKeyword)
.fix("Change to 'var'", replace: variable.bindingKeyword, with: .var)
.build()
// Warning
let diag = function.warning("Consider making this async").build()
// Emit in macro context
context.diagnose(diag)Reader types can be interpolated directly into SwiftSyntax string interpolations:
// Property interpolates as its name
DeclSyntax("case \(prop)") // -> "case myProperty"
// AccessLevel interpolates as keyword + space
FunctionDeclSyntax("\(fn.accessLevel)func wrapper()") // -> "public func wrapper()"
// Parameter interpolates as its local name
ExprSyntax("\(param)") // -> "value"
// EnumCaseDecl interpolates as its name
ExprSyntax(".\(enumCase)") // -> ".success"The Builder side provides Swift functions that generate SwiftSyntax nodes. All builder functions are free functions with capitalized names mirroring Swift keywords.
// Built-in type constants
TypeRef.bool, .int, .double, .float, .string, .void, .any
TypeRef.data, .date, .url, .error
// Constructors
TypeRef.custom("MyModel")
TypeRef.array(of: .int) // [Int]
TypeRef.dict(key: .string, value: .int) // [String: Int]
TypeRef.optional(.string) // String?
TypeRef.result(success: .data) // Result<Data, Error>Literal.true, .false, .nil, .self
Literal.zero, .one, .int(42)
Literal.emptyString, .string("hello")
Literal.emptyArray, .emptyDict
Literal.expr("someExpression()")// String type name
Arg("name", type: "String")
// Metatype (Any.Type)
Arg("url", type: URL.self)
// External + internal name
Arg("for", name: "value", type: Int.self)ArgType is a marker protocol for extensibility — String conforms out of the box.
The Func() function returns a FuncBuilder that you terminate with .returns(type) { body } or .body { ... }:
// Function with return type
try Func("fetch", Arg("url", type: URL.self), async: true, throws: true)
.returns(.data) {
Return(expr: "try await URLSession.shared.data(from: url)")
}
// Void function
try Func("configure", Arg("name", type: String.self))
.body {
"self.name = name"
}
// Static, public function
try Func("create", access: .public, static: true)
.returns(.custom("Self")) {
Return(expr: "Self()")
}try Init(Arg("name", type: String.self), Arg("age", type: Int.self), access: .public) {
"self.name = name"
"self.age = age"
}
// Failable initializer
try Init(Arg("value", type: Int.self), failable: true) {
try Guard("value > 0", otherwise: { Return(.nil) })
"self.value = value"
}
// Array overload for programmatic use
let args = properties.map { Arg($0.name, type: $0.type!) }
try Init(args, access: .public) { ... }StoredVar("count", type: .int, value: .zero)
StoredVar("name", type: .string, isLet: true, access: .public)try Var("isValid", type: .bool) {
Return(.true)
}
try Var("description", type: .string, access: .public) {
Return(expr: "\"\\(name): \\(age)\"")
}try Struct("User", access: .public, inherits: ["Codable", "Equatable"]) {
StoredVar("name", type: .string)
StoredVar("age", type: .int)
}try Class("ViewModel", access: .public, inherits: ["ObservableObject"]) {
StoredVar("title", type: .string, value: .emptyString)
}try Enum("Direction", inherits: ["String"]) {
Case("north")
Case("south", rawValue: "\"S\"")
Case("error", args: (nil, "String"), ("code", "Int"))
}try Extension("User", conformingTo: ["CustomStringConvertible"]) {
try Var("description", type: .string) {
Return(expr: "name")
}
}try If("condition") {
Return(.true)
} else: {
Return(.false)
}try Guard("let value = optional", otherwise: {
Return(.nil)
})try Switch("direction") {
try SwitchCase("case .north:") {
Return(expr: "\"up\"")
}
try SwitchCase("default:") {
Return(expr: "\"unknown\"")
}
}try For("item", in: "items") {
"process(item)"
}
try While("condition") {
"doWork()"
}try Do {
"try riskyOperation()"
} catch: {
try Catch("let error as NetworkError") {
"handleNetwork(error)"
}
try Catch {
"print(error)"
}
}Return(.true)
Return(expr: "value + 1")
Return()
Throw("MyError.invalid")Here's a complete macro that generates a memberwise initializer using both the Reader and Builder APIs:
import MacroSyn
import SwiftSyntax
import SwiftSyntaxMacros
public struct MemberwiseInitMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf decl: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Reader side: inspect the struct
guard let syntax = decl.as(StructDeclSyntax.self) else { return [] }
let structDecl = StructDecl(syntax)
let properties = structDecl.properties.filter { !$0.isStatic && $0.type != nil }
guard !properties.isEmpty else { return [] }
// Builder side: generate the init
let args = properties.map { Arg($0.name, type: $0.type!) }
let initDecl = try Init(args, access: .public) {
for prop in properties {
CodeBlockItemSyntax(stringLiteral: "self.\(prop.name) = \(prop.name)")
}
}
return [initDecl]
}
}Applied to:
@MemberwiseInit
struct User {
let name: String
var age: Int
}Generates:
public init(name: String, age: Int) {
self.name = name
self.age = age
}The Sources/Examples/ directory contains working macro implementations:
| Macro | Type | Demonstrates |
|---|---|---|
@CaseDetection |
MemberMacro | EnumDecl reader + Var, If, Return builders |
@Watch |
BodyMacro | Attribute argument parsing + code block manipulation |
@MemberwiseInit |
MemberMacro | StructDecl reader + Init, Arg builders |
@CustomCodingKeys |
MemberMacro | StructDecl reader + Enum, Case builders |
Sources/MacroSyn/
├── Reader/
│ ├── SyntaxReader.swift — Core protocol + trait extensions
│ ├── TraitsReader.swift — Modifier, inheritance, generic traits
│ ├── Expressions.swift — Literal and collection expression readers
│ └── Initializer.swift — Initializer declaration reader
├── Builder/
│ ├── DSL/
│ │ ├── Arg.swift — ArgType protocol + Arg parameter descriptor
│ │ ├── Decl.swift — TypeRef, Var, StoredVar, Func, Init builders
│ │ ├── TypeDecl.swift — Struct, Class, Enum, Extension, Case builders
│ │ ├── Stmt.swift — If, Guard, Return, For, While, Throw, Switch, Do/Catch
│ │ └── Expr.swift — Literal values
│ └── Interpolation.swift — SyntaxStringInterpolation extensions
├── StructDecl.swift — Struct declaration reader
├── ClassDecl.swift — Class declaration reader
├── EnumDecl.swift — Enum declaration + case readers
├── ActorDecl.swift — Actor declaration reader
├── ProtocolDecl.swift — Protocol declaration + associated type readers
├── Function.swift — Function + Parameter readers
├── Variable.swift — Variable reader (binding analysis, tuple destructuring)
├── Property.swift — Property typealias + stored property detection
├── GroupDecl.swift — Shared DeclGroupSyntax member accessors
├── Attribute.swift — Attribute reader
├── CodeBlock.swift — Statement + CodeBlock readers
└── Diagnostics.swift — DiagnosticBuilder + ergonomic extensions
- Swift 6.0+
- SwiftSyntax 600.0+
See LICENSE for details.