Skip to content

Commit

Permalink
Merge pull request #5 from WendellXY/add-codable-options-support
Browse files Browse the repository at this point in the history
Add `skipSuperCoding` Codable Options
  • Loading branch information
WendellXY authored Jan 13, 2025
2 parents 8d858f6 + d14ca19 commit 3d5b379
Show file tree
Hide file tree
Showing 10 changed files with 397 additions and 13 deletions.
12 changes: 9 additions & 3 deletions Sources/CodableKit/CodableKit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@
/// ```
@attached(extension, conformances: Codable, names: named(CodingKeys), named(init(from:)))
@attached(member, conformances: Codable, names: named(init(from:)), named(encode(to:)))
public macro Codable() = #externalMacro(module: "CodableKitMacros", type: "CodableMacro")
public macro Codable(
options: CodableOptions = .default
) = #externalMacro(module: "CodableKitMacros", type: "CodableMacro")

/// A macro that generates `Decodable` conformance and boilerplate code for a struct, such that the Decodable struct can
/// have default values for its properties, and custom keys for encoding and decoding with `@CodableKey`.
Expand Down Expand Up @@ -96,7 +98,9 @@ public macro Codable() = #externalMacro(module: "CodableKitMacros", type: "Codab
/// ```
@attached(extension, conformances: Decodable, names: named(CodingKeys), named(init(from:)))
@attached(member, conformances: Decodable, names: named(init(from:)))
public macro Decodable() = #externalMacro(module: "CodableKitMacros", type: "CodableMacro")
public macro Decodable(
options: CodableOptions = .default
) = #externalMacro(module: "CodableKitMacros", type: "CodableMacro")

/// A macro that generates `Encodable` conformance and boilerplate code for a struct, such that the Encodable struct can
/// have default values for its properties, and custom keys for encoding and decoding with `@CodableKey`.
Expand Down Expand Up @@ -136,7 +140,9 @@ public macro Decodable() = #externalMacro(module: "CodableKitMacros", type: "Cod
/// ```
@attached(extension, conformances: Encodable, names: named(CodingKeys))
@attached(member, conformances: Encodable, names: named(encode(to:)))
public macro Encodable() = #externalMacro(module: "CodableKitMacros", type: "CodableMacro")
public macro Encodable(
options: CodableOptions = .default
) = #externalMacro(module: "CodableKitMacros", type: "CodableMacro")

/// Custom the key used for encoding and decoding a property.
///
Expand Down
53 changes: 44 additions & 9 deletions Sources/CodableKitMacros/CodableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by Wendell on 3/30/24.
//

import CodableKitShared
import Foundation
import SwiftDiagnostics
import SwiftSyntax
Expand All @@ -25,18 +26,22 @@ extension CodableMacro: ExtensionMacro {
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
try core.prepareCodeGeneration(for: declaration, in: context, conformingTo: protocols)
try core.prepareCodeGeneration(of: node, for: declaration, in: context, conformingTo: protocols)

let properties = try core.properties(for: declaration, in: context)
let accessModifier = try core.accessModifier(for: declaration, in: context)
let structureType = try core.accessStructureType(for: declaration, in: context)
let codableType = try core.accessCodableType(for: declaration, in: context)
let codableOptions = try core.accessCodableOptions(for: declaration, in: context)

// If there are no properties, return an empty array.
guard !properties.isEmpty else { return [] }

let inheritanceClause: InheritanceClauseSyntax? =
if case .classType(let hasSuperclass) = structureType, hasSuperclass {
if case .classType(let hasSuperclass) = structureType,
hasSuperclass,
!codableOptions.contains(.skipSuperCoding)
{
nil
} else {
InheritanceClauseSyntax {
Expand All @@ -62,7 +67,14 @@ extension CodableMacro: ExtensionMacro {
) {
genCodingKeyEnumDecl(from: properties)
if codableType.contains(.decodable) {
DeclSyntax(genInitDecoderDecl(from: properties, modifiers: [accessModifier], hasSuper: false))
DeclSyntax(
genInitDecoderDecl(
from: properties,
modifiers: [accessModifier],
codableOptions: codableOptions,
hasSuper: false
)
)
}
}
]
Expand All @@ -87,12 +99,13 @@ extension CodableMacro: MemberMacro {
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
try core.prepareCodeGeneration(for: declaration, in: context, conformingTo: protocols)
try core.prepareCodeGeneration(of: node, for: declaration, in: context, conformingTo: protocols)

let properties = try core.properties(for: declaration, in: context)
let accessModifier = try core.accessModifier(for: declaration, in: context)
let structureType = try core.accessStructureType(for: declaration, in: context)
let codableType = try core.accessCodableType(for: declaration, in: context)
let codableOptions = try core.accessCodableOptions(for: declaration, in: context)

// If there are no properties, return an empty array.
guard !properties.isEmpty else { return [] }
Expand All @@ -108,7 +121,9 @@ extension CodableMacro: MemberMacro {
case let .classType(hasSuperclass):
decodeModifiers.append(.init(name: .keyword(.required)))
if hasSuperclass {
encodeModifiers.append(.init(name: .keyword(.override)))
if !codableOptions.contains(.skipSuperCoding) {
encodeModifiers.append(.init(name: .keyword(.override)))
}
hasSuper = true
}
case .structType, .enumType:
Expand All @@ -121,14 +136,28 @@ extension CodableMacro: MemberMacro {
case .classType:
if codableType.contains(.decodable) {
result.append(
DeclSyntax(genInitDecoderDecl(from: properties, modifiers: decodeModifiers, hasSuper: hasSuper))
DeclSyntax(
genInitDecoderDecl(
from: properties,
modifiers: decodeModifiers,
codableOptions: codableOptions,
hasSuper: hasSuper
)
)
)
}
fallthrough
case .structType:
if codableType.contains(.encodable) {
result.append(
DeclSyntax(genEncodeFuncDecl(from: properties, modifiers: encodeModifiers, hasSuper: hasSuper))
DeclSyntax(
genEncodeFuncDecl(
from: properties,
modifiers: encodeModifiers,
codableOptions: codableOptions,
hasSuper: hasSuper
)
)
)
}
case .enumType:
Expand Down Expand Up @@ -170,6 +199,7 @@ extension CodableMacro {
fileprivate static func genInitDecoderDecl(
from properties: [Property],
modifiers: [DeclModifierSyntax],
codableOptions: CodableOptions,
hasSuper: Bool
) -> InitializerDeclSyntax {
InitializerDeclSyntax(
Expand Down Expand Up @@ -231,7 +261,11 @@ extension CodableMacro {
}

if hasSuper {
"try super.init(from: decoder)"
if codableOptions.contains(.skipSuperCoding) {
"super.init()"
} else {
"try super.init(from: decoder)"
}
}
}
}
Expand All @@ -240,6 +274,7 @@ extension CodableMacro {
fileprivate static func genEncodeFuncDecl(
from properties: [Property],
modifiers: [DeclModifierSyntax],
codableOptions: CodableOptions,
hasSuper: Bool
) -> FunctionDeclSyntax {
FunctionDeclSyntax(
Expand Down Expand Up @@ -292,7 +327,7 @@ extension CodableMacro {
)
}

if hasSuper {
if hasSuper, !codableOptions.contains(.skipSuperCoding) {
"try super.encode(to: encoder)"
}
}
Expand Down
28 changes: 27 additions & 1 deletion Sources/CodableKitMacros/CodeGenCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// Copyright © 2024 WendellXY. All rights reserved.
//

import CodableKitShared
import Foundation
import SwiftDiagnostics
import SwiftSyntax
Expand Down Expand Up @@ -33,6 +34,7 @@ internal final class CodeGenCore: @unchecked Sendable {
private var accessModifiers: [MacroContextKey: DeclModifierSyntax] = [:]
private var structureTypes: [MacroContextKey: StructureType] = [:]
private var codableTypes: [MacroContextKey: CodableType] = [:]
private var codableOptions: [MacroContextKey: CodableOptions] = [:]

func key(for declaration: some SyntaxProtocol, in context: some MacroExpansionContext) -> MacroContextKey {
let location = context.location(of: declaration)
Expand All @@ -47,7 +49,10 @@ internal final class CodeGenCore: @unchecked Sendable {
}

extension CodeGenCore {
func properties(for declaration: some SyntaxProtocol, in context: some MacroExpansionContext) throws -> [Property] {
func properties(
for declaration: some SyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [Property] {
properties[key(for: declaration, in: context)] ?? []
}

Expand Down Expand Up @@ -95,6 +100,21 @@ extension CodeGenCore {
severity: .error
)
}

func accessCodableOptions(
for declaration: some SyntaxProtocol,
in context: some MacroExpansionContext
) throws -> CodableOptions {
if let codableOptions = codableOptions[key(for: declaration, in: context)] {
return codableOptions
}

throw SimpleDiagnosticMessage(
message: "Codable options for declaration not found",
diagnosticID: messageID,
severity: .error
)
}
}

// MARK: - Property Extraction
Expand Down Expand Up @@ -216,6 +236,7 @@ extension CodeGenCore {

/// Prepare the code generation by extracting properties and access modifier.
func prepareCodeGeneration(
of node: AttributeSyntax,
for declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext,
conformingTo protocols: [TypeSyntax] = []
Expand All @@ -237,6 +258,11 @@ extension CodeGenCore {
preparedDeclarations.insert(id)
}

codableOptions[id] = node.arguments?
.as(LabeledExprListSyntax.self)?
.first(where: { $0.label?.text == "options" })?
.parseCodableOptions() ?? .default

// Check if properties and access modifier are already prepared

if accessModifiers[id] == nil {
Expand Down
67 changes: 67 additions & 0 deletions Sources/CodableKitShared/CodableOptions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// CodableOptions.swift
// CodableKit
//
// Created by Wendell Wang on 2025/1/13.
//

import SwiftSyntax

/// Options that customize the behavior of the `@Codable` macro expansion.
public struct CodableOptions: OptionSet, Sendable {
public let rawValue: Int32

public init(rawValue: Int32) {
self.rawValue = rawValue
}

/// The default options, which perform standard Codable expansion with super encode/decode calls.
public static let `default`: Self = []

/// Skips generating super encode/decode calls in the expanded code.
///
/// Use this option when the superclass doesn't conform to `Codable`.
/// When enabled:
/// - Replaces `super.init(from: decoder)` with `super.init()`
/// - Removes `super.encode(to: encoder)` call entirely
public static let skipSuperCoding = Self(rawValue: 1 << 0)
}

extension CodableOptions {
package init(from expr: MemberAccessExprSyntax) {
let variableName = expr.declName.baseName.text
switch variableName {
case "skipSuperCoding":
self = .skipSuperCoding
default:
self = .default
}
}
}

extension CodableOptions {
/// Parse the options from 1a `LabelExprSyntax`. It support parse a single element like `.default`,
/// or multiple elements like `[.ignored, .explicitNil]`
package static func parse(from labeledExpr: LabeledExprSyntax) -> Self {
if let memberAccessExpr = labeledExpr.expression.as(MemberAccessExprSyntax.self) {
Self.init(from: memberAccessExpr)
} else if let arrayExpr = labeledExpr.expression.as(ArrayExprSyntax.self) {
arrayExpr.elements
.compactMap { $0.expression.as(MemberAccessExprSyntax.self) }
.map { Self.init(from: $0) }
.reduce(.default) { $0.union($1) }
} else {
.default
}
}
}

extension LabeledExprSyntax {
/// Parse the options from a `LabelExprSyntax`. It support parse a single element like .default,
/// or multiple elements like [.ignored, .explicitNil].
///
/// This is a convenience method to use for chaining.
package func parseCodableOptions() -> CodableOptions {
CodableOptions.parse(from: self)
}
}
47 changes: 47 additions & 0 deletions Tests/CodableKitTests/CodableMacroTests+class+inheritance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -736,4 +736,51 @@ final class CodableKitTestsForSubClass: XCTestCase {
)

}

func testMacrosWithCodableOptionSkipSuperCoding() throws {

assertMacroExpansion(
"""
@Codable(options: .skipSuperCoding)
public class User: NSObject {
let id: UUID
let name: String
let age: Int
}
""",
expandedSource: """
public class User: NSObject {
let id: UUID
let name: String
let age: Int
public required init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
super.init()
}
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(age, forKey: .age)
}
}
extension User: Codable {
enum CodingKeys: String, CodingKey {
case id
case name
case age
}
}
""",
macroSpecs: macroSpecs,
indentationWidth: .spaces(2)
)

}
}
Loading

0 comments on commit 3d5b379

Please sign in to comment.