Skip to content

Commit

Permalink
feat: added non-variadic generics support (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Nov 3, 2023
1 parent 5344f13 commit b615251
Show file tree
Hide file tree
Showing 12 changed files with 530 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ extension RegistrationAttribute {
!self.diagnoser().produce(for: declaration, in: context)
else { return nil }

let options = Registrar.Options(modifiers: declaration.modifiers)
let options = Registrar.Options(decl: declaration)
var registrar = Registrar(options: options)

declaration.memberBlock.members.forEach { member in
Expand Down
127 changes: 127 additions & 0 deletions Sources/CodableMacroPlugin/Registration/ConstraintGenerator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
@_implementationOnly import SwiftSyntax

extension Registrar.Options {
/// A where clause generator for `Codable` conformance.
///
/// This generator keeps track of generic type arguments,
/// and generates where clause based on whether these
/// types need to conform to `Decodable` or `Encodable`
/// for `Codable` conformance.
struct ConstraintGenerator {
/// List of generic type arguments.
///
/// Contains all the type requirement arguments
/// of generic declaration.
var typeArguments: [TokenSyntax] = []

/// Creates a new generator with provided declaration group.
///
/// - Parameters:
/// - decl: The declaration group generator picks
/// generic type arguments from.
///
/// - Returns: The newly created generator.
init(decl: DeclGroupSyntax) {
guard
let decl = decl as? GenericTypeDeclSyntax,
let paramClause = decl.genericParameterClause
else { return }

typeArguments.reserveCapacity(paramClause.parameters.count)
for param in paramClause.parameters {
typeArguments.append(param.name.trimmed)
}
}

/// Provides where clause for the `Codable` extension declaration.
///
/// The where clause contains conformance requirement for generic
/// arguments necessary for `Codable` conformance.
///
/// - Parameters:
/// - path: The requirement check path to be used for `Variable`.
/// - variables: List of all the variables registered in `Registrar`.
/// - protocol: The`Codable` protocol type syntax.
///
/// - Returns: The generated where clause.
@inlinable
func codingClause(
forRequirementPath path: KeyPath<any Variable, Bool?>,
withVariables variables: [any Variable],
conformingTo protocol: TypeSyntax
) -> GenericWhereClauseSyntax? {
let allTypes = variables.filter { $0[keyPath: path] ?? true }
.map(\.type.trimmed.description)
let typeArguments = self.typeArguments.filter { type in
return allTypes.contains(type.description)
}
guard !typeArguments.isEmpty else { return nil }
return GenericWhereClauseSyntax {
for argument in typeArguments {
GenericRequirementSyntax(
requirement: .conformanceRequirement(
.init(
leftType: IdentifierTypeSyntax(name: argument),
rightType: `protocol`
)
)
)
}
}
}

/// Provides where clause for the `Decodable` extension declaration.
///
/// The where clause contains conformance requirement for generic
/// arguments necessary for `Decodable` conformance.
///
/// - Parameters:
/// - variables: List of all the variables registered in `Registrar`.
/// - protocol: The`Decodable` protocol type syntax.
///
/// - Returns: The generated where clause.
func decodingClause(
withVariables variables: [any Variable],
conformingTo protocol: TypeSyntax
) -> GenericWhereClauseSyntax? {
return codingClause(
forRequirementPath: \.requireDecodable,
withVariables: variables, conformingTo: `protocol`
)
}

/// Provides where clause for the `Encodable` extension declaration.
///
/// The where clause contains conformance requirement for generic
/// arguments necessary for `Encodable` conformance.
///
/// - Parameters:
/// - variables: List of all the variables registered in `Registrar`.
/// - protocol: The`Encodable` protocol type syntax.
///
/// - Returns: The generated where clause.
func encodingClause(
withVariables variables: [any Variable],
conformingTo protocol: TypeSyntax
) -> GenericWhereClauseSyntax? {
return codingClause(
forRequirementPath: \.requireEncodable,
withVariables: variables, conformingTo: `protocol`
)
}
}
}

/// A declaration group syntax type that accepts generic parameter clause.
///
/// This type has optional `GenericParameterClauseSyntax` that
/// can be used for where clause generation for `Codable` conformance.
protocol GenericTypeDeclSyntax: DeclGroupSyntax {
/// A where clause that places additional constraints on generic parameters
/// like `where Element: Hashable`.
var genericParameterClause: GenericParameterClauseSyntax? { get }
}

extension StructDeclSyntax: GenericTypeDeclSyntax {}
extension ClassDeclSyntax: GenericTypeDeclSyntax {}
extension EnumDeclSyntax: GenericTypeDeclSyntax {}
28 changes: 19 additions & 9 deletions Sources/CodableMacroPlugin/Registration/Registrar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ struct Registrar {
/// The default list of modifiers to be applied to generated
/// conformance implementation declarations.
fileprivate let modifiers: DeclModifierListSyntax
/// The where clause generator for generic type arguments.
fileprivate let constraintGenerator: ConstraintGenerator

/// Memberwise initialization generator with provided options.
///
Expand All @@ -27,15 +29,15 @@ struct Registrar {
return .init(options: .init(modifiers: modifiers))
}

/// Creates a new options instance with provided parameters.
/// Creates a new options instance with provided declaration group.
///
/// - Parameters:
/// - modifiers: List of modifiers need to be applied
/// to generated declarations.
/// - decl: The declaration group options will be applied to.
///
/// - Returns: The newly created options.
init(modifiers: DeclModifierListSyntax = []) {
self.modifiers = modifiers
init(decl: DeclGroupSyntax) {
self.modifiers = decl.modifiers
self.constraintGenerator = .init(decl: decl)
}
}

Expand Down Expand Up @@ -176,12 +178,16 @@ struct Registrar {
/// - Returns: The generated extension declaration.
func decoding(
type: some TypeSyntaxProtocol,
conformingTo protocol: TypeSyntax = "Decodable",
conformingTo protocol: TypeSyntax,
in context: some MacroExpansionContext
) -> ExtensionDeclSyntax {
return .init(
extendedType: type,
inheritanceClause: .init { .init(type: `protocol`) }
inheritanceClause: .init { .init(type: `protocol`) },
genericWhereClause: options.constraintGenerator.decodingClause(
withVariables: root.linkedVariables,
conformingTo: `protocol`
)
) {
InitializerDeclSyntax.decode(
modifiers: options.modifiers
Expand All @@ -206,12 +212,16 @@ struct Registrar {
/// - Returns: The generated extension declaration.
func encoding(
type: some TypeSyntaxProtocol,
conformingTo protocol: TypeSyntax = "Encodable",
conformingTo protocol: TypeSyntax,
in context: some MacroExpansionContext
) -> ExtensionDeclSyntax {
return .init(
extendedType: type,
inheritanceClause: .init { .init(type: `protocol`) }
inheritanceClause: .init { .init(type: `protocol`) },
genericWhereClause: options.constraintGenerator.encodingClause(
withVariables: root.linkedVariables,
conformingTo: `protocol`
)
) {
FunctionDeclSyntax.encode(modifiers: options.modifiers) { encoder in
let type = caseMap.type
Expand Down
11 changes: 11 additions & 0 deletions Sources/CodableMacroPlugin/Variables/AnyVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,17 @@ struct AnyVariable<Initialization: VariableInitialization>: Variable {
/// is to be encoded.
var encode: Bool? { base.encode }

/// Whether the variable type requires `Decodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Decodable` conformance.
var requireDecodable: Bool? { base.requireDecodable }
/// Whether the variable type requires `Encodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Encodable` conformance.
var requireEncodable: Bool? { base.requireEncodable }

/// Wraps the provided variable erasing its type and
/// initialization type.
///
Expand Down
15 changes: 15 additions & 0 deletions Sources/CodableMacroPlugin/Variables/BasicVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ struct BasicVariable: BasicCodingVariable {
/// during initialization.
let encode: Bool?

/// Whether the variable type requires
/// `Decodable` conformance.
///
/// By default set as `nil`, unless
/// `decode` is set explicitly during
/// initialization.
var requireDecodable: Bool? { self.decode }
/// Whether the variable type requires
/// `Encodable` conformance.
///
/// By default set as `nil`, unless
/// `encode` is set explicitly during
/// initialization.
var requireEncodable: Bool? { self.encode }

/// Creates a new variable with provided data.
///
/// Basic implementation for this variable provided
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,23 @@ struct ConditionalCodingVariable<Var: Variable>: ComposedVariable {
/// Provides whether underlying variable value is to be encoded,
/// if provided encode option is set as `true` otherwise `false`.
var encode: Bool? { options.encode ? base.encode : false }

/// Whether the variable type requires `Decodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Decodable` conformance, if provided decode
/// option is set as `true` otherwise `false`.
var requireDecodable: Bool? {
return options.decode ? base.requireDecodable : false
}
/// Whether the variable type requires `Encodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Encodable` conformance, if provided encode
/// option is set as `true` otherwise `false`.
var requireEncodable: Bool? {
return options.encode ? base.requireEncodable : false
}
}

extension ConditionalCodingVariable: DefaultOptionComposedVariable {
Expand Down
11 changes: 11 additions & 0 deletions Sources/CodableMacroPlugin/Variables/DefaultValueVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,17 @@ where Var.Initialization == RequiredInitialization {
/// Always `true` for this type.
var encode: Bool? { true }

/// Whether the variable type requires `Decodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Decodable` conformance.
var requireDecodable: Bool? { base.requireDecodable }
/// Whether the variable type requires `Encodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Encodable` conformance.
var requireEncodable: Bool? { base.requireEncodable }

/// Indicates the initialization type for this variable.
///
/// Provides default initialization value in initialization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ struct HelperCodedVariable<Var: BasicCodingVariable>: ComposedVariable {
/// Always `true` for this type.
var encode: Bool? { true }

/// Whether the variable type requires `Decodable` conformance.
///
/// Always `false` for this type.
var requireDecodable: Bool? { false }
/// Whether the variable type requires `Encodable` conformance.
///
/// Always `false` for this type.
var requireEncodable: Bool? { false }

/// Provides the code syntax for encoding this variable
/// at the provided location.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,25 @@ where Wrapped.Initialization: RequiredVariableInitialization {
/// Depends on whether variable is initializable if underlying variable doesn't
/// specify explicit encoding. Otherwise depends on whether underlying variable
/// is to be decoded.
var encode: Bool? { base.encode == nil ? options.`init` : base.encode! }
var encode: Bool? { base.encode ?? options.`init` }

/// Whether the variable type requires `Decodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Decodable` conformance, if variable can be
/// initialized otherwise `false`.
var requireDecodable: Bool? {
return options.`init` ? base.requireDecodable : false
}
/// Whether the variable type requires `Encodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Encodable` conformance, if underlying variable
/// specifies explicit encoding. Otherwise depends on
/// whether underlying variable is to be decoded.
var requireEncodable: Bool? {
return base.requireEncodable ?? options.`init`
}

/// Indicates the initialization type for this variable.
///
Expand Down
19 changes: 19 additions & 0 deletions Sources/CodableMacroPlugin/Variables/KeyedVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,25 @@ struct KeyedVariable<Var: Variable>: ComposedVariable {
/// Provides whether underlying variable value is to be encoded,
/// if provided code option is set as `false` otherwise `true`.
var encode: Bool? { options.code ? true : base.encode }

/// Whether the variable type requires `Decodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Decodable` conformance and provided code option
/// is set as `true`. Otherwise depends on whether
/// underlying variable is to be decoded.
var requireDecodable: Bool? {
return options.code ? base.requireDecodable : base.decode
}
/// Whether the variable type requires `Encodable` conformance.
///
/// Provides whether underlying variable type requires
/// `Encodable` conformance and provided code option
/// is set as `true`. Otherwise depends on whether
/// underlying variable is to be encoded.
var requireEncodable: Bool? {
return options.code ? base.requireEncodable : base.encode
}
}

extension KeyedVariable: BasicCodingVariable
Expand Down
19 changes: 19 additions & 0 deletions Sources/CodableMacroPlugin/Variables/Variable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,25 @@ protocol Variable<Initialization> {
/// is encoded by default.
var encode: Bool? { get }

/// Whether the variable type requires
/// `Decodable` conformance.
///
/// Used for generic where clause, for
/// `Decodable` conformance generation.
///
/// If `nil` is returned, variable is used in
/// generic where clause by default.
var requireDecodable: Bool? { get }
/// Whether the variable type requires
/// `Encodable` conformance.
///
/// Used for generic where clause, for
/// `Encodable` conformance generation.
///
/// If `nil` is returned, variable is used in
/// generic where clause by default.
var requireEncodable: Bool? { get }

/// Indicates the initialization type for this variable.
///
/// Indicates whether initialization is required, optional
Expand Down

0 comments on commit b615251

Please sign in to comment.