Section 5/101 menit
5. Cara Membuat Swift Macro
5. Cara Membuat Swift Macro
Step 1: Buat Swift Package untuk Macro
Gunakan Xcode File menu atau command line:
swift
# Di terminal:
mkdir MyMacros && cd MyMacros
swift package init --type macro
Atau di Xcode: File → New → Package → Swift Macro
Step 2: Struktur Folder
swift
MyMacros/
├── Package.swift
├── Sources/
│ ├── MyMacros/ ← Public API (definisi macro)
│ │ └── MyMacros.swift
│ └── MyMacrosMacros/ ← Implementasi (plugin)
│ └── MyMacrosMacro.swift
└── Tests/
└── MyMacrosTests/
└── MyMacrosTests.swift
Step 3: Konfigurasi Package.swift
swift
// swift-tools-version: 5.9
import PackageDescription
import CompilerPluginSupport // ← wajib untuk macro
let package = Package(
name: "MyMacros",
platforms: [
.macOS(.v13),
.iOS(.v13),
],
dependencies: [
.package(
url: "https://github.com/apple/swift-syntax.git",
from: "510.0.0" // sesuaikan dengan versi Swift toolchain
),
],
targets: [
// Target implementasi macro (plugin)
.macro(
name: "MyMacrosMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
]
),
// Target public API yang digunakan konsumen
.target(
name: "MyMacros",
dependencies: ["MyMacrosMacros"]
),
// Target test
.testTarget(
name: "MyMacrosTests",
dependencies: [
"MyMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
Step 4: Implementasi Macro — Contoh @AutoEquatable
File: Sources/MyMacros/MyMacros.swift (Public API)
swift
import Foundation
// Deklarasi public — ini yang digunakan oleh konsumen macro
@attached(conformance, conformances: Equatable)
@attached(member, names: named(==))
public macro AutoEquatable() = #externalMacro(
module: "MyMacrosMacros",
type: "AutoEquatableMacro"
)
File: Sources/MyMacrosMacros/AutoEquatableMacro.swift (Implementasi)
swift
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct AutoEquatableMacro: MemberMacro, ConformanceMacro {
// MARK: - ConformanceMacro: tambahkan Equatable conformance
public static func expansion(
of node: AttributeSyntax,
providingConformancesOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
return [("Equatable", nil)]
}
// MARK: - MemberMacro: generate fungsi ==
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
// Ambil nama type
guard let typeName = declaration.asProtocol(NamedDeclSyntax.self)?.name.text else {
throw MacroError.notApplicable("@AutoEquatable hanya untuk named types")
}
// Ambil semua stored property
let storedVars = declaration.memberBlock.members
.compactMap { $0.decl.as(VariableDeclSyntax.self) }
.filter { decl in
// Hanya stored property (bukan computed)
decl.bindings.first?.accessorBlock == nil
}
if storedVars.isEmpty {
// Jika tidak ada property, return == trivial
return [
"""
public static func == (lhs: \(raw: typeName), rhs: \(raw: typeName)) -> Bool {
return true
}
"""
]
}
// Generate perbandingan untuk setiap property
let comparisons = storedVars.compactMap { variable -> String? in
guard let propName = variable.bindings.first?.pattern.description else { return nil }
return "lhs.\(propName) == rhs.\(propName)"
}.joined(separator: " &&\n ")
return [
"""
public static func == (lhs: \(raw: typeName), rhs: \(raw: typeName)) -> Bool {
return \(raw: comparisons)
}
"""
]
}
}
// MARK: - Custom Error
enum MacroError: Error, CustomStringConvertible {
case notApplicable(String)
var description: String {
switch self {
case .notApplicable(let msg): return msg
}
}
}
// MARK: - Plugin Registration
@main
struct MyMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
AutoEquatableMacro.self,
]
}
Step 5: Testing Macro
Testing macro menggunakan assertMacroExpansion untuk memverifikasi hasil expansion:
swift
import XCTest
import SwiftSyntaxMacrosTestSupport
@testable import MyMacrosMacros
final class AutoEquatableMacroTests: XCTestCase {
let testMacros: [String: Macro.Type] = [
"AutoEquatable": AutoEquatableMacro.self,
]
func testAutoEquatableOnStruct() throws {
assertMacroExpansion(
// Input: kode sebelum expansion
"""
@AutoEquatable
struct Point {
var x: Double
var y: Double
}
""",
// Expected output: kode setelah expansion
expandedSource: """
struct Point {
var x: Double
var y: Double
public static func == (lhs: Point, rhs: Point) -> Bool {
return lhs.x == rhs.x &&
lhs.y == rhs.y
}
}
extension Point: Equatable {}
""",
macros: testMacros
)
}
func testAutoEquatableOnEmptyStruct() throws {
assertMacroExpansion(
"""
@AutoEquatable
struct Empty {}
""",
expandedSource: """
struct Empty {
public static func == (lhs: Empty, rhs: Empty) -> Bool {
return true
}
}
extension Empty: Equatable {}
""",
macros: testMacros
)
}
}
Step 6: Integrasi ke Xcode Project
Opsi A: Local Package (development)
Drag folder MyMacros ke Xcode project navigator. Lalu di target Build Phases → Link Binary With Libraries, tambahkan MyMacros.framework.
Opsi B: Swift Package Dependency (distribusi)
Di Package.swift aplikasi atau di Xcode → Add Package Dependencies:
swift
dependencies: [
.package(path: "../MyMacros"), // local
// atau
.package(url: "https://github.com/username/MyMacros", from: "1.0.0"), // remote
]
Kemudian gunakan di kode aplikasi:
swift
import MyMacros
@AutoEquatable
struct User {
let id: UUID
let name: String
}
// Sekarang User.== tersedia otomatis
let u1 = User(id: UUID(), name: "Alice")
let u2 = User(id: UUID(), name: "Bob")
print(u1 == u2) // false