CodableMacro/
├── Package.swift
├── Sources/
│ ├── CodableMacro/ # 매크로 선언
│ └── CodableMacroCore/ # 매크로 구현
└── Tests/
└── CodableMacrosTests/ # 매크로 테스트
public
등의 접근제어자, @discardableResult
, @objc
, 또는 프로퍼티 래퍼를 추가할 수 있다.attached(conformance)
가 필요하다.init(from:)
)와 타입(CodingKeys
)을 추가해야하므로 attached(member)
가 필요하다.// CodableMacros.swift
@attached(extension, conformances: Decodable)
@attached(member, names: named(CodingKeys), named(init(from:)))
// CodableMacroCore 폴더 안에 정의된 DecodableMacro 타입을 보라는 것. DecodableMacro는 아직 정의되지 않았다.
public macro Decodable() = #externalMacro(module: "CodableMacroCore", type: "DecodableMacro")
이렇게 정의하면 아래처럼 사용할 수 있다.
@Decodable
struct Person {
let name: String
}
.named(CodingKeys), named(init(from:)
를 사용해서 이름을 명시해줬다. .prefix(String)
, .suffix(String)
를 써서 선언에 붙은 이름에 prefix/suffix가 붙는 이름의 선언이 추가됨을 알려주거나.overloaded
로 동일한 이름의 선언이 추가됨을 알려주거나.arbitrary
로 무작위 이름의 선언이 추가된다고 알려줄 수 있다..arbitrary
로 정의하면 어떤게 추가돼도 에러가 뜨지 않는다.)// CodableMacroTests.swift
import SwiftSyntaxMacros
import SwiftSyntaxMacrosTestSupport
import XCTest
import CodableMacroCore
// 테스트를 원하는 매크로들, 선언의 #externalMacro(module:type:) 에서 type에 써준 이름을 그대로 써준다.
// 아직은 해당 타입을 추가하지 않아서 에러가 뜬다.
// 테스트에서 자동으로 @Decodable 부분을 DecodableMacro 로 변경해준다.
let testMacros: [String: Macro.Type] = [
"Decodable": DecodableMacro.self,
]
final class CodableMacroTests: XCTestCase {
func testMacro() throws {
assertMacroExpansion(
"""
@Decodable
struct Person {
let name: String
var age: Int?
}
""",
expandedSource: """
struct Person {
let name: String
var age: Int?
enum CodingKeys: String, CodingKey {
case name, age
}
init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decodeIfPresent(Int.self, forKey: .age)
}
}
""",
macros: testMacros
)
}
}
assertMacroExpansion
은 첫번째 arg로 매크로가 사용된 코드를 받고, 두번째 arg로 해당 코드가 확장할 결과물을 받는다. 둘 사이가 다르면 에러가 난다.\n
)나 tab 하나만 달라져도 에러가 난다는 것. 처음 작성할 때는 정성껏 작성해주고 나중에 코드 수정하면서 테스트 변경할 때는 테스트를 하나씩 실행하고 콘솔에서 actual source를 복붙한 후 변경된 부분을 git으로 확인하면 공수를 줄일 수 있다.// CodableMacroCore.swift
import SwiftCompilerPlugin // Swift 컴파일러와 연결되어 매크로 기능을 등록하고 실행할 수 있게 해준다.
import SwiftSyntax // 소스 코드를 Sytax Tree 구조로 표현해준다.
import SwiftSyntaxBuilder // Syntax Tree 구성을 위한 편리 API를 제공한다.
import SwiftSyntaxMacros // 매크로 작성에 필요한 프로토콜과 타입을 제공한다.
/// 매크로 기능을 Swift 컴파일러에 등록하는 진입점
/// @main은 이 구조체가 프로그램의 시작점임을 나타낸다.
///
/// CompilerPlugin을 채택하여:
/// 1. 컴파일러가 이 매크로 패키지를 플러그인으로 인식하게 하고
/// 2. providingMacros 배열을 통해 사용 가능한 매크로 목록을 컴파일러에 알린다.
///
/// 새로운 매크로를 추가하려면 providingMacros 배열에 해당 매크로 타입을 추가해야한다.
@main
struct CodableMacroPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
DecodableMacro.self
]
}
// DecodableMacro.swift
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
public struct DecodableMacro: ExtensionMacro, MemberMacro {
// ExtensionMacro
public static func expansion(
of node: SwiftSyntax.AttributeSyntax,
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo protocols: [SwiftSyntax.TypeSyntax],
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
[]
}
// MemberMacro
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
[]
}
}
StructDeclSyntax
: 구조체 선언ClassDeclSyntax
: 클래스 선언FunctionDeclSyntax
: 함수 선언VariableDeclSyntax
: 변수 선언LiteralExprSyntax
: 리터럴 표현식(문자열, 숫자 등)MemberAccessExprSyntax
: 멤버 접근 표현식FunctionCallExprSyntax
: 함수 호출 표현식ArrayExprSyntax
: 배열 표현식ReturnStmtSyntax
: return 구문IfStmtSyntax
: if 구문ForStmtSyntax
: for 루프 구문SimpleTypeIdentifierSyntax
: 간단한 타입 식별자ArrayTypeSyntax
: 배열 타입OptionalTypeSyntax
: 옵셔널 타입declaration.as(StructDeclSyntax.self)
structDecl.memberBlock
또는 for member in memberBlock.member
// ExtensionMacro
public static func expansion(
of node: SwiftSyntax.AttributeSyntax,
attachedTo declaration: some SwiftSyntax.DeclGroupSyntax,
providingExtensionsOf type: some SwiftSyntax.TypeSyntaxProtocol,
conformingTo protocols: [SwiftSyntax.TypeSyntax],
in context: some SwiftSyntaxMacros.MacroExpansionContext
) throws -> [SwiftSyntax.ExtensionDeclSyntax] {
let decodableExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Decodable {}")
return [decodableExtension]
}
Actual expanded source:
struct Person {
let name: String
var age: Int?
}
extension Person: Decodable {
}
// CodableMacroCore
// CodableMacroError.swift
enum CodableMacroError: Error, CustomStringConvertible {
case message(String)
var description: String {
switch self {
case .message(let text):
return text
}
}
}
// Decodable annotation이 struct/class에 추가된 것인지 체크
guard declaration.is(StructDeclSyntax.self) || declaration.is(ClassDeclSyntax.self) else {
throw CodableMacroError.message("@Codable은 class 또는 struct에 선언되어야 합니다.")
}
let decodableExtension = try ExtensionDeclSyntax("extension \(type.trimmed): Decodable {}")
return [decodableExtension]
func testErrorIfEnum() throws {
assertMacroExpansion(
"""
@Decodable
enum Cafe {
case starbucks
}
""",
expandedSource: """
enum Cafe {
case starbucks
}
""",
diagnostics: [
DiagnosticSpec(message: "@Codable은 class 또는 struct에 선언되어야 합니다.", line: 1, column: 1)
],
macros: testMacros
)
}
@Decodable
이 정의된 부분에서 에러가 발생할 것이므로 (1,1) 을 넣어줬다.CodingKeys
, init(from:)
선언도 추가할 수 있다.