Skip to content

Commit c27e3a8

Browse files
authored
fix: provided Inherits macro as workaround for class conformance error in SwiftUI (SwiftyLab#71)
* fix: provided `Inherits` macro as workaround for class conformance error in SwiftUI * chore: add words for spellcheck dictionary
1 parent de6374b commit c27e3a8

10 files changed

Lines changed: 373 additions & 30 deletions

File tree

.github/config/spellcheck-wordlist.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,5 @@ cocoapods
6262
DocumentationExtension
6363
mergeBehavior
6464
lowercasing
65+
SwiftData
66+
SwiftUI

Sources/MacroPlugin/Definitions.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,3 +535,40 @@ struct IgnoreCodingInitialized: PeerMacro {
535535
)
536536
}
537537
}
538+
539+
/// Attribute type for `Inherits` macro-attribute.
540+
///
541+
/// This type can validate`Inherits` macro-attribute
542+
/// usage and extract data for `Codable` macro to
543+
/// generate implementation.
544+
///
545+
/// Attaching this macro to type allows indicating the generated
546+
/// `Codable` conformance whether a class already inheriting
547+
/// conformance from super class or not.
548+
struct Inherits: PeerMacro {
549+
/// Provide metadata to `Codable` macro for final expansion
550+
/// and verify proper usage of this macro.
551+
///
552+
/// This macro doesn't perform any expansion rather `Codable` macro
553+
/// uses when performing expansion.
554+
///
555+
/// This macro verifies that macro usage condition is met by attached
556+
/// declaration by using the `validate` implementation provided.
557+
///
558+
/// - Parameters:
559+
/// - node: The attribute describing this macro.
560+
/// - declaration: The declaration this macro attribute is attached to.
561+
/// - context: The context in which to perform the macro expansion.
562+
///
563+
/// - Returns: No declaration is returned, only attached declaration is
564+
/// analyzed.
565+
static func expansion(
566+
of node: AttributeSyntax,
567+
providingPeersOf declaration: some DeclSyntaxProtocol,
568+
in context: some MacroExpansionContext
569+
) throws -> [DeclSyntax] {
570+
return try PluginCore.Inherits.expansion(
571+
of: node, providingPeersOf: declaration, in: context
572+
)
573+
}
574+
}

Sources/MacroPlugin/Plugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ struct MetaCodablePlugin: CompilerPlugin {
2424
MemberInit.self,
2525
CodingKeys.self,
2626
IgnoreCodingInitialized.self,
27+
Inherits.self,
2728
]
2829
}

Sources/MetaCodable/Codable/Codable.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,36 @@
5858
@available(swift 5.9)
5959
public macro Codable() =
6060
#externalMacro(module: "MacroPlugin", type: "Codable")
61+
62+
/// Indicates whether super class conforms to `Codable` or not.
63+
///
64+
/// By default, ``Codable()`` assumes class inherits `Decodable`
65+
/// or `Encodable` conformance if it doesn't receive protocol needs
66+
/// to be conformed from the compiler. Using this macro, it can be explicitly
67+
/// indicated that the class doesn't inherit conformance in such cases.
68+
///
69+
/// Following code indicates ``Codable()`` that `Item` class doesn't
70+
/// inherit conformance:
71+
/// ```swift
72+
/// @Codable
73+
/// @Model
74+
/// @Inherits(decodable: false, encodable: false)
75+
/// final class Item {
76+
/// @CodedAt("timestamp")
77+
/// var timestamp: Date? = nil
78+
///
79+
/// init() { }
80+
/// }
81+
/// ```
82+
///
83+
/// - Parameters:
84+
/// - decodable: Whether super class conforms to `Decodable`.
85+
/// - encodable: Whether super class conforms to `Encodable`.
86+
///
87+
/// - Note: This macro on its own only validates if attached declaration
88+
/// is a class declaration. ``Codable()`` macro uses this macro
89+
/// when generating final implementations.
90+
@attached(peer)
91+
@available(swift 5.9)
92+
public macro Inherits(decodable: Bool, encodable: Bool) =
93+
#externalMacro(module: "MacroPlugin", type: "Inherits")

Sources/MetaCodable/MetaCodable.docc/Limitations.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,9 @@ Due to these limitations, `Encodable` conformance isn't generated, users has to
7676
### Why MetaProtocolCodable plugin can't scan Xcode target dependencies?
7777

7878
Currently Swift Package Manager always returns empty list for Xcode target dependencies as noted in [this bug](https://github.com/apple/swift-package-manager/issues/6003). Hence `MetaProtocolCodable` can currently only scan the files from the target or from the project including the target.
79+
80+
### Why macro is breaking with SwiftData class?
81+
82+
Currently during certain customization in SwiftUI, compiler is sending no protocol data to ``Codable()``. Due to this, ``Codable()`` tries to find `Codable` protocol implementation for the class. If no implementation found, ``Codable()`` assumes class inherits conformance from super class, and generates implementation accordingly causing issues like [#56](https://github.com/SwiftyLab/MetaCodable/issues/56).
83+
84+
Until this is fixes from Swift compiler, ``Inherits(decodable:encodable:)`` macro can be used to indicate explicitly that class doesn't inherit `Codable` conformance.

Sources/MetaCodable/MetaCodable.docc/MetaCodable.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ Supercharge `Swift`'s `Codable` implementations with macros.
8585
- ``IgnoreEncoding()``
8686
- ``CodingKeys(_:)``
8787
- ``IgnoreCodingInitialized()``
88+
- ``Inherits(decodable:encodable:)``
8889

8990
### Helpers
9091

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
@_implementationOnly import SwiftSyntax
2+
3+
/// Attribute type for `Inherits` macro-attribute.
4+
///
5+
/// This type can validate`Inherits` macro-attribute
6+
/// usage and extract data for `Codable` macro to
7+
/// generate implementation.
8+
///
9+
/// Attaching this macro to type allows indicating the generated
10+
/// `Codable` conformance whether a class already inheriting
11+
/// conformance from super class or not.
12+
package struct Inherits: PeerAttribute {
13+
/// The node syntax provided
14+
/// during initialization.
15+
let node: AttributeSyntax
16+
17+
/// Whether super class conforms to `Decodable`.
18+
///
19+
/// In case of no super class, this should be `false`.
20+
var decodable: Bool {
21+
return node.arguments?.as(LabeledExprListSyntax.self)?.first { expr in
22+
expr.label?.tokenKind == .identifier("decodable")
23+
}?.expression.as(BooleanLiteralExprSyntax.self)?.literal
24+
.tokenKind == .keyword(.true)
25+
}
26+
27+
/// Whether super class conforms to `Encodable`.
28+
///
29+
/// In case of no super class, this should be `false`.
30+
var encodable: Bool {
31+
return node.arguments?.as(LabeledExprListSyntax.self)?.first { expr in
32+
expr.label?.tokenKind == .identifier("encodable")
33+
}?.expression.as(BooleanLiteralExprSyntax.self)?.literal
34+
.tokenKind == .keyword(.true)
35+
}
36+
37+
/// Creates a new instance with the provided node
38+
///
39+
/// The initializer fails to create new instance if the name
40+
/// of the provided node is different than this attribute.
41+
///
42+
/// - Parameter node: The attribute syntax to create with.
43+
/// - Returns: Newly created attribute instance.
44+
init?(from node: AttributeSyntax) {
45+
guard
46+
node.attributeName.as(IdentifierTypeSyntax.self)!
47+
.name.text == Self.name
48+
else { return nil }
49+
self.node = node
50+
}
51+
52+
/// Builds diagnoser that can validate this macro
53+
/// attached declaration.
54+
///
55+
/// Builds diagnoser that validates attached declaration
56+
/// is a class declaration, has `Codable` macro attached
57+
/// and macro usage is not duplicated for the same declaration.
58+
///
59+
/// - Returns: The built diagnoser instance.
60+
func diagnoser() -> DiagnosticProducer {
61+
return AggregatedDiagnosticProducer {
62+
shouldNotDuplicate()
63+
mustBeCombined(with: Codable.self)
64+
expect(syntaxes: ClassDeclSyntax.self)
65+
}
66+
}
67+
}

Sources/PluginCore/Variables/Type/ClassVariable.swift

Lines changed: 39 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ struct ClassVariable: TypeVariable, DeclaredVariable {
1212
/// The declaration used to create this variable.
1313
let decl: ClassDeclSyntax
1414

15+
/// The `Codable` protocol conformance inheritance data.
16+
///
17+
/// Allows customizing generated implementation, based on whether
18+
/// super class conforms to `Decodable` or `Encodable`.
19+
var inherits: Inherits? {
20+
for attribute in decl.attributes {
21+
guard
22+
case let .attribute(attribute) = attribute,
23+
let attr = Inherits(from: attribute)
24+
else { continue }
25+
return attr
26+
}
27+
return nil
28+
}
29+
1530
/// Creates a new variable from declaration and expansion context.
1631
///
1732
/// Uses the class declaration with member group to generate conformances.
@@ -110,28 +125,25 @@ struct ClassVariable: TypeVariable, DeclaredVariable {
110125
let newLocation: TypeCodingLocation
111126
let overridden: Bool
112127
var modifiers: DeclModifierListSyntax = [.init(name: "required")]
113-
var code: CodeBlockItemListSyntax
114-
#if swift(>=5.9.2)
128+
let method = location.method
129+
let conformance = TypeSyntax(stringLiteral: method.protocol)
130+
115131
if location.conformance == nil, implementDecodable(location: location) {
116-
let method = location.method
117-
let conformance = TypeSyntax(stringLiteral: method.protocol)
118132
newLocation = .init(method: method, conformance: conformance)
119-
overridden = true
120-
code = "try super.\(method.name)(\(method.argLabel): \(method.arg))"
133+
overridden = inherits?.decodable ?? true
121134
} else {
122135
newLocation = location
123136
overridden = false
124-
code = ""
125137
}
126-
#else
127-
let conformance = TypeSyntax(stringLiteral: location.method.protocol)
128-
newLocation = .init(method: location.method, conformance: conformance)
129-
overridden = false
130-
code = ""
131-
#endif
138+
132139
guard
133140
let generated = group.decoding(in: context, from: newLocation)
134141
else { return nil }
142+
var code: CodeBlockItemListSyntax = if overridden {
143+
"try super.\(method.name)(\(method.argLabel): \(method.arg))"
144+
} else {
145+
""
146+
}
135147
code.insert(contentsOf: generated.code, at: code.startIndex)
136148
modifiers.append(contentsOf: generated.modifiers)
137149
return .init(
@@ -157,32 +169,29 @@ struct ClassVariable: TypeVariable, DeclaredVariable {
157169
) -> TypeGenerated? {
158170
let newLocation: TypeCodingLocation
159171
let overridden: Bool
160-
var modifiers: DeclModifierListSyntax
161-
var code: CodeBlockItemListSyntax
162-
#if swift(>=5.9.2)
172+
let method = location.method
173+
let conformance = TypeSyntax(stringLiteral: method.protocol)
174+
163175
if location.conformance == nil, implementEncodable(location: location) {
164-
let method = location.method
165-
let conformance = TypeSyntax(stringLiteral: method.protocol)
166176
newLocation = .init(method: method, conformance: conformance)
167-
overridden = true
168-
modifiers = [.init(name: "override")]
169-
code = "try super.\(method.name)(\(method.argLabel): \(method.arg))"
177+
overridden = inherits?.encodable ?? true
170178
} else {
171179
newLocation = location
172180
overridden = false
173-
modifiers = []
174-
code = ""
175181
}
176-
#else
177-
let conformance = TypeSyntax(stringLiteral: location.method.protocol)
178-
newLocation = .init(method: location.method, conformance: conformance)
179-
overridden = false
180-
modifiers = []
181-
code = ""
182-
#endif
182+
183183
guard
184184
let generated = group.encoding(in: context, to: newLocation)
185185
else { return nil }
186+
var code: CodeBlockItemListSyntax
187+
var modifiers: DeclModifierListSyntax
188+
if overridden {
189+
modifiers = [.init(name: "override")]
190+
code = "try super.\(method.name)(\(method.argLabel): \(method.arg))"
191+
} else {
192+
modifiers = []
193+
code = ""
194+
}
186195
code.insert(contentsOf: generated.code, at: code.startIndex)
187196
modifiers.append(contentsOf: generated.modifiers)
188197
return .init(

0 commit comments

Comments
 (0)