Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
///
/// - 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")
Original file line number Diff line number Diff line change
Expand Up @@ -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 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.
@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")
Original file line number Diff line number Diff line change
Expand Up @@ -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 encoding.
@attached(member, names: named(encode))
@attached(extension, conformances: Encodable)
public macro PolymorphicEnumEncodable(
identifierCodingKey: String = "type"
) = #externalMacro(module: "KarrotCodableKitMacros", type: "PolymorphicEnumEncodableMacro")
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -45,24 +37,38 @@ 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)",
]
}
}

extension PolymorphicEnumCodableMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo _: [TypeSyntax],
in _: some MacroExpansionContext,
) throws -> [ExtensionDeclSyntax] {
guard let enumDecl = declaration.as(EnumDeclSyntax.self) else {
throw CodableKitError.message("`@PolymorphicEnumCodable` can only be attached to enums")
}

\(raw: initFromDecoderSyntax)
try PolymorphicEnumCodableFactory.validateIdentifierCodingKey(in: node)
let caseInfos = try PolymorphicEnumCodableFactory.extractCaseInfos(from: enumDecl)
try PolymorphicEnumCodableFactory.validateFallbackCaseName(in: node, caseInfos: caseInfos)

\(raw: encodeToEncoderSyntax)
"""
},
]
return try [ExtensionDeclSyntax("extension \(type.trimmed): Codable {}")]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -47,17 +37,32 @@ 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 node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo _: [TypeSyntax],
in _: some MacroExpansionContext,
) throws -> [ExtensionDeclSyntax] {
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 {}")]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,47 @@ 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)"
]
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

extension PolymorphicEnumEncodableMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo _: [TypeSyntax],
in _: some MacroExpansionContext,
) throws -> [ExtensionDeclSyntax] {
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 {}")]
}
Comment on lines +39 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

[MEDIUM] Both macro roles duplicate validation, causing duplicate diagnostics.

The MemberMacro and ExtensionMacro expansions independently perform the same validations (enum check, validateIdentifierCodingKey, extractCaseInfos). When validation fails, both roles throw, resulting in duplicate error messages shown to the user.

This is consistent with commit message noting "added validation to the ExtensionMacro role to prevent emitting orphaned protocol conformances when MemberMacro validation fails." However, consider extracting validation into a shared helper that caches results, or accept the duplicate diagnostics as an intentional design trade-off for preventing orphaned extensions.

The tests explicitly expect duplicate DiagnosticSpec entries, so this appears intentional—just flagging for awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/KarrotCodableKitMacros/PolymorphicEnumCodableMacros/PolymorphicEnumEncodableMacro.swift`
around lines 39 - 55, Extract the duplicate validation into a shared helper
(e.g., PolymorphicEnumCodableFactory.validateAttachment(...) or
validatePolymorphicEnumAttachment(...)) that performs the enum check, calls
PolymorphicEnumCodableFactory.validateIdentifierCodingKey and
PolymorphicEnumCodableFactory.extractCaseInfos and returns the validated
EnumDeclSyntax (or cached result); then replace the current validation calls in
PolymorphicEnumEncodableMacro.expansion and the MemberMacro expansion with a
single call to that helper so diagnostics are emitted once and both roles still
prevent orphaned conformances.

}
Loading
Loading