Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
8 changes: 4 additions & 4 deletions Sources/MetaCodable/IgnoreCoding.swift
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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"
Expand Down
14 changes: 11 additions & 3 deletions Sources/PluginCore/Diagnostics/UninitializedVariableDecl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ struct UninitializedVariableDecl<Attr: PropertyAttribute>: 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
Expand All @@ -78,10 +79,17 @@ struct UninitializedVariableDecl<Attr: PropertyAttribute>: 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
{
Expand Down
48 changes: 33 additions & 15 deletions Sources/PluginCore/Variables/Property/PropertyVariable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,7 @@
/// `?` optional type syntax (i.e. `Type?`) or
/// `!` implicitly unwrapped optional type syntax (i.e. `Type!`) or
/// generic optional syntax (i.e. `Optional<Type>`).
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.
Expand Down Expand Up @@ -193,3 +179,35 @@
}
}
}

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<Type>`).
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

Check warning on line 208 in Sources/PluginCore/Variables/Property/PropertyVariable.swift

View check run for this annotation

Codecov / codecov/patch

Sources/PluginCore/Variables/Property/PropertyVariable.swift#L208

Added line #L208 was not covered by tests
} else {
return false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
130 changes: 112 additions & 18 deletions Tests/MetaCodableTests/IgnoreCodingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand All @@ -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)
}
}
}
Expand All @@ -51,20 +54,22 @@ 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"
}
}
""",
diagnostics: [
.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")
Expand All @@ -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")
]
),
]
)
}
Expand Down Expand Up @@ -168,6 +182,86 @@ struct IgnoreCodingTests {
"""
)
}

struct Optional {
@Codable
struct SomeCodable {
@IgnoreCoding
var one: String?
@IgnoreCoding
var two: String!
// @IgnoreCoding
// var three: Swift.Optional<String>
let four: String
}

@Test
func expansion() throws {
assertMacroExpansion(
"""
@Codable
struct SomeCodable {
@IgnoreCoding
var one: String?
@IgnoreCoding
var two: String!
@IgnoreCoding
var three: Optional<String>
let four: String
}
""",
expandedSource:
"""
struct SomeCodable {
var one: String?
var two: String!
var three: Optional<String>
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 {
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
}
Expand All @@ -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)
}
}

Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"

Expand All @@ -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 {
Expand Down
Loading