Skip to content

Commit b615251

Browse files
authored
feat: added non-variadic generics support (#23)
1 parent 5344f13 commit b615251

12 files changed

+530
-11
lines changed

Sources/CodableMacroPlugin/Attributes/RegistrationAttribute.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ extension RegistrationAttribute {
3434
!self.diagnoser().produce(for: declaration, in: context)
3535
else { return nil }
3636

37-
let options = Registrar.Options(modifiers: declaration.modifiers)
37+
let options = Registrar.Options(decl: declaration)
3838
var registrar = Registrar(options: options)
3939

4040
declaration.memberBlock.members.forEach { member in
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
@_implementationOnly import SwiftSyntax
2+
3+
extension Registrar.Options {
4+
/// A where clause generator for `Codable` conformance.
5+
///
6+
/// This generator keeps track of generic type arguments,
7+
/// and generates where clause based on whether these
8+
/// types need to conform to `Decodable` or `Encodable`
9+
/// for `Codable` conformance.
10+
struct ConstraintGenerator {
11+
/// List of generic type arguments.
12+
///
13+
/// Contains all the type requirement arguments
14+
/// of generic declaration.
15+
var typeArguments: [TokenSyntax] = []
16+
17+
/// Creates a new generator with provided declaration group.
18+
///
19+
/// - Parameters:
20+
/// - decl: The declaration group generator picks
21+
/// generic type arguments from.
22+
///
23+
/// - Returns: The newly created generator.
24+
init(decl: DeclGroupSyntax) {
25+
guard
26+
let decl = decl as? GenericTypeDeclSyntax,
27+
let paramClause = decl.genericParameterClause
28+
else { return }
29+
30+
typeArguments.reserveCapacity(paramClause.parameters.count)
31+
for param in paramClause.parameters {
32+
typeArguments.append(param.name.trimmed)
33+
}
34+
}
35+
36+
/// Provides where clause for the `Codable` extension declaration.
37+
///
38+
/// The where clause contains conformance requirement for generic
39+
/// arguments necessary for `Codable` conformance.
40+
///
41+
/// - Parameters:
42+
/// - path: The requirement check path to be used for `Variable`.
43+
/// - variables: List of all the variables registered in `Registrar`.
44+
/// - protocol: The`Codable` protocol type syntax.
45+
///
46+
/// - Returns: The generated where clause.
47+
@inlinable
48+
func codingClause(
49+
forRequirementPath path: KeyPath<any Variable, Bool?>,
50+
withVariables variables: [any Variable],
51+
conformingTo protocol: TypeSyntax
52+
) -> GenericWhereClauseSyntax? {
53+
let allTypes = variables.filter { $0[keyPath: path] ?? true }
54+
.map(\.type.trimmed.description)
55+
let typeArguments = self.typeArguments.filter { type in
56+
return allTypes.contains(type.description)
57+
}
58+
guard !typeArguments.isEmpty else { return nil }
59+
return GenericWhereClauseSyntax {
60+
for argument in typeArguments {
61+
GenericRequirementSyntax(
62+
requirement: .conformanceRequirement(
63+
.init(
64+
leftType: IdentifierTypeSyntax(name: argument),
65+
rightType: `protocol`
66+
)
67+
)
68+
)
69+
}
70+
}
71+
}
72+
73+
/// Provides where clause for the `Decodable` extension declaration.
74+
///
75+
/// The where clause contains conformance requirement for generic
76+
/// arguments necessary for `Decodable` conformance.
77+
///
78+
/// - Parameters:
79+
/// - variables: List of all the variables registered in `Registrar`.
80+
/// - protocol: The`Decodable` protocol type syntax.
81+
///
82+
/// - Returns: The generated where clause.
83+
func decodingClause(
84+
withVariables variables: [any Variable],
85+
conformingTo protocol: TypeSyntax
86+
) -> GenericWhereClauseSyntax? {
87+
return codingClause(
88+
forRequirementPath: \.requireDecodable,
89+
withVariables: variables, conformingTo: `protocol`
90+
)
91+
}
92+
93+
/// Provides where clause for the `Encodable` extension declaration.
94+
///
95+
/// The where clause contains conformance requirement for generic
96+
/// arguments necessary for `Encodable` conformance.
97+
///
98+
/// - Parameters:
99+
/// - variables: List of all the variables registered in `Registrar`.
100+
/// - protocol: The`Encodable` protocol type syntax.
101+
///
102+
/// - Returns: The generated where clause.
103+
func encodingClause(
104+
withVariables variables: [any Variable],
105+
conformingTo protocol: TypeSyntax
106+
) -> GenericWhereClauseSyntax? {
107+
return codingClause(
108+
forRequirementPath: \.requireEncodable,
109+
withVariables: variables, conformingTo: `protocol`
110+
)
111+
}
112+
}
113+
}
114+
115+
/// A declaration group syntax type that accepts generic parameter clause.
116+
///
117+
/// This type has optional `GenericParameterClauseSyntax` that
118+
/// can be used for where clause generation for `Codable` conformance.
119+
protocol GenericTypeDeclSyntax: DeclGroupSyntax {
120+
/// A where clause that places additional constraints on generic parameters
121+
/// like `where Element: Hashable`.
122+
var genericParameterClause: GenericParameterClauseSyntax? { get }
123+
}
124+
125+
extension StructDeclSyntax: GenericTypeDeclSyntax {}
126+
extension ClassDeclSyntax: GenericTypeDeclSyntax {}
127+
extension EnumDeclSyntax: GenericTypeDeclSyntax {}

Sources/CodableMacroPlugin/Registration/Registrar.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ struct Registrar {
1818
/// The default list of modifiers to be applied to generated
1919
/// conformance implementation declarations.
2020
fileprivate let modifiers: DeclModifierListSyntax
21+
/// The where clause generator for generic type arguments.
22+
fileprivate let constraintGenerator: ConstraintGenerator
2123

2224
/// Memberwise initialization generator with provided options.
2325
///
@@ -27,15 +29,15 @@ struct Registrar {
2729
return .init(options: .init(modifiers: modifiers))
2830
}
2931

30-
/// Creates a new options instance with provided parameters.
32+
/// Creates a new options instance with provided declaration group.
3133
///
3234
/// - Parameters:
33-
/// - modifiers: List of modifiers need to be applied
34-
/// to generated declarations.
35+
/// - decl: The declaration group options will be applied to.
3536
///
3637
/// - Returns: The newly created options.
37-
init(modifiers: DeclModifierListSyntax = []) {
38-
self.modifiers = modifiers
38+
init(decl: DeclGroupSyntax) {
39+
self.modifiers = decl.modifiers
40+
self.constraintGenerator = .init(decl: decl)
3941
}
4042
}
4143

@@ -176,12 +178,16 @@ struct Registrar {
176178
/// - Returns: The generated extension declaration.
177179
func decoding(
178180
type: some TypeSyntaxProtocol,
179-
conformingTo protocol: TypeSyntax = "Decodable",
181+
conformingTo protocol: TypeSyntax,
180182
in context: some MacroExpansionContext
181183
) -> ExtensionDeclSyntax {
182184
return .init(
183185
extendedType: type,
184-
inheritanceClause: .init { .init(type: `protocol`) }
186+
inheritanceClause: .init { .init(type: `protocol`) },
187+
genericWhereClause: options.constraintGenerator.decodingClause(
188+
withVariables: root.linkedVariables,
189+
conformingTo: `protocol`
190+
)
185191
) {
186192
InitializerDeclSyntax.decode(
187193
modifiers: options.modifiers
@@ -206,12 +212,16 @@ struct Registrar {
206212
/// - Returns: The generated extension declaration.
207213
func encoding(
208214
type: some TypeSyntaxProtocol,
209-
conformingTo protocol: TypeSyntax = "Encodable",
215+
conformingTo protocol: TypeSyntax,
210216
in context: some MacroExpansionContext
211217
) -> ExtensionDeclSyntax {
212218
return .init(
213219
extendedType: type,
214-
inheritanceClause: .init { .init(type: `protocol`) }
220+
inheritanceClause: .init { .init(type: `protocol`) },
221+
genericWhereClause: options.constraintGenerator.encodingClause(
222+
withVariables: root.linkedVariables,
223+
conformingTo: `protocol`
224+
)
215225
) {
216226
FunctionDeclSyntax.encode(modifiers: options.modifiers) { encoder in
217227
let type = caseMap.type

Sources/CodableMacroPlugin/Variables/AnyVariable.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ struct AnyVariable<Initialization: VariableInitialization>: Variable {
3838
/// is to be encoded.
3939
var encode: Bool? { base.encode }
4040

41+
/// Whether the variable type requires `Decodable` conformance.
42+
///
43+
/// Provides whether underlying variable type requires
44+
/// `Decodable` conformance.
45+
var requireDecodable: Bool? { base.requireDecodable }
46+
/// Whether the variable type requires `Encodable` conformance.
47+
///
48+
/// Provides whether underlying variable type requires
49+
/// `Encodable` conformance.
50+
var requireEncodable: Bool? { base.requireEncodable }
51+
4152
/// Wraps the provided variable erasing its type and
4253
/// initialization type.
4354
///

Sources/CodableMacroPlugin/Variables/BasicVariable.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,21 @@ struct BasicVariable: BasicCodingVariable {
3434
/// during initialization.
3535
let encode: Bool?
3636

37+
/// Whether the variable type requires
38+
/// `Decodable` conformance.
39+
///
40+
/// By default set as `nil`, unless
41+
/// `decode` is set explicitly during
42+
/// initialization.
43+
var requireDecodable: Bool? { self.decode }
44+
/// Whether the variable type requires
45+
/// `Encodable` conformance.
46+
///
47+
/// By default set as `nil`, unless
48+
/// `encode` is set explicitly during
49+
/// initialization.
50+
var requireEncodable: Bool? { self.encode }
51+
3752
/// Creates a new variable with provided data.
3853
///
3954
/// Basic implementation for this variable provided

Sources/CodableMacroPlugin/Variables/ConditionalCodingVariable.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,23 @@ struct ConditionalCodingVariable<Var: Variable>: ComposedVariable {
4747
/// Provides whether underlying variable value is to be encoded,
4848
/// if provided encode option is set as `true` otherwise `false`.
4949
var encode: Bool? { options.encode ? base.encode : false }
50+
51+
/// Whether the variable type requires `Decodable` conformance.
52+
///
53+
/// Provides whether underlying variable type requires
54+
/// `Decodable` conformance, if provided decode
55+
/// option is set as `true` otherwise `false`.
56+
var requireDecodable: Bool? {
57+
return options.decode ? base.requireDecodable : false
58+
}
59+
/// Whether the variable type requires `Encodable` conformance.
60+
///
61+
/// Provides whether underlying variable type requires
62+
/// `Encodable` conformance, if provided encode
63+
/// option is set as `true` otherwise `false`.
64+
var requireEncodable: Bool? {
65+
return options.encode ? base.requireEncodable : false
66+
}
5067
}
5168

5269
extension ConditionalCodingVariable: DefaultOptionComposedVariable {

Sources/CodableMacroPlugin/Variables/DefaultValueVariable.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,17 @@ where Var.Initialization == RequiredInitialization {
4444
/// Always `true` for this type.
4545
var encode: Bool? { true }
4646

47+
/// Whether the variable type requires `Decodable` conformance.
48+
///
49+
/// Provides whether underlying variable type requires
50+
/// `Decodable` conformance.
51+
var requireDecodable: Bool? { base.requireDecodable }
52+
/// Whether the variable type requires `Encodable` conformance.
53+
///
54+
/// Provides whether underlying variable type requires
55+
/// `Encodable` conformance.
56+
var requireEncodable: Bool? { base.requireEncodable }
57+
4758
/// Indicates the initialization type for this variable.
4859
///
4960
/// Provides default initialization value in initialization

Sources/CodableMacroPlugin/Variables/HelperCodedVariable.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ struct HelperCodedVariable<Var: BasicCodingVariable>: ComposedVariable {
4646
/// Always `true` for this type.
4747
var encode: Bool? { true }
4848

49+
/// Whether the variable type requires `Decodable` conformance.
50+
///
51+
/// Always `false` for this type.
52+
var requireDecodable: Bool? { false }
53+
/// Whether the variable type requires `Encodable` conformance.
54+
///
55+
/// Always `false` for this type.
56+
var requireEncodable: Bool? { false }
57+
4958
/// Provides the code syntax for encoding this variable
5059
/// at the provided location.
5160
///

Sources/CodableMacroPlugin/Variables/InitializationVariable.swift

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,25 @@ where Wrapped.Initialization: RequiredVariableInitialization {
4646
/// Depends on whether variable is initializable if underlying variable doesn't
4747
/// specify explicit encoding. Otherwise depends on whether underlying variable
4848
/// is to be decoded.
49-
var encode: Bool? { base.encode == nil ? options.`init` : base.encode! }
49+
var encode: Bool? { base.encode ?? options.`init` }
50+
51+
/// Whether the variable type requires `Decodable` conformance.
52+
///
53+
/// Provides whether underlying variable type requires
54+
/// `Decodable` conformance, if variable can be
55+
/// initialized otherwise `false`.
56+
var requireDecodable: Bool? {
57+
return options.`init` ? base.requireDecodable : false
58+
}
59+
/// Whether the variable type requires `Encodable` conformance.
60+
///
61+
/// Provides whether underlying variable type requires
62+
/// `Encodable` conformance, if underlying variable
63+
/// specifies explicit encoding. Otherwise depends on
64+
/// whether underlying variable is to be decoded.
65+
var requireEncodable: Bool? {
66+
return base.requireEncodable ?? options.`init`
67+
}
5068

5169
/// Indicates the initialization type for this variable.
5270
///

Sources/CodableMacroPlugin/Variables/KeyedVariable.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,25 @@ struct KeyedVariable<Var: Variable>: ComposedVariable {
4242
/// Provides whether underlying variable value is to be encoded,
4343
/// if provided code option is set as `false` otherwise `true`.
4444
var encode: Bool? { options.code ? true : base.encode }
45+
46+
/// Whether the variable type requires `Decodable` conformance.
47+
///
48+
/// Provides whether underlying variable type requires
49+
/// `Decodable` conformance and provided code option
50+
/// is set as `true`. Otherwise depends on whether
51+
/// underlying variable is to be decoded.
52+
var requireDecodable: Bool? {
53+
return options.code ? base.requireDecodable : base.decode
54+
}
55+
/// Whether the variable type requires `Encodable` conformance.
56+
///
57+
/// Provides whether underlying variable type requires
58+
/// `Encodable` conformance and provided code option
59+
/// is set as `true`. Otherwise depends on whether
60+
/// underlying variable is to be encoded.
61+
var requireEncodable: Bool? {
62+
return options.code ? base.requireEncodable : base.encode
63+
}
4564
}
4665

4766
extension KeyedVariable: BasicCodingVariable

0 commit comments

Comments
 (0)