From 7c55cf6b2e35677c400c3977ca77908d3d7894bd Mon Sep 17 00:00:00 2001 From: Soumya Ranjan Mahunt Date: Wed, 2 Oct 2024 17:13:41 +0530 Subject: [PATCH] feat: added support for mutable optional variable with `@IgnoreCoding` --- CONTRIBUTING.md | 2 +- Sources/MetaCodable/IgnoreCoding.swift | 8 +- .../UninitializedVariableDecl.swift | 14 +- .../Variables/Property/PropertyVariable.swift | 48 +++++-- .../Tree/PropertyVariableTreeNode.swift | 2 +- .../MetaCodableTests/IgnoreCodingTests.swift | 130 +++++++++++++++--- 6 files changed, 162 insertions(+), 42 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7193f851..373c317b3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,7 +33,7 @@ open $PATH_TO_XCODE_INSTALLATION --env METACODABLE_CI=1 # i.e. open /Applications/Xcode.app --env METACODABLE_CI=1 ``` -> [!IMPORTANT] +> [!IMPORTANT] > Make sure that Xcode is not running before this command executed. > Otherwise, this command will have no effect. diff --git a/Sources/MetaCodable/IgnoreCoding.swift b/Sources/MetaCodable/IgnoreCoding.swift index aa6188d2d..819fd9348 100644 --- a/Sources/MetaCodable/IgnoreCoding.swift +++ b/Sources/MetaCodable/IgnoreCoding.swift @@ -1,7 +1,7 @@ /// Indicates the field/case/type needs to ignored from decoding and encoding. /// -/// This macro can be applied to initialized variables to ignore them -/// from both decoding and encoding. +/// This macro can be applied to initialized variables or mutable optional +/// variables to ignore them from both decoding and encoding. /// ```swift /// @IgnoreCoding /// var field: String = "some" @@ -39,8 +39,8 @@ public macro IgnoreCoding() = /// Indicates the field/case/type needs to ignored from decoding. /// -/// This macro can be applied to initialized mutable variables to ignore -/// them from decoding. +/// This macro can be applied to initialized or optional mutable variables +/// to ignore them from decoding. /// ```swift /// @IgnoreDecoding /// var field: String = "some" diff --git a/Sources/PluginCore/Diagnostics/UninitializedVariableDecl.swift b/Sources/PluginCore/Diagnostics/UninitializedVariableDecl.swift index 4c264e0fd..d034a410b 100644 --- a/Sources/PluginCore/Diagnostics/UninitializedVariableDecl.swift +++ b/Sources/PluginCore/Diagnostics/UninitializedVariableDecl.swift @@ -62,7 +62,8 @@ struct UninitializedVariableDecl: DiagnosticProducer { guard !base.produce(for: syntax, in: context) else { return true } var result = false - for binding in syntax.as(VariableDeclSyntax.self)!.bindings { + let decl = syntax.as(VariableDeclSyntax.self)! + for binding in decl.bindings { switch binding.accessorBlock?.accessors { case .getter: continue @@ -78,10 +79,17 @@ struct UninitializedVariableDecl: DiagnosticProducer { guard computed else { fallthrough } continue default: - guard binding.initializer == nil else { continue } + let type = binding.typeAnnotation?.type + let isOptional = type?.isOptionalTypeSyntax ?? false + let mutable = decl.bindingSpecifier.tokenKind == .keyword(.var) + guard + binding.initializer == nil && !(isOptional && mutable) + else { continue } } - var msg = "@\(attr.name) can't be used with uninitialized variable" + var msg = """ + @\(attr.name) can't be used with uninitialized non-optional variable + """ if let varName = binding.pattern.as(IdentifierPatternSyntax.self)? .identifier.text { diff --git a/Sources/PluginCore/Variables/Property/PropertyVariable.swift b/Sources/PluginCore/Variables/Property/PropertyVariable.swift index 7f6e2dc44..4bf71f2b7 100644 --- a/Sources/PluginCore/Variables/Property/PropertyVariable.swift +++ b/Sources/PluginCore/Variables/Property/PropertyVariable.swift @@ -121,21 +121,7 @@ extension PropertyVariable { /// `?` optional type syntax (i.e. `Type?`) or /// `!` implicitly unwrapped optional type syntax (i.e. `Type!`) or /// generic optional syntax (i.e. `Optional`). - var hasOptionalType: Bool { - if type.is(OptionalTypeSyntax.self) { - return true - } else if type.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) { - return true - } else if let type = type.as(IdentifierTypeSyntax.self), - type.name.text == "Optional", - let gArgs = type.genericArgumentClause?.arguments, - gArgs.count == 1 - { - return true - } else { - return false - } - } + var hasOptionalType: Bool { type.isOptionalTypeSyntax } /// Provides type and method expression to use /// with container expression for decoding/encoding. @@ -193,3 +179,35 @@ extension CodeBlockItemListSyntax: ConditionalVariableSyntax { } } } + +extension TypeSyntax { + /// Check whether current type syntax represents an optional type. + /// + /// Checks whether the type syntax uses + /// `?` optional type syntax (i.e. `Type?`) or + /// `!` implicitly unwrapped optional type syntax (i.e. `Type!`) or + /// generic optional syntax (i.e. `Optional`). + var isOptionalTypeSyntax: Bool { + if self.is(OptionalTypeSyntax.self) { + return true + } else if self.is(ImplicitlyUnwrappedOptionalTypeSyntax.self) { + return true + } else if let type = self.as(IdentifierTypeSyntax.self), + type.name.trimmed.text == "Optional", + let gArgs = type.genericArgumentClause?.arguments, + gArgs.count == 1 + { + return true + } else if let type = self.as(MemberTypeSyntax.self), + let baseType = type.baseType.as(IdentifierTypeSyntax.self), + baseType.trimmed.name.text == "Swift", + type.trimmed.name.text == "Optional", + let gArgs = type.genericArgumentClause?.arguments, + gArgs.count == 1 + { + return true + } else { + return false + } + } +} diff --git a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift index 9a5dce88a..f4b02069e 100644 --- a/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift +++ b/Sources/PluginCore/Variables/Property/Tree/PropertyVariableTreeNode.swift @@ -24,7 +24,7 @@ final class PropertyVariableTreeNode: Variable { /// This is used for caching the container variable name to be reused, /// allowing not to retrieve container repeatedly. private var decodingContainer: TokenSyntax? - /// Whether the encoding container variable linked to this node + /// Whether the encoding container variable linked to this node /// should be declared as immutable. /// /// This is used to suppress mutability warning in case of diff --git a/Tests/MetaCodableTests/IgnoreCodingTests.swift b/Tests/MetaCodableTests/IgnoreCodingTests.swift index d74681590..44c3e80cd 100644 --- a/Tests/MetaCodableTests/IgnoreCodingTests.swift +++ b/Tests/MetaCodableTests/IgnoreCodingTests.swift @@ -16,14 +16,16 @@ struct IgnoreCodingTests { var one: String @IgnoreDecoding var two: String + @IgnoreDecoding + let three: String? @IgnoreCoding - var three: String { "some" } + var four: String { "some" } @IgnoreDecoding - var four: String { get { "some" } } + var five: String { get { "some" } } @IgnoreCoding - var five: String = "some" { + var six: String = "some" { didSet { - print(five) + print(six) } } } @@ -33,11 +35,12 @@ struct IgnoreCodingTests { struct SomeCodable { var one: String var two: String - var three: String { "some" } - var four: String { get { "some" } } - var five: String = "some" { + let three: String? + var four: String { "some" } + var five: String { get { "some" } } + var six: String = "some" { didSet { - print(five) + print(six) } } } @@ -51,12 +54,14 @@ struct IgnoreCodingTests { func encode(to encoder: any Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(self.two, forKey: CodingKeys.two) + try container.encodeIfPresent(self.three, forKey: CodingKeys.three) } } extension SomeCodable { enum CodingKeys: String, CodingKey { case two = "two" + case three = "three" } } """, @@ -64,7 +69,7 @@ struct IgnoreCodingTests { .init( id: IgnoreCoding.misuseID, message: - "@IgnoreCoding can't be used with uninitialized variable one", + "@IgnoreCoding can't be used with uninitialized non-optional variable one", line: 3, column: 5, fixIts: [ .init(message: "Remove @IgnoreCoding attribute") @@ -73,12 +78,21 @@ struct IgnoreCodingTests { .init( id: IgnoreDecoding.misuseID, message: - "@IgnoreDecoding can't be used with uninitialized variable two", + "@IgnoreDecoding can't be used with uninitialized non-optional variable two", line: 5, column: 5, fixIts: [ .init(message: "Remove @IgnoreDecoding attribute") ] ), + .init( + id: IgnoreDecoding.misuseID, + message: + "@IgnoreDecoding can't be used with uninitialized non-optional variable three", + line: 7, column: 5, + fixIts: [ + .init(message: "Remove @IgnoreDecoding attribute") + ] + ), ] ) } @@ -168,6 +182,86 @@ struct IgnoreCodingTests { """ ) } + + struct Optional { + @Codable + struct SomeCodable { + @IgnoreCoding + var one: String? + @IgnoreCoding + var two: String! + // @IgnoreCoding + // var three: Swift.Optional + let four: String + } + + @Test + func expansion() throws { + assertMacroExpansion( + """ + @Codable + struct SomeCodable { + @IgnoreCoding + var one: String? + @IgnoreCoding + var two: String! + @IgnoreCoding + var three: Optional + let four: String + } + """, + expandedSource: + """ + struct SomeCodable { + var one: String? + var two: String! + var three: Optional + let four: String + } + + extension SomeCodable: Decodable { + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.four = try container.decode(String.self, forKey: CodingKeys.four) + } + } + + extension SomeCodable: Encodable { + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.four, forKey: CodingKeys.four) + } + } + + extension SomeCodable { + enum CodingKeys: String, CodingKey { + case four = "four" + } + } + """ + ) + } + + @Test + func decoding() throws { + let json = try #require("{\"four\":\"som\"}".data(using: .utf8)) + let obj = try JSONDecoder().decode(SomeCodable.self, from: json) + #expect(obj.one == nil) + #expect(obj.two == nil) + // #expect(obj.three == nil) + #expect(obj.four == "som") + } + + @Test + func encoding() throws { + let obj = SomeCodable(one: "one", two: "two", four: "some") + let json = try JSONEncoder().encode(obj) + let jObj = try JSONSerialization.jsonObject(with: json) + let dict = try #require(jObj as? [String: Any]) + #expect(dict.count == 1) + #expect(dict["four"] as? String == "some") + } + } } struct EnumDecodingEncodingIgnore { @@ -736,7 +830,7 @@ struct IgnoreCodingTests { var one: String = "some" @IgnoreDecoding @CodedAt("deeply", "nested", "key") - var two: String = "some" + var two: String! @IgnoreEncoding @CodedIn("deeply", "nested") var three: String = "some" @@ -756,7 +850,7 @@ struct IgnoreCodingTests { var one: String = "some" @IgnoreDecoding @CodedAt("deeply", "nested", "key") - var two: String = "some" + var two: String! @IgnoreEncoding @CodedIn("deeply", "nested") var three: String = "some" @@ -769,7 +863,7 @@ struct IgnoreCodingTests { """ struct SomeCodable { var one: String = "some" - var two: String = "some" + var two: String! var three: String = "some" var four: String = "some" } @@ -790,7 +884,7 @@ struct IgnoreCodingTests { var deeply_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply) var nested_deeply_container = deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested) try nested_deeply_container.encode(self.one, forKey: CodingKeys.one) - try nested_deeply_container.encode(self.two, forKey: CodingKeys.two) + try nested_deeply_container.encodeIfPresent(self.two, forKey: CodingKeys.two) } } @@ -816,7 +910,7 @@ struct IgnoreCodingTests { var one: String = "some" @IgnoreDecoding @CodedAt("deeply", "nested", "key") - var two: String = "some" + var two: String! @IgnoreEncoding @CodedIn("deeply", "nested") var three: String = "some" @@ -836,7 +930,7 @@ struct IgnoreCodingTests { var one: String = "some" @IgnoreDecoding @CodedAt("deeply", "nested", "key") - var two: String = "some" + var two: String! @IgnoreEncoding @CodedIn("deeply", "nested") var three: String = "some" @@ -849,7 +943,7 @@ struct IgnoreCodingTests { """ class SomeCodable { var one: String = "some" - var two: String = "some" + var two: String! var three: String = "some" var four: String = "some" @@ -866,7 +960,7 @@ struct IgnoreCodingTests { var deeply_container = container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.deeply) var nested_deeply_container = deeply_container.nestedContainer(keyedBy: CodingKeys.self, forKey: CodingKeys.nested) try nested_deeply_container.encode(self.one, forKey: CodingKeys.one) - try nested_deeply_container.encode(self.two, forKey: CodingKeys.two) + try nested_deeply_container.encodeIfPresent(self.two, forKey: CodingKeys.two) } enum CodingKeys: String, CodingKey {