Skip to content

Commit 1043da0

Browse files
authored
feat: added support for mutable optional variable with @IgnoreCoding (#104)
1 parent 5676cd9 commit 1043da0

File tree

6 files changed

+162
-42
lines changed

6 files changed

+162
-42
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ open $PATH_TO_XCODE_INSTALLATION --env METACODABLE_CI=1
3333
# i.e. open /Applications/Xcode.app --env METACODABLE_CI=1
3434
```
3535

36-
> [!IMPORTANT]
36+
> [!IMPORTANT]
3737
> Make sure that Xcode is not running before this command executed.
3838
> Otherwise, this command will have no effect.
3939

Sources/MetaCodable/IgnoreCoding.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/// Indicates the field/case/type needs to ignored from decoding and encoding.
22
///
3-
/// This macro can be applied to initialized variables to ignore them
4-
/// from both decoding and encoding.
3+
/// This macro can be applied to initialized variables or mutable optional
4+
/// variables to ignore them from both decoding and encoding.
55
/// ```swift
66
/// @IgnoreCoding
77
/// var field: String = "some"
@@ -39,8 +39,8 @@ public macro IgnoreCoding() =
3939

4040
/// Indicates the field/case/type needs to ignored from decoding.
4141
///
42-
/// This macro can be applied to initialized mutable variables to ignore
43-
/// them from decoding.
42+
/// This macro can be applied to initialized or optional mutable variables
43+
/// to ignore them from decoding.
4444
/// ```swift
4545
/// @IgnoreDecoding
4646
/// var field: String = "some"

Sources/PluginCore/Diagnostics/UninitializedVariableDecl.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ struct UninitializedVariableDecl<Attr: PropertyAttribute>: DiagnosticProducer {
6262
guard !base.produce(for: syntax, in: context) else { return true }
6363

6464
var result = false
65-
for binding in syntax.as(VariableDeclSyntax.self)!.bindings {
65+
let decl = syntax.as(VariableDeclSyntax.self)!
66+
for binding in decl.bindings {
6667
switch binding.accessorBlock?.accessors {
6768
case .getter:
6869
continue
@@ -78,10 +79,17 @@ struct UninitializedVariableDecl<Attr: PropertyAttribute>: DiagnosticProducer {
7879
guard computed else { fallthrough }
7980
continue
8081
default:
81-
guard binding.initializer == nil else { continue }
82+
let type = binding.typeAnnotation?.type
83+
let isOptional = type?.isOptionalTypeSyntax ?? false
84+
let mutable = decl.bindingSpecifier.tokenKind == .keyword(.var)
85+
guard
86+
binding.initializer == nil && !(isOptional && mutable)
87+
else { continue }
8288
}
8389

84-
var msg = "@\(attr.name) can't be used with uninitialized variable"
90+
var msg = """
91+
@\(attr.name) can't be used with uninitialized non-optional variable
92+
"""
8593
if let varName = binding.pattern.as(IdentifierPatternSyntax.self)?
8694
.identifier.text
8795
{

Sources/PluginCore/Variables/Property/PropertyVariable.swift

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,21 +121,7 @@ extension PropertyVariable {
121121
/// `?` optional type syntax (i.e. `Type?`) or
122122
/// `!` implicitly unwrapped optional type syntax (i.e. `Type!`) or
123123
/// generic optional syntax (i.e. `Optional<Type>`).
124-
var hasOptionalType: Bool {
125-
if type.is(OptionalTypeSyntax.self) {
126-
return true
127-
} else if type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
128-
return true
129-
} else if let type = type.as(IdentifierTypeSyntax.self),
130-
type.name.text == "Optional",
131-
let gArgs = type.genericArgumentClause?.arguments,
132-
gArgs.count == 1
133-
{
134-
return true
135-
} else {
136-
return false
137-
}
138-
}
124+
var hasOptionalType: Bool { type.isOptionalTypeSyntax }
139125

140126
/// Provides type and method expression to use
141127
/// with container expression for decoding/encoding.
@@ -193,3 +179,35 @@ extension CodeBlockItemListSyntax: ConditionalVariableSyntax {
193179
}
194180
}
195181
}
182+
183+
extension TypeSyntax {
184+
/// Check whether current type syntax represents an optional type.
185+
///
186+
/// Checks whether the type syntax uses
187+
/// `?` optional type syntax (i.e. `Type?`) or
188+
/// `!` implicitly unwrapped optional type syntax (i.e. `Type!`) or
189+
/// generic optional syntax (i.e. `Optional<Type>`).
190+
var isOptionalTypeSyntax: Bool {
191+
if self.is(OptionalTypeSyntax.self) {
192+
return true
193+
} else if self.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) {
194+
return true
195+
} else if let type = self.as(IdentifierTypeSyntax.self),
196+
type.name.trimmed.text == "Optional",
197+
let gArgs = type.genericArgumentClause?.arguments,
198+
gArgs.count == 1
199+
{
200+
return true
201+
} else if let type = self.as(MemberTypeSyntax.self),
202+
let baseType = type.baseType.as(IdentifierTypeSyntax.self),
203+
baseType.trimmed.name.text == "Swift",
204+
type.trimmed.name.text == "Optional",
205+
let gArgs = type.genericArgumentClause?.arguments,
206+
gArgs.count == 1
207+
{
208+
return true
209+
} else {
210+
return false
211+
}
212+
}
213+
}

Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ final class PropertyVariableTreeNode: Variable {
2424
/// This is used for caching the container variable name to be reused,
2525
/// allowing not to retrieve container repeatedly.
2626
private var decodingContainer: TokenSyntax?
27-
/// Whether the encoding container variable linked to this node
27+
/// Whether the encoding container variable linked to this node
2828
/// should be declared as immutable.
2929
///
3030
/// This is used to suppress mutability warning in case of

Tests/MetaCodableTests/IgnoreCodingTests.swift

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ struct IgnoreCodingTests {
1616
var one: String
1717
@IgnoreDecoding
1818
var two: String
19+
@IgnoreDecoding
20+
let three: String?
1921
@IgnoreCoding
20-
var three: String { "some" }
22+
var four: String { "some" }
2123
@IgnoreDecoding
22-
var four: String { get { "some" } }
24+
var five: String { get { "some" } }
2325
@IgnoreCoding
24-
var five: String = "some" {
26+
var six: String = "some" {
2527
didSet {
26-
print(five)
28+
print(six)
2729
}
2830
}
2931
}
@@ -33,11 +35,12 @@ struct IgnoreCodingTests {
3335
struct SomeCodable {
3436
var one: String
3537
var two: String
36-
var three: String { "some" }
37-
var four: String { get { "some" } }
38-
var five: String = "some" {
38+
let three: String?
39+
var four: String { "some" }
40+
var five: String { get { "some" } }
41+
var six: String = "some" {
3942
didSet {
40-
print(five)
43+
print(six)
4144
}
4245
}
4346
}
@@ -51,20 +54,22 @@ struct IgnoreCodingTests {
5154
func encode(to encoder: any Encoder) throws {
5255
var container = encoder.container(keyedBy: CodingKeys.self)
5356
try container.encode(self.two, forKey: CodingKeys.two)
57+
try container.encodeIfPresent(self.three, forKey: CodingKeys.three)
5458
}
5559
}
5660
5761
extension SomeCodable {
5862
enum CodingKeys: String, CodingKey {
5963
case two = "two"
64+
case three = "three"
6065
}
6166
}
6267
""",
6368
diagnostics: [
6469
.init(
6570
id: IgnoreCoding.misuseID,
6671
message:
67-
"@IgnoreCoding can't be used with uninitialized variable one",
72+
"@IgnoreCoding can't be used with uninitialized non-optional variable one",
6873
line: 3, column: 5,
6974
fixIts: [
7075
.init(message: "Remove @IgnoreCoding attribute")
@@ -73,12 +78,21 @@ struct IgnoreCodingTests {
7378
.init(
7479
id: IgnoreDecoding.misuseID,
7580
message:
76-
"@IgnoreDecoding can't be used with uninitialized variable two",
81+
"@IgnoreDecoding can't be used with uninitialized non-optional variable two",
7782
line: 5, column: 5,
7883
fixIts: [
7984
.init(message: "Remove @IgnoreDecoding attribute")
8085
]
8186
),
87+
.init(
88+
id: IgnoreDecoding.misuseID,
89+
message:
90+
"@IgnoreDecoding can't be used with uninitialized non-optional variable three",
91+
line: 7, column: 5,
92+
fixIts: [
93+
.init(message: "Remove @IgnoreDecoding attribute")
94+
]
95+
),
8296
]
8397
)
8498
}
@@ -168,6 +182,86 @@ struct IgnoreCodingTests {
168182
"""
169183
)
170184
}
185+
186+
struct Optional {
187+
@Codable
188+
struct SomeCodable {
189+
@IgnoreCoding
190+
var one: String?
191+
@IgnoreCoding
192+
var two: String!
193+
// @IgnoreCoding
194+
// var three: Swift.Optional<String>
195+
let four: String
196+
}
197+
198+
@Test
199+
func expansion() throws {
200+
assertMacroExpansion(
201+
"""
202+
@Codable
203+
struct SomeCodable {
204+
@IgnoreCoding
205+
var one: String?
206+
@IgnoreCoding
207+
var two: String!
208+
@IgnoreCoding
209+
var three: Optional<String>
210+
let four: String
211+
}
212+
""",
213+
expandedSource:
214+
"""
215+
struct SomeCodable {
216+
var one: String?
217+
var two: String!
218+
var three: Optional<String>
219+
let four: String
220+
}
221+
222+
extension SomeCodable: Decodable {
223+
init(from decoder: any Decoder) throws {
224+
let container = try decoder.container(keyedBy: CodingKeys.self)
225+
self.four = try container.decode(String.self, forKey: CodingKeys.four)
226+
}
227+
}
228+
229+
extension SomeCodable: Encodable {
230+
func encode(to encoder: any Encoder) throws {
231+
var container = encoder.container(keyedBy: CodingKeys.self)
232+
try container.encode(self.four, forKey: CodingKeys.four)
233+
}
234+
}
235+
236+
extension SomeCodable {
237+
enum CodingKeys: String, CodingKey {
238+
case four = "four"
239+
}
240+
}
241+
"""
242+
)
243+
}
244+
245+
@Test
246+
func decoding() throws {
247+
let json = try #require("{\"four\":\"som\"}".data(using: .utf8))
248+
let obj = try JSONDecoder().decode(SomeCodable.self, from: json)
249+
#expect(obj.one == nil)
250+
#expect(obj.two == nil)
251+
// #expect(obj.three == nil)
252+
#expect(obj.four == "som")
253+
}
254+
255+
@Test
256+
func encoding() throws {
257+
let obj = SomeCodable(one: "one", two: "two", four: "some")
258+
let json = try JSONEncoder().encode(obj)
259+
let jObj = try JSONSerialization.jsonObject(with: json)
260+
let dict = try #require(jObj as? [String: Any])
261+
#expect(dict.count == 1)
262+
#expect(dict["four"] as? String == "some")
263+
}
264+
}
171265
}
172266

173267
struct EnumDecodingEncodingIgnore {
@@ -736,7 +830,7 @@ struct IgnoreCodingTests {
736830
var one: String = "some"
737831
@IgnoreDecoding
738832
@CodedAt("deeply", "nested", "key")
739-
var two: String = "some"
833+
var two: String!
740834
@IgnoreEncoding
741835
@CodedIn("deeply", "nested")
742836
var three: String = "some"
@@ -756,7 +850,7 @@ struct IgnoreCodingTests {
756850
var one: String = "some"
757851
@IgnoreDecoding
758852
@CodedAt("deeply", "nested", "key")
759-
var two: String = "some"
853+
var two: String!
760854
@IgnoreEncoding
761855
@CodedIn("deeply", "nested")
762856
var three: String = "some"
@@ -769,7 +863,7 @@ struct IgnoreCodingTests {
769863
"""
770864
struct SomeCodable {
771865
var one: String = "some"
772-
var two: String = "some"
866+
var two: String!
773867
var three: String = "some"
774868
var four: String = "some"
775869
}
@@ -790,7 +884,7 @@ struct IgnoreCodingTests {
790884
var deeply_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply)
791885
var nested_deeply_container = deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested)
792886
try nested_deeply_container.encode(self.one, forKey: CodingKeys.one)
793-
try nested_deeply_container.encode(self.two, forKey: CodingKeys.two)
887+
try nested_deeply_container.encodeIfPresent(self.two, forKey: CodingKeys.two)
794888
}
795889
}
796890
@@ -816,7 +910,7 @@ struct IgnoreCodingTests {
816910
var one: String = "some"
817911
@IgnoreDecoding
818912
@CodedAt("deeply", "nested", "key")
819-
var two: String = "some"
913+
var two: String!
820914
@IgnoreEncoding
821915
@CodedIn("deeply", "nested")
822916
var three: String = "some"
@@ -836,7 +930,7 @@ struct IgnoreCodingTests {
836930
var one: String = "some"
837931
@IgnoreDecoding
838932
@CodedAt("deeply", "nested", "key")
839-
var two: String = "some"
933+
var two: String!
840934
@IgnoreEncoding
841935
@CodedIn("deeply", "nested")
842936
var three: String = "some"
@@ -849,7 +943,7 @@ struct IgnoreCodingTests {
849943
"""
850944
class SomeCodable {
851945
var one: String = "some"
852-
var two: String = "some"
946+
var two: String!
853947
var three: String = "some"
854948
var four: String = "some"
855949
@@ -866,7 +960,7 @@ struct IgnoreCodingTests {
866960
var deeply_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply)
867961
var nested_deeply_container = deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested)
868962
try nested_deeply_container.encode(self.one, forKey: CodingKeys.one)
869-
try nested_deeply_container.encode(self.two, forKey: CodingKeys.two)
963+
try nested_deeply_container.encodeIfPresent(self.two, forKey: CodingKeys.two)
870964
}
871965
872966
enum CodingKeys: String, CodingKey {

0 commit comments

Comments
 (0)