From 3f8b5ef92297b75f64a5bcdee179fad64eb557fb Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 8 Jun 2026 20:42:45 +0800 Subject: [PATCH 1/2] fix: preserve nonisolated macro conformances Co-authored-by: Cursor --- .../MacroImplementations/CodableImpl.swift | 132 +++++--- Tests/ReerCodableTests/MacroExpandTests.swift | 287 ++++++++++++++++++ .../NonisolatedCodableTests.swift | 78 +++++ 3 files changed, 447 insertions(+), 50 deletions(-) create mode 100644 Tests/ReerCodableTests/NonisolatedCodableTests.swift diff --git a/Sources/ReerCodableMacros/MacroImplementations/CodableImpl.swift b/Sources/ReerCodableMacros/MacroImplementations/CodableImpl.swift index 8bc67bc..22d3319 100644 --- a/Sources/ReerCodableMacros/MacroImplementations/CodableImpl.swift +++ b/Sources/ReerCodableMacros/MacroImplementations/CodableImpl.swift @@ -40,6 +40,56 @@ private func parameterMatchesType( return String(identifier) == baseName } +private func normalizedInheritedTypeName(_ type: TypeSyntax) -> String { + type.trimmedDescription + .split(whereSeparator: \.isWhitespace) + .filter { token in + token != "nonisolated" && !token.hasPrefix("@") + } + .joined(separator: " ") +} + +private func hasInheritedType( + in declaration: some DeclGroupSyntax, + matching predicate: (String) -> Bool +) -> Bool { + guard let inheritedTypes = declaration.inheritanceClause?.inheritedTypes else { return false } + return inheritedTypes.contains { predicate(normalizedInheritedTypeName($0.type)) } +} + +private func hasNonisolatedModifier(_ declaration: some DeclGroupSyntax) -> Bool { + let modifiers: DeclModifierListSyntax? + if let structDecl = declaration.as(StructDeclSyntax.self) { + modifiers = structDecl.modifiers + } else if let classDecl = declaration.as(ClassDeclSyntax.self) { + modifiers = classDecl.modifiers + } else if let enumDecl = declaration.as(EnumDeclSyntax.self) { + modifiers = enumDecl.modifiers + } else { + modifiers = nil + } + return modifiers?.contains(where: { $0.name.text == "nonisolated" }) == true +} + +private func extensionIsolationModifier(for declaration: some DeclGroupSyntax) -> String { + hasNonisolatedModifier(declaration) ? "nonisolated " : "" +} + +private func makeConformanceExtension( + for type: some TypeSyntaxProtocol, + declaration: some DeclGroupSyntax, + conformances: [String] +) -> ExtensionDeclSyntax? { + guard !conformances.isEmpty else { return nil } + let isolationModifier = extensionIsolationModifier(for: declaration) + let conformanceList = conformances.joined(separator: ", ") + let extensionDecl: DeclSyntax = + """ + \(raw: isolationModifier)extension \(type.trimmed): \(raw: conformanceList) {} + """ + return extensionDecl.cast(ExtensionDeclSyntax.self) +} + extension RECodable: ExtensionMacro { public static func expansion( of node: SwiftSyntax.AttributeSyntax, @@ -56,22 +106,16 @@ extension RECodable: ExtensionMacro { throw MacroError(text: "@Codable macro is only for `struct`, `class` or `enum`.") } - var codableExisted = false - if let inheritedType = declaration.inheritanceClause?.inheritedTypes, - inheritedType.contains(where: { $0.type.trimmedDescription == "Codable" }) { - codableExisted = true - } - var delegateExisted = false - if let inheritedType = declaration.inheritanceClause?.inheritedTypes, - inheritedType.contains(where: { $0.type.trimmedDescription == "ReerCodableDelegate" }) { - delegateExisted = true - } - if codableExisted && delegateExisted { return [] } - let extensionDecl: DeclSyntax = - """ - extension \(type.trimmed):\(raw: codableExisted ? "" : "Codable,") ReerCodableDelegate {} - """ - return [extensionDecl.cast(ExtensionDeclSyntax.self)] + let codableExisted = hasInheritedType(in: declaration) { $0 == "Codable" } + let delegateExisted = hasInheritedType(in: declaration) { $0 == "ReerCodableDelegate" } + let conformances = [ + codableExisted ? nil : "Codable", + delegateExisted ? nil : "ReerCodableDelegate" + ].compactMap(\.self) + guard let extensionDecl = makeConformanceExtension(for: type, declaration: declaration, conformances: conformances) else { + return [] + } + return [extensionDecl] } } @@ -166,24 +210,18 @@ extension REDecodable: ExtensionMacro { throw MacroError(text: "@Decodable macro is only for `struct`, `class` or `enum`.") } - var decodableExisted = false - if let inheritedTypeClause = declaration.inheritanceClause { - decodableExisted = inheritedTypeClause.inheritedTypes.contains { inheritedType in - let typeName = inheritedType.type.trimmedDescription - return typeName == "Decodable" || typeName == "Codable" - } + let decodableExisted = hasInheritedType(in: declaration) { + $0 == "Decodable" || $0 == "Codable" + } + let delegateExisted = hasInheritedType(in: declaration) { $0 == "ReerCodableDelegate" } + let conformances = [ + decodableExisted ? nil : "Decodable", + delegateExisted ? nil : "ReerCodableDelegate" + ].compactMap(\.self) + guard let extensionDecl = makeConformanceExtension(for: type, declaration: declaration, conformances: conformances) else { + return [] } - var delegateExisted = false - if let inheritedType = declaration.inheritanceClause?.inheritedTypes, - inheritedType.contains(where: { $0.type.trimmedDescription == "ReerCodableDelegate" }) { - delegateExisted = true - } - if decodableExisted && delegateExisted { return [] } - let extensionDecl: DeclSyntax = - """ - extension \(type.trimmed): \(raw: decodableExisted ? "" : "Decodable, ")ReerCodableDelegate {} - """ - return [extensionDecl.cast(ExtensionDeclSyntax.self)] + return [extensionDecl] } } @@ -271,24 +309,18 @@ extension REEncodable: ExtensionMacro { throw MacroError(text: "@Encodable macro is only for `struct`, `class` or `enum`.") } - var encodableExisted = false - if let inheritedTypeClause = declaration.inheritanceClause { - encodableExisted = inheritedTypeClause.inheritedTypes.contains { inheritedType in - let typeName = inheritedType.type.trimmedDescription - return typeName == "Encodable" || typeName == "Codable" - } + let encodableExisted = hasInheritedType(in: declaration) { + $0 == "Encodable" || $0 == "Codable" + } + let delegateExisted = hasInheritedType(in: declaration) { $0 == "ReerCodableDelegate" } + let conformances = [ + encodableExisted ? nil : "Encodable", + delegateExisted ? nil : "ReerCodableDelegate" + ].compactMap(\.self) + guard let extensionDecl = makeConformanceExtension(for: type, declaration: declaration, conformances: conformances) else { + return [] } - var delegateExisted = false - if let inheritedType = declaration.inheritanceClause?.inheritedTypes, - inheritedType.contains(where: { $0.type.trimmedDescription == "ReerCodableDelegate" }) { - delegateExisted = true - } - if encodableExisted && delegateExisted { return [] } - let extensionDecl: DeclSyntax = - """ - extension \(type.trimmed): \(raw: encodableExisted ? "" : "Encodable, ")ReerCodableDelegate {} - """ - return [extensionDecl.cast(ExtensionDeclSyntax.self)] + return [extensionDecl] } } diff --git a/Tests/ReerCodableTests/MacroExpandTests.swift b/Tests/ReerCodableTests/MacroExpandTests.swift index 1a8a797..39d87a7 100644 --- a/Tests/ReerCodableTests/MacroExpandTests.swift +++ b/Tests/ReerCodableTests/MacroExpandTests.swift @@ -12,6 +12,8 @@ import ReerCodableMacros let testMacros: [String: Macro.Type] = [ "Codable": RECodable.self, + "Decodable": REDecodable.self, + "Encodable": REEncodable.self, "InheritedCodable": InheritedCodable.self, "CodingKey": CodingKey.self, "EncodingKey": EncodingKey.self, @@ -101,6 +103,291 @@ final class ReerCodableTests: XCTestCase { throw XCTSkip("macros are only supported when running tests for the host platform") #endif } + + func testNonisolatedTypeGeneratesNonisolatedConformanceExtension() throws { + #if canImport(ReerCodableMacros) + assertMacroExpansion( + """ + @Codable + nonisolated struct User { + let id: Int + } + """, + expandedSource: """ + nonisolated struct User { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + nonisolated extension User: Codable, ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Decodable + nonisolated struct Incoming { + let id: Int + } + """, + expandedSource: """ + nonisolated struct Incoming { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + init( + id: Int + ) { + self.id = id + } + } + + nonisolated extension Incoming: Decodable, ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Encodable + nonisolated struct Outgoing { + let id: Int + } + """, + expandedSource: """ + nonisolated struct Outgoing { + let id: Int + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + nonisolated extension Outgoing: Encodable, ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Codable + nonisolated final class Session { + let id: Int + } + """, + expandedSource: """ + nonisolated final class Session { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + nonisolated extension Session: Codable, ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Codable + nonisolated struct DelegateBacked: ReerCodableDelegate { + let id: Int + } + """, + expandedSource: """ + nonisolated struct DelegateBacked: ReerCodableDelegate { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + nonisolated extension DelegateBacked: Codable { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Codable + nonisolated struct NonisolatedCodableBacked: nonisolated Codable { + let id: Int + } + """, + expandedSource: """ + nonisolated struct NonisolatedCodableBacked: nonisolated Codable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + nonisolated extension NonisolatedCodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Codable + struct PreconcurrencyCodableBacked: @preconcurrency Codable { + let id: Int + } + """, + expandedSource: """ + struct PreconcurrencyCodableBacked: @preconcurrency Codable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + extension PreconcurrencyCodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Codable + @MainActor + struct MainActorCodableBacked: @MainActor Codable { + let id: Int + } + """, + expandedSource: """ + @MainActor + struct MainActorCodableBacked: @MainActor Codable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + extension MainActorCodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } func testEnumMacro() throws { #if canImport(ReerCodableMacros) diff --git a/Tests/ReerCodableTests/NonisolatedCodableTests.swift b/Tests/ReerCodableTests/NonisolatedCodableTests.swift new file mode 100644 index 0000000..d12977c --- /dev/null +++ b/Tests/ReerCodableTests/NonisolatedCodableTests.swift @@ -0,0 +1,78 @@ +@testable import ReerCodable +import Foundation +import Testing + +@Codable +nonisolated struct NonisolatedCodableStruct: Equatable { + let id: Int + let name: String +} + +@Codable +nonisolated final class NonisolatedCodableClass { + let id: Int + let name: String +} + +@Codable +nonisolated enum NonisolatedCodableEnum: String { + case active + case paused +} + +@Decodable +nonisolated struct NonisolatedDecodableStruct: Equatable { + let id: Int +} + +@Encodable +nonisolated struct NonisolatedEncodableStruct { + let id: Int +} + +private func roundTrip(_ value: T, as type: T.Type = T.self) throws -> T { + let data = try JSONEncoder().encode(value) + return try JSONDecoder().decode(T.self, from: data) +} + +extension TestReerCodable { + @Test + func nonisolatedCodableStructRoundTrips() throws { + let original = NonisolatedCodableStruct(id: 1, name: "Phoenix") + let decoded = try roundTrip(original) + + #expect(decoded == original) + } + + @Test + func nonisolatedCodableClassRoundTrips() throws { + let original = NonisolatedCodableClass(id: 2, name: "Reer") + let decoded = try roundTrip(original) + + #expect(decoded.id == original.id) + #expect(decoded.name == original.name) + } + + @Test + func nonisolatedCodableEnumRoundTrips() throws { + let decoded = try roundTrip(NonisolatedCodableEnum.active) + + #expect(decoded == .active) + } + + @Test + func nonisolatedDecodableStructDecodes() throws { + let data = #"{"id":3}"#.data(using: .utf8)! + let decoded = try JSONDecoder().decode(NonisolatedDecodableStruct.self, from: data) + + #expect(decoded.id == 3) + } + + @Test + func nonisolatedEncodableStructEncodes() throws { + let data = try JSONEncoder().encode(NonisolatedEncodableStruct(id: 4)) + let dict = data.stringAnyDictionary + + #expect(dict.int("id") == 4) + } +} From 6079df459fc768b339bb8917f85ae5afe5016fc8 Mon Sep 17 00:00:00 2001 From: phoenix Date: Mon, 8 Jun 2026 20:56:21 +0800 Subject: [PATCH 2/2] fix: cover concurrency annotated conformances Co-authored-by: Cursor --- .../xcschemes/ReerCodable.xcscheme | 10 + Package.swift | 9 + .../MacroImplementations/CodableImpl.swift | 20 +- .../MainActorIsolationTests.swift | 63 ++++++ Tests/ReerCodableTests/MacroExpandTests.swift | 198 ++++++++++++++++++ 5 files changed, 297 insertions(+), 3 deletions(-) create mode 100644 Tests/ReerCodableMainActorIsolationTests/MainActorIsolationTests.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ReerCodable.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ReerCodable.xcscheme index 4f52d6a..91da376 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/ReerCodable.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ReerCodable.xcscheme @@ -40,6 +40,16 @@ ReferencedContainer = "container:"> + + + + String { - type.trimmedDescription +private func normalizedInheritedTypeNames(_ type: TypeSyntax) -> [String] { + let normalized = type.trimmedDescription .split(whereSeparator: \.isWhitespace) .filter { token in token != "nonisolated" && !token.hasPrefix("@") } .joined(separator: " ") + + return normalized + .split(separator: "&") + .map { component in + component + .split(whereSeparator: \.isWhitespace) + .joined(separator: " ") + .split(separator: ".") + .last + .map(String.init) ?? "" + } + .filter { !$0.isEmpty } } private func hasInheritedType( @@ -54,7 +66,9 @@ private func hasInheritedType( matching predicate: (String) -> Bool ) -> Bool { guard let inheritedTypes = declaration.inheritanceClause?.inheritedTypes else { return false } - return inheritedTypes.contains { predicate(normalizedInheritedTypeName($0.type)) } + return inheritedTypes.contains { inheritedType in + normalizedInheritedTypeNames(inheritedType.type).contains(where: predicate) + } } private func hasNonisolatedModifier(_ declaration: some DeclGroupSyntax) -> Bool { diff --git a/Tests/ReerCodableMainActorIsolationTests/MainActorIsolationTests.swift b/Tests/ReerCodableMainActorIsolationTests/MainActorIsolationTests.swift new file mode 100644 index 0000000..57c843b --- /dev/null +++ b/Tests/ReerCodableMainActorIsolationTests/MainActorIsolationTests.swift @@ -0,0 +1,63 @@ +import Foundation +import ReerCodable +import Testing + +@Codable +nonisolated struct MainActorDefaultCodableModel: Sendable { + let id: Int + let name: String +} + +@Decodable +nonisolated struct MainActorDefaultDecodableModel: Sendable { + let id: Int +} + +@Encodable +nonisolated struct MainActorDefaultEncodableModel: Sendable { + let id: Int +} + +private func acceptsCodableSendable(_ type: T.Type) {} + +private func acceptsDecodableSendable(_ type: T.Type) {} + +private func acceptsEncodableSendable(_ type: T.Type) {} + +@Test +func nonisolatedCodableConformanceSatisfiesNonisolatedGenericRequirement() throws { + acceptsCodableSendable(MainActorDefaultCodableModel.self) + + let original = MainActorDefaultCodableModel(id: 1, name: "Reer") + let data = try JSONEncoder().encode(original) + let decoded = try JSONDecoder().decode(MainActorDefaultCodableModel.self, from: data) + + #expect(decoded.id == original.id) + #expect(decoded.name == original.name) +} + +@Test +func nonisolatedDecodableConformanceSatisfiesNonisolatedGenericRequirement() throws { + acceptsDecodableSendable(MainActorDefaultDecodableModel.self) + + let data = #"{"id":2}"#.data(using: .utf8)! + let decoded = try JSONDecoder().decode(MainActorDefaultDecodableModel.self, from: data) + + #expect(decoded.id == 2) +} + +@Test +func nonisolatedEncodableConformanceSatisfiesNonisolatedGenericRequirement() throws { + acceptsEncodableSendable(MainActorDefaultEncodableModel.self) + + let data = try JSONEncoder().encode(MainActorDefaultEncodableModel(id: 3)) + let value = try #require(data.stringAnyDictionary?["id"] as? Int) + + #expect(value == 3) +} + +private extension Data { + var stringAnyDictionary: [String: Any]? { + try? JSONSerialization.jsonObject(with: self) as? [String: Any] + } +} diff --git a/Tests/ReerCodableTests/MacroExpandTests.swift b/Tests/ReerCodableTests/MacroExpandTests.swift index 39d87a7..335c4fb 100644 --- a/Tests/ReerCodableTests/MacroExpandTests.swift +++ b/Tests/ReerCodableTests/MacroExpandTests.swift @@ -310,6 +310,204 @@ final class ReerCodableTests: XCTestCase { macros: testMacros, indentationWidth: .spaces(4) ) + assertMacroExpansion( + """ + @Codable + struct CompositionCodableBacked: Codable & Sendable { + let id: Int + } + """, + expandedSource: """ + struct CompositionCodableBacked: Codable & Sendable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + extension CompositionCodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Codable + struct CommaSeparatedCodableBacked: Codable, Sendable { + let id: Int + } + """, + expandedSource: """ + struct CommaSeparatedCodableBacked: Codable, Sendable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + extension CommaSeparatedCodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Codable + nonisolated struct NonisolatedCompositionCodableBacked: nonisolated Codable & Sendable { + let id: Int + } + """, + expandedSource: """ + nonisolated struct NonisolatedCompositionCodableBacked: nonisolated Codable & Sendable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + nonisolated extension NonisolatedCompositionCodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Decodable + struct CompositionDecodableBacked: Decodable & Sendable { + let id: Int + } + """, + expandedSource: """ + struct CompositionDecodableBacked: Decodable & Sendable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + init( + id: Int + ) { + self.id = id + } + } + + extension CompositionDecodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Decodable + struct CommaSeparatedDecodableBacked: Decodable, Sendable { + let id: Int + } + """, + expandedSource: """ + struct CommaSeparatedDecodableBacked: Decodable, Sendable { + let id: Int + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: AnyCodingKey.self) + self.id = try container.decode(Int.self, forKey: AnyCodingKey("id", false)) + try self.didDecode(from: decoder) + } + + init( + id: Int + ) { + self.id = id + } + } + + extension CommaSeparatedDecodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) + assertMacroExpansion( + """ + @Encodable + struct CommaSeparatedEncodableBacked: Encodable, Sendable { + let id: Int + } + """, + expandedSource: """ + struct CommaSeparatedEncodableBacked: Encodable, Sendable { + let id: Int + + func encode(to encoder: any Encoder) throws { + try self.willEncode(to: encoder) + var container = encoder.container(keyedBy: AnyCodingKey.self) + try container.encode(value: self.id, key: AnyCodingKey("id", false), treatDotAsNested: true) + } + + init( + id: Int + ) { + self.id = id + } + } + + extension CommaSeparatedEncodableBacked: ReerCodableDelegate { + } + """, + macros: testMacros, + indentationWidth: .spaces(4) + ) assertMacroExpansion( """ @Codable