Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add skipSuperCoding Codable Options #5

Merged
merged 3 commits into from
Jan 13, 2025
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
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