From f5e131347fb3c06d5df5e2e62df5d7a22477d5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elon=20Park=20=28=E1=84=8B=E1=85=A6=E1=86=AF=E1=84=85?= =?UTF-8?q?=E1=85=A9=E1=86=AB=29?= Date: Tue, 7 Apr 2026 13:09:37 +0900 Subject: [PATCH 1/5] fix: convert PolymorphicEnumCodableMacro from extension to member macro for nested type support --- .../Interface/PolymorphicEnumCodable.swift | 53 +++---- .../PolymorphicEnumCodableMacro.swift | 48 +++--- .../PolymorphicEnumCodableMacroTests.swift | 141 ++++++++++++++---- 3 files changed, 159 insertions(+), 83 deletions(-) diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift index 0d3f032..9f03658 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift @@ -7,36 +7,29 @@ import Foundation -/** - A macro that makes enum types polymorphically codable. - - This macro adds Codable conformance to enum types, allowing them to be serialized and - deserialized based on a type identifier. It generates the necessary coding keys, - initializer, and encoding methods. - - Each enum case must have exactly one associated value, and the type of that value must conform - to `PolymorphicIdentifiable`. - - - Parameters: - - identifierCodingKey: The key name in the JSON used to store the type identifier. - The default value for this property is `"type"`. This key is used to identify the specific - case of the enum during - - fallbackCaseName: The name of the `case` to use when the type identifier is not found. - The default value for this property is `nil`. If this property is not provided, the macro will - throw an error if the type identifier is not found. - - - Warning: When decoding falls back to the fallback case, during encoding the original `type` value - will not be preserved. Instead, the `type` value of the fallback case will be used in the - encoded output. - */ -@attached( - extension, - conformances: Codable, - names: named(PolymorphicMetaCodingKey), - named(init), - named(encode) -) +/// A macro that makes enum types polymorphically codable. +/// +/// This macro adds Codable conformance to enum types, allowing them to be serialized and +/// deserialized based on a type identifier. It generates the necessary coding keys, +/// initializer, and encoding methods. +/// +/// Each enum case must have exactly one associated value, and the type of that value must conform +/// to `PolymorphicIdentifiable`. +/// +/// - Parameters: +/// - identifierCodingKey: The key name in the JSON used to store the type identifier. +/// The default value for this property is `"type"`. This key is used to identify the specific +/// case of the enum during +/// - fallbackCaseName: The name of the `case` to use when the type identifier is not found. +/// The default value for this property is `nil`. If this property is not provided, the macro will +/// throw an error if the type identifier is not found. +/// +/// - Warning: When decoding falls back to the fallback case, during encoding the original `type` value +/// will not be preserved. Instead, the `type` value of the fallback case will be used in the +/// encoded output. +@attached(member, names: named(PolymorphicMetaCodingKey), named(init), named(encode)) +@attached(extension, conformances: Codable) public macro PolymorphicEnumCodable( identifierCodingKey: String = "type", - fallbackCaseName: String? = nil + fallbackCaseName: String? = nil, ) = #externalMacro(module: "KarrotCodableKitMacros", type: "PolymorphicEnumCodableMacro") diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift index f811fac..203431a 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift @@ -10,29 +10,21 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -public struct PolymorphicEnumCodableMacro: ExtensionMacro { +public struct PolymorphicEnumCodableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, - attachedTo declaration: some DeclGroupSyntax, - providingExtensionsOf type: some TypeSyntaxProtocol, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [ExtensionDeclSyntax] { - // Ensure the declaration is an enum and extract case information + providingMembersOf declaration: some DeclGroupSyntax, + in _: some MacroExpansionContext, + ) throws -> [DeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw CodableKitError.message("`@PolymorphicEnumCodable` can only be attached to enums") } - // Validate and extract identifierCodingKey let identifierCodingKey = try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) - - // Extract case information from the enum let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) - - // Validate and extract fallbackCaseName if provided let fallbackCaseName = try PolymorphicEnumCodableFactory.validateFallbackCaseName( in: node, - caseInfos: caseInfos + caseInfos: caseInfos, ) let polymorphicMetaCodingKeySyntax = PolymorphicEnumCodableFactory.makePolymorphicMetaCodingKey( @@ -45,24 +37,34 @@ public struct PolymorphicEnumCodableMacro: ExtensionMacro { with: caseInfos, identifierCodingKey: identifierCodingKey, accessLevel: accessLevel, - fallbackCaseName: fallbackCaseName + fallbackCaseName: fallbackCaseName, ) let encodeToEncoderSyntax = PolymorphicEnumCodableFactory.makeEncodeToEncoder( with: caseInfos, - accessLevel: accessLevel + accessLevel: accessLevel, ) return [ - try ExtensionDeclSyntax("extension \(raw: enumDecl.name.text): Codable") { - """ - \(raw: polymorphicMetaCodingKeySyntax) + "\(raw: polymorphicMetaCodingKeySyntax)", + "\(raw: initFromDecoderSyntax)", + "\(raw: encodeToEncoderSyntax)", + ] + } +} - \(raw: initFromDecoderSyntax) +extension PolymorphicEnumCodableMacro: ExtensionMacro { + public static func expansion( + of _: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo _: [TypeSyntax], + in _: some MacroExpansionContext, + ) throws -> [ExtensionDeclSyntax] { + guard declaration.is(EnumDeclSyntax.self) else { + return [] + } - \(raw: encodeToEncoderSyntax) - """ - }, - ] + return try [ExtensionDeclSyntax("extension \(type.trimmed): Codable {}")] } } diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift index 22f86aa..a642219 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift @@ -1,5 +1,5 @@ // -// PolymorphicEnumCodableEnumMacroTests.swift +// PolymorphicEnumCodableMacroTests.swift // // // Created by Elon on 10/19/24. @@ -15,10 +15,9 @@ import KarrotCodableKitMacros #endif final class PolymorphicEnumCodableMacroTests: XCTestCase { - #if canImport(KarrotCodableKitMacros) let testMacros: [String: Macro.Type] = [ - "PolymorphicEnumCodable": PolymorphicEnumCodableMacro.self, + "PolymorphicEnumCodable": PolymorphicEnumCodableMacro.self ] #endif @@ -41,9 +40,7 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Codable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -75,9 +72,12 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Codable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -103,9 +103,7 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Codable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -137,9 +135,12 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Codable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -170,11 +171,11 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { DiagnosticSpec( message: "`@PolymorphicEnumCodable` can only be attached to enums", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -200,16 +201,19 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Codable { + } """, diagnostics: [ DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -235,16 +239,19 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Codable { + } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -270,16 +277,83 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Codable { + } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} + +// MARK: - Nested Type + +extension PolymorphicEnumCodableMacroTests { + func testPolymorphicEnumCodableMacroInsideNestedType() throws { + #if canImport(KarrotCodableKitMacros) + // given + assertMacroExpansion( + """ + enum SomeEnum { + @PolymorphicEnumCodable(identifierCodingKey: "type") + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + } + } + """, + // when + expandedSource: """ + enum SomeEnum { + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + + enum PolymorphicMetaCodingKey: CodingKey { + case `type` + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: PolymorphicMetaCodingKey.self) + let type = try container.decode(String.self, forKey: PolymorphicMetaCodingKey.type) + + switch type { + case DummyCallout.polymorphicIdentifier: + self = .callout(try DummyCallout(from: decoder)) + case DummyActionableCallout.polymorphicIdentifier: + self = .actionableCallout(try DummyActionableCallout(from: decoder)) + default: + throw PolymorphicCodableError.unableToFindPolymorphicType(type) + } + } + + public func encode(to encoder: any Encoder) throws { + switch self { + case .callout(let value): + try value.encode(to: encoder) + case .actionableCallout(let value): + try value.encode(to: encoder) + } + } + } + } + + extension SomeEnum.CalloutBadge: Codable { + } + """, + macros: testMacros, + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -311,9 +385,7 @@ extension PolymorphicEnumCodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) case undefinedCallout(DummyUndefinedCallout) - } - extension CalloutBadge: Codable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -349,9 +421,12 @@ extension PolymorphicEnumCodableMacroTests { } } } + + extension CalloutBadge: Codable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -377,16 +452,19 @@ extension PolymorphicEnumCodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Codable { + } """, diagnostics: [ DiagnosticSpec( message: "Missing fallback case: should be defined as `case undefinedCallout", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -412,16 +490,19 @@ extension PolymorphicEnumCodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Codable { + } """, diagnostics: [ DiagnosticSpec( message: "Invalid fallback case name: expected a non-empty string.", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") From 29ad1b2869fa87779b80613c86100f9e5327dd63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elon=20Park=20=28=E1=84=8B=E1=85=A6=E1=86=AF=E1=84=85?= =?UTF-8?q?=E1=85=A9=E1=86=AB=29?= Date: Tue, 7 Apr 2026 13:09:41 +0900 Subject: [PATCH 2/5] fix: convert PolymorphicEnumDecodableMacro from extension to member macro for nested type support --- .../Interface/PolymorphicEnumDecodable.swift | 37 +++-- .../PolymorphicEnumDecodableMacro.swift | 47 +++---- .../PolymorphicEnumDecodableMacroTests.swift | 130 ++++++++++++++---- 3 files changed, 143 insertions(+), 71 deletions(-) diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift index 513b07e..5e0b3cf 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift @@ -7,25 +7,24 @@ import Foundation -/** - A macro that makes enum types polymorphically decodable. - - This macro adds Decodable conformance to enum types, allowing them to be deserialized - based on a type identifier. It generates the necessary coding keys and initializer methods. - - Each enum case must have exactly one associated value, and the type of that value must conform to - `PolymorphicIdentifiable`. - - - Parameters: - - identifierCodingKey: The key name in the JSON used to store the type identifier. - The default value for this property is `"type"`. This key is used to identify the specific - case of the enum during - - fallbackCaseName: The name of the `case` to use when the type identifier is not found. - The default value for this property is `nil`. If this property is not provided, the macro will - throw an error if the type identifier is not found. - */ -@attached(extension, conformances: Decodable, names: named(PolymorphicMetaCodingKey), named(init)) +/// A macro that makes enum types polymorphically decodable. +/// +/// This macro adds Decodable conformance to enum types, allowing them to be deserialized +/// based on a type identifier. It generates the necessary coding keys and initializer methods. +/// +/// Each enum case must have exactly one associated value, and the type of that value must conform to +/// `PolymorphicIdentifiable`. +/// +/// - Parameters: +/// - identifierCodingKey: The key name in the JSON used to store the type identifier. +/// The default value for this property is `"type"`. This key is used to identify the specific +/// case of the enum during +/// - fallbackCaseName: The name of the `case` to use when the type identifier is not found. +/// The default value for this property is `nil`. If this property is not provided, the macro will +/// throw an error if the type identifier is not found. +@attached(member, names: named(PolymorphicMetaCodingKey), named(init)) +@attached(extension, conformances: Decodable) public macro PolymorphicEnumDecodable( identifierCodingKey: String = "type", - fallbackCaseName: String? = nil + fallbackCaseName: String? = nil, ) = #externalMacro(module: "KarrotCodableKitMacros", type: "PolymorphicEnumDecodableMacro") diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift index 9c601b9..493b091 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift @@ -6,35 +6,25 @@ // Copyright © 2025 Danggeun Market Inc. All rights reserved. // -import Foundation - import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -public struct PolymorphicEnumDecodableMacro: ExtensionMacro { +public struct PolymorphicEnumDecodableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, - attachedTo declaration: some DeclGroupSyntax, - providingExtensionsOf type: some TypeSyntaxProtocol, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [ExtensionDeclSyntax] { - // Ensure the declaration is an enum and extract case information + providingMembersOf declaration: some DeclGroupSyntax, + in _: some MacroExpansionContext, + ) throws -> [DeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw CodableKitError.message("`@PolymorphicEnumDecodable` can only be attached to enums") } - // Validate and extract identifierCodingKey let identifierCodingKey = try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) - - // Extract case information from the enum let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) - - // Validate and extract fallbackCaseName if provided let fallbackCaseName = try PolymorphicEnumCodableFactory.validateFallbackCaseName( in: node, - caseInfos: caseInfos + caseInfos: caseInfos, ) let polymorphicMetaCodingKeySyntax = PolymorphicEnumCodableFactory.makePolymorphicMetaCodingKey( @@ -47,17 +37,28 @@ public struct PolymorphicEnumDecodableMacro: ExtensionMacro { with: caseInfos, identifierCodingKey: identifierCodingKey, accessLevel: accessLevel, - fallbackCaseName: fallbackCaseName + fallbackCaseName: fallbackCaseName, ) return [ - try ExtensionDeclSyntax("extension \(raw: enumDecl.name.text): Decodable") { - """ - \(raw: polymorphicMetaCodingKeySyntax) - - \(raw: initFromDecoderSyntax) - """ - }, + "\(raw: polymorphicMetaCodingKeySyntax)", + "\(raw: initFromDecoderSyntax)", ] } } + +extension PolymorphicEnumDecodableMacro: ExtensionMacro { + public static func expansion( + of _: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo _: [TypeSyntax], + in _: some MacroExpansionContext, + ) throws -> [ExtensionDeclSyntax] { + guard declaration.is(EnumDeclSyntax.self) else { + return [] + } + + return try [ExtensionDeclSyntax("extension \(type.trimmed): Decodable {}")] + } +} diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift index 0a94f94..7a34125 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift @@ -15,10 +15,9 @@ import KarrotCodableKitMacros #endif final class PolymorphicEnumDecodableMacroTests: XCTestCase { - #if canImport(KarrotCodableKitMacros) let testMacros: [String: Macro.Type] = [ - "PolymorphicEnumDecodable": PolymorphicEnumDecodableMacro.self, + "PolymorphicEnumDecodable": PolymorphicEnumDecodableMacro.self ] #endif @@ -41,9 +40,7 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Decodable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -64,9 +61,12 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Decodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -92,9 +92,7 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Decodable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -115,9 +113,12 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Decodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -148,11 +149,11 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { DiagnosticSpec( message: "`@PolymorphicEnumDecodable` can only be attached to enums", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -178,16 +179,19 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Decodable { + } """, diagnostics: [ DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -213,16 +217,19 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Decodable { + } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -248,16 +255,74 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Decodable { + } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +} + +// MARK: - Nested Type + +extension PolymorphicEnumDecodableMacroTests { + func testPolymorphicEnumDecodableMacroInsideNestedType() throws { + #if canImport(KarrotCodableKitMacros) + // given + assertMacroExpansion( + """ + enum SomeEnum { + @PolymorphicEnumDecodable(identifierCodingKey: "type") + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + } + } + """, + // when + expandedSource: """ + enum SomeEnum { + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + + enum PolymorphicMetaCodingKey: CodingKey { + case `type` + } + + public init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: PolymorphicMetaCodingKey.self) + let type = try container.decode(String.self, forKey: PolymorphicMetaCodingKey.type) + + switch type { + case DummyCallout.polymorphicIdentifier: + self = .callout(try DummyCallout(from: decoder)) + case DummyActionableCallout.polymorphicIdentifier: + self = .actionableCallout(try DummyActionableCallout(from: decoder)) + default: + throw PolymorphicCodableError.unableToFindPolymorphicType(type) + } + } + } + } + + extension SomeEnum.CalloutBadge: Decodable { + } + """, + macros: testMacros, + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -289,9 +354,7 @@ extension PolymorphicEnumDecodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) case undefinedCallout(DummyUndefinedCallout) - } - extension CalloutBadge: Decodable { enum PolymorphicMetaCodingKey: CodingKey { case `type` } @@ -314,9 +377,12 @@ extension PolymorphicEnumDecodableMacroTests { } } } + + extension CalloutBadge: Decodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -342,16 +408,19 @@ extension PolymorphicEnumDecodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Decodable { + } """, diagnostics: [ DiagnosticSpec( message: "Missing fallback case: should be defined as `case undefinedCallout", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -377,16 +446,19 @@ extension PolymorphicEnumDecodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Decodable { + } """, diagnostics: [ DiagnosticSpec( message: "Invalid fallback case name: expected a non-empty string.", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") From e6bf128c60d966abc3fb0b8a9b6387e288260b4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elon=20Park=20=28=E1=84=8B=E1=85=A6=E1=86=AF=E1=84=85?= =?UTF-8?q?=E1=85=A9=E1=86=AB=29?= Date: Tue, 7 Apr 2026 13:09:47 +0900 Subject: [PATCH 3/5] fix: convert PolymorphicEnumEncodableMacro from extension to member macro for nested type support --- .../Interface/PolymorphicEnumEncodable.swift | 29 +++--- .../PolymorphicEnumEncodableMacro.swift | 38 ++++---- .../PolymorphicEnumEncodableMacroTests.swift | 92 +++++++++++++++---- 3 files changed, 108 insertions(+), 51 deletions(-) diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift index 24e20e5..d419b66 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift @@ -7,21 +7,20 @@ import Foundation -/** - A macro that makes enum types polymorphically encodable. - - This macro adds Encodable conformance to enum types, allowing them to be serialized - based on a type identifier. It generates the necessary coding keys and encoding methods. - - Each enum case must have exactly one associated value, and the type of that value must conform - to `PolymorphicIdentifiable`. - - - Parameters: - - identifierCodingKey: The key name in the JSON used to store the type identifier. - The default value for this property is `"type"`. This key is used to identify the specific - case of the enum during - */ -@attached(extension, conformances: Encodable, names: named(PolymorphicMetaCodingKey), named(encode)) +/// A macro that makes enum types polymorphically encodable. +/// +/// This macro adds Encodable conformance to enum types, allowing them to be serialized +/// based on a type identifier. It generates the necessary coding keys and encoding methods. +/// +/// Each enum case must have exactly one associated value, and the type of that value must conform +/// to `PolymorphicIdentifiable`. +/// +/// - Parameters: +/// - identifierCodingKey: The key name in the JSON used to store the type identifier. +/// The default value for this property is `"type"`. This key is used to identify the specific +/// case of the enum during +@attached(member, names: named(PolymorphicMetaCodingKey), named(encode)) +@attached(extension, conformances: Encodable) public macro PolymorphicEnumEncodable( identifierCodingKey: String = "type" ) = #externalMacro(module: "KarrotCodableKitMacros", type: "PolymorphicEnumEncodableMacro") diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift index 080dc30..393bef0 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift @@ -10,38 +10,44 @@ import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros -public struct PolymorphicEnumEncodableMacro: ExtensionMacro { +public struct PolymorphicEnumEncodableMacro: MemberMacro { public static func expansion( of node: AttributeSyntax, - attachedTo declaration: some DeclGroupSyntax, - providingExtensionsOf type: some TypeSyntaxProtocol, - conformingTo protocols: [TypeSyntax], - in context: some MacroExpansionContext - ) throws -> [ExtensionDeclSyntax] { - // Ensure the declaration is an enum and extract case information + providingMembersOf declaration: some DeclGroupSyntax, + in _: some MacroExpansionContext, + ) throws -> [DeclSyntax] { guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { throw CodableKitError.message("`@PolymorphicEnumEncodable` can only be attached to enums") } - // Validate identifierCodingKey try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) - // Extract case information from the enum let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) - let accessLevel = AccessLevelModifier.stringValue(from: declaration) let encodeToEncoderSyntax = PolymorphicEnumCodableFactory.makeEncodeToEncoder( with: caseInfos, - accessLevel: accessLevel + accessLevel: accessLevel, ) return [ - try ExtensionDeclSyntax("extension \(raw: enumDecl.name.text): Encodable") { - """ - \(raw: encodeToEncoderSyntax) - """ - }, + "\(raw: encodeToEncoderSyntax)" ] } } + +extension PolymorphicEnumEncodableMacro: ExtensionMacro { + public static func expansion( + of _: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingExtensionsOf type: some TypeSyntaxProtocol, + conformingTo _: [TypeSyntax], + in _: some MacroExpansionContext, + ) throws -> [ExtensionDeclSyntax] { + guard declaration.is(EnumDeclSyntax.self) else { + return [] + } + + return try [ExtensionDeclSyntax("extension \(type.trimmed): Encodable {}")] + } +} diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift index b22a87b..1e5c89d 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift @@ -15,10 +15,9 @@ import KarrotCodableKitMacros #endif final class PolymorphicEnumEncodableMacroTests: XCTestCase { - #if canImport(KarrotCodableKitMacros) let testMacros: [String: Macro.Type] = [ - "PolymorphicEnumEncodable": PolymorphicEnumEncodableMacro.self, + "PolymorphicEnumEncodable": PolymorphicEnumEncodableMacro.self ] #endif @@ -41,9 +40,7 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Encodable { public func encode(to encoder: any Encoder) throws { switch self { case .callout(let value): @@ -55,9 +52,12 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Encodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -83,9 +83,7 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case callout(DummyCallout) case actionableCallout(DummyActionableCallout) case dismissibleCallout(value: DummyDismissibleCallout) - } - extension CalloutBadge: Encodable { public func encode(to encoder: any Encoder) throws { switch self { case .callout(let value): @@ -97,9 +95,54 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { } } } + + extension CalloutBadge: Encodable { + } + """, + macros: testMacros, + indentationWidth: .spaces(2), + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testPolymorphicEnumEncodableMacroInsideNestedType() throws { + #if canImport(KarrotCodableKitMacros) + // given + assertMacroExpansion( + """ + enum SomeEnum { + @PolymorphicEnumEncodable(identifierCodingKey: "type") + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + } + } + """, + // when + expandedSource: """ + enum SomeEnum { + public enum CalloutBadge { + case callout(DummyCallout) + case actionableCallout(DummyActionableCallout) + + public func encode(to encoder: any Encoder) throws { + switch self { + case .callout(let value): + try value.encode(to: encoder) + case .actionableCallout(let value): + try value.encode(to: encoder) + } + } + } + } + + extension SomeEnum.CalloutBadge: Encodable { + } """, macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -130,11 +173,11 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { DiagnosticSpec( message: "`@PolymorphicEnumEncodable` can only be attached to enums", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -160,16 +203,19 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Encodable { + } """, diagnostics: [ DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -195,16 +241,19 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Encodable { + } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") @@ -230,16 +279,19 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } + + extension CalloutBadge: Encodable { + } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, - column: 1 - ), + column: 1, + ) ], macros: testMacros, - indentationWidth: .spaces(2) + indentationWidth: .spaces(2), ) #else throw XCTSkip("macros are only supported when running tests for the host platform") From 7ed53435414cc11328797d2ffd3ee717d4af3102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elon=20Park=20=28=E1=84=8B=E1=85=A6=E1=86=AF=E1=84=85?= =?UTF-8?q?=E1=85=A9=E1=86=AB=29?= Date: Tue, 7 Apr 2026 18:55:47 +0900 Subject: [PATCH 4/5] docs: fix truncated doc comments and remove unused named declaration - Complete truncated identifierCodingKey doc comments in PolymorphicEnumCodable, PolymorphicEnumDecodable, and PolymorphicEnumEncodable interface files - Remove unused named(PolymorphicMetaCodingKey) from PolymorphicEnumEncodable macro declaration since encode(to:) does not use PolymorphicMetaCodingKey --- .../PolymorphicCodable/Interface/PolymorphicEnumCodable.swift | 2 +- .../Interface/PolymorphicEnumDecodable.swift | 2 +- .../Interface/PolymorphicEnumEncodable.swift | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift index 9f03658..0e348a3 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumCodable.swift @@ -19,7 +19,7 @@ import Foundation /// - Parameters: /// - identifierCodingKey: The key name in the JSON used to store the type identifier. /// The default value for this property is `"type"`. This key is used to identify the specific -/// case of the enum during +/// case of the enum during encoding and decoding. /// - fallbackCaseName: The name of the `case` to use when the type identifier is not found. /// The default value for this property is `nil`. If this property is not provided, the macro will /// throw an error if the type identifier is not found. diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift index 5e0b3cf..2f7cb0b 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumDecodable.swift @@ -18,7 +18,7 @@ import Foundation /// - Parameters: /// - identifierCodingKey: The key name in the JSON used to store the type identifier. /// The default value for this property is `"type"`. This key is used to identify the specific -/// case of the enum during +/// case of the enum during decoding. /// - fallbackCaseName: The name of the `case` to use when the type identifier is not found. /// The default value for this property is `nil`. If this property is not provided, the macro will /// throw an error if the type identifier is not found. diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift index d419b66..d26f09b 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Interface/PolymorphicEnumEncodable.swift @@ -18,8 +18,8 @@ import Foundation /// - Parameters: /// - identifierCodingKey: The key name in the JSON used to store the type identifier. /// The default value for this property is `"type"`. This key is used to identify the specific -/// case of the enum during -@attached(member, names: named(PolymorphicMetaCodingKey), named(encode)) +/// case of the enum during encoding. +@attached(member, names: named(encode)) @attached(extension, conformances: Encodable) public macro PolymorphicEnumEncodable( identifierCodingKey: String = "type" From c97f340e0c5cfc172c29750d0a333874f07efd8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elon=20Park=20=28=E1=84=8B=E1=85=A6=E1=86=AF=E1=84=85?= =?UTF-8?q?=E1=85=A9=E1=86=AB=29?= Date: Tue, 7 Apr 2026 18:55:56 +0900 Subject: [PATCH 5/5] fix: add validation to ExtensionMacro to prevent orphaned conformance Add enum and argument validation to the ExtensionMacro role of PolymorphicEnumCodable/Decodable/Encodable macros. Previously, when the MemberMacro failed validation, the ExtensionMacro could still emit protocol conformance without actual implementations, causing confusing secondary compiler errors. Now both roles validate consistently and throw on invalid input. --- .../PolymorphicEnumCodableMacro.swift | 10 +++- .../PolymorphicEnumDecodableMacro.swift | 10 +++- .../PolymorphicEnumEncodableMacro.swift | 9 ++- .../PolymorphicEnumCodableMacroTests.swift | 57 ++++++++++++------- .../PolymorphicEnumDecodableMacroTests.swift | 57 ++++++++++++------- .../PolymorphicEnumEncodableMacroTests.swift | 37 +++++++----- 6 files changed, 116 insertions(+), 64 deletions(-) diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift index 203431a..d7f79b0 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumCodableMacro.swift @@ -55,16 +55,20 @@ public struct PolymorphicEnumCodableMacro: MemberMacro { extension PolymorphicEnumCodableMacro: ExtensionMacro { public static func expansion( - of _: AttributeSyntax, + of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo _: [TypeSyntax], in _: some MacroExpansionContext, ) throws -> [ExtensionDeclSyntax] { - guard declaration.is(EnumDeclSyntax.self) else { - return [] + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw CodableKitError.message("`@PolymorphicEnumCodable` can only be attached to enums") } + try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) + let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) + try PolymorphicEnumCodableFactory.validateFallbackCaseName(in: node, caseInfos: caseInfos) + return try [ExtensionDeclSyntax("extension \(type.trimmed): Codable {}")] } } diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift index 493b091..79d9340 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumDecodableMacro.swift @@ -49,16 +49,20 @@ public struct PolymorphicEnumDecodableMacro: MemberMacro { extension PolymorphicEnumDecodableMacro: ExtensionMacro { public static func expansion( - of _: AttributeSyntax, + of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo _: [TypeSyntax], in _: some MacroExpansionContext, ) throws -> [ExtensionDeclSyntax] { - guard declaration.is(EnumDeclSyntax.self) else { - return [] + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw CodableKitError.message("`@PolymorphicEnumDecodable` can only be attached to enums") } + try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) + let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) + try PolymorphicEnumCodableFactory.validateFallbackCaseName(in: node, caseInfos: caseInfos) + return try [ExtensionDeclSyntax("extension \(type.trimmed): Decodable {}")] } } diff --git a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift index 393bef0..5ad3948 100644 --- a/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift @@ -38,16 +38,19 @@ public struct PolymorphicEnumEncodableMacro: MemberMacro { extension PolymorphicEnumEncodableMacro: ExtensionMacro { public static func expansion( - of _: AttributeSyntax, + of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax, providingExtensionsOf type: some TypeSyntaxProtocol, conformingTo _: [TypeSyntax], in _: some MacroExpansionContext, ) throws -> [ExtensionDeclSyntax] { - guard declaration.is(EnumDeclSyntax.self) else { - return [] + guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { + throw CodableKitError.message("`@PolymorphicEnumEncodable` can only be attached to enums") } + try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node) + _ = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl) + return try [ExtensionDeclSyntax("extension \(type.trimmed): Encodable {}")] } } diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift index a642219..2d420cb 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumCodableMacroTests.swift @@ -172,7 +172,12 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { message: "`@PolymorphicEnumCodable` can only be attached to enums", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "`@PolymorphicEnumCodable` can only be attached to enums", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -201,16 +206,18 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Codable { - } """, diagnostics: [ DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Invalid polymorphic identifier: expected a non-empty string.", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -239,16 +246,18 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Codable { - } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Polymorphic Enum cases can only have one associated value", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -277,16 +286,18 @@ final class PolymorphicEnumCodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Codable { - } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Polymorphic Enum cases should have one associated value", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -452,16 +463,18 @@ extension PolymorphicEnumCodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Codable { - } """, diagnostics: [ DiagnosticSpec( message: "Missing fallback case: should be defined as `case undefinedCallout", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Missing fallback case: should be defined as `case undefinedCallout", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -490,16 +503,18 @@ extension PolymorphicEnumCodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Codable { - } """, diagnostics: [ DiagnosticSpec( message: "Invalid fallback case name: expected a non-empty string.", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Invalid fallback case name: expected a non-empty string.", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift index 7a34125..163cbb2 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumDecodableMacroTests.swift @@ -150,7 +150,12 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { message: "`@PolymorphicEnumDecodable` can only be attached to enums", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "`@PolymorphicEnumDecodable` can only be attached to enums", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -179,16 +184,18 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Decodable { - } """, diagnostics: [ DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Invalid polymorphic identifier: expected a non-empty string.", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -217,16 +224,18 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Decodable { - } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Polymorphic Enum cases can only have one associated value", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -255,16 +264,18 @@ final class PolymorphicEnumDecodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Decodable { - } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Polymorphic Enum cases should have one associated value", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -408,16 +419,18 @@ extension PolymorphicEnumDecodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Decodable { - } """, diagnostics: [ DiagnosticSpec( message: "Missing fallback case: should be defined as `case undefinedCallout", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Missing fallback case: should be defined as `case undefinedCallout", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -446,16 +459,18 @@ extension PolymorphicEnumDecodableMacroTests { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Decodable { - } """, diagnostics: [ DiagnosticSpec( message: "Invalid fallback case name: expected a non-empty string.", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Invalid fallback case name: expected a non-empty string.", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), diff --git a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift index 1e5c89d..9a905cd 100644 --- a/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift +++ b/Tests/KarrotCodableMacrosTests/PolymorphicEnumCodableMacroTests/PolymorphicEnumEncodableMacroTests.swift @@ -174,7 +174,12 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { message: "`@PolymorphicEnumEncodable` can only be attached to enums", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "`@PolymorphicEnumEncodable` can only be attached to enums", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -203,16 +208,18 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Encodable { - } """, diagnostics: [ DiagnosticSpec( message: "Invalid polymorphic identifier: expected a non-empty string.", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Invalid polymorphic identifier: expected a non-empty string.", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -241,16 +248,18 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Encodable { - } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases can only have one associated value", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Polymorphic Enum cases can only have one associated value", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2), @@ -279,16 +288,18 @@ final class PolymorphicEnumEncodableMacroTests: XCTestCase { case actionableCallout(DummyActionableCallout) case dismissibleCallout(DummyDismissibleCallout) } - - extension CalloutBadge: Encodable { - } """, diagnostics: [ DiagnosticSpec( message: "Polymorphic Enum cases should have one associated value", line: 1, column: 1, - ) + ), + DiagnosticSpec( + message: "Polymorphic Enum cases should have one associated value", + line: 1, + column: 1, + ), ], macros: testMacros, indentationWidth: .spaces(2),