From 5685efb8a0926a96f6fc2fce890e88ad4d8eaba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=9A=A8=EC=84=B1?= Date: Mon, 6 Jul 2026 19:11:47 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix(concurrency):=20Swift=206=20language=20?= =?UTF-8?q?mode=20=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=B6=88=EB=B3=80=20?= =?UTF-8?q?=EA=B3=B5=EC=9C=A0=20=EC=83=81=ED=83=9C=20=EC=96=B4=EB=85=B8?= =?UTF-8?q?=ED=85=8C=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 불변·1회 설정 static 상태에 nonisolated(unsafe) 적용: static ISO8601DateFormatter, 매크로 타입 서술자(protocolType, macroType) - 불변 값/메타타입/에러 홀더에 @unchecked Sendable 적용: LosslessValueCodable, OptionalLosslessValueCodable(조건부), PolymorphicCodableError, ArrayDecodingError, DictionaryDecodingError, UnknownNovelValueError --- .../Strategy/ISO8601WithFractionalSecondsStrategy.swift | 4 +++- .../BetterCodable/LosslessValue/LosslessValue.swift | 3 ++- .../BetterCodable/LosslessValue/OptionalLosslessValue.swift | 3 ++- .../PolymorphicCodable/Error/PolymorphicCodableError.swift | 3 +++ Sources/KarrotCodableKit/Resilient/ArrayDecodingError.swift | 5 +++++ .../KarrotCodableKit/Resilient/DictionaryDecodingError.swift | 5 +++++ Sources/KarrotCodableKit/Resilient/ErrorReporting.swift | 3 +++ .../UnnestedPolymorphicCodableMacro.swift | 4 ++-- .../UnnestedPolymorphicDecodableMacro.swift | 4 ++-- 9 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/ISO8601WithFractionalSecondsStrategy.swift b/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/ISO8601WithFractionalSecondsStrategy.swift index d243220..127788f 100644 --- a/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/ISO8601WithFractionalSecondsStrategy.swift +++ b/Sources/KarrotCodableKit/BetterCodable/DateValue/Strategy/ISO8601WithFractionalSecondsStrategy.swift @@ -16,7 +16,9 @@ import Foundation /// representing 39 minutes and 57 seconds after the 16th hour of December 19th, 1996 with an offset of -08:00 from UTC /// (Pacific Standard Time). public struct ISO8601WithFractionalSecondsStrategy: DateValueCodableStrategy { - private static let formatter: ISO8601DateFormatter = { + // `ISO8601DateFormatter` is configured once and never mutated, so concurrent reads are safe. + // `nonisolated(unsafe)` acknowledges this until the SDK annotates the type `Sendable`. + private nonisolated(unsafe) static let formatter: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter diff --git a/Sources/KarrotCodableKit/BetterCodable/LosslessValue/LosslessValue.swift b/Sources/KarrotCodableKit/BetterCodable/LosslessValue/LosslessValue.swift index 893917f..97187e3 100644 --- a/Sources/KarrotCodableKit/BetterCodable/LosslessValue/LosslessValue.swift +++ b/Sources/KarrotCodableKit/BetterCodable/LosslessValue/LosslessValue.swift @@ -101,7 +101,8 @@ extension LosslessValueCodable: Hashable where Strategy.Value: Hashable { } } -extension LosslessValueCodable: Sendable where Strategy.Value: Sendable {} +// `type` is an immutable metatype (no shared mutable state); the where-clause keeps `wrappedValue` safe. +extension LosslessValueCodable: @unchecked Sendable where Strategy.Value: Sendable {} public struct LosslessDefaultStrategy: LosslessDecodingStrategy { public static var losslessDecodableTypes: [(Decoder) -> LosslessStringCodable?] { diff --git a/Sources/KarrotCodableKit/BetterCodable/LosslessValue/OptionalLosslessValue.swift b/Sources/KarrotCodableKit/BetterCodable/LosslessValue/OptionalLosslessValue.swift index 4bbff1f..6527515 100644 --- a/Sources/KarrotCodableKit/BetterCodable/LosslessValue/OptionalLosslessValue.swift +++ b/Sources/KarrotCodableKit/BetterCodable/LosslessValue/OptionalLosslessValue.swift @@ -120,7 +120,8 @@ extension OptionalLosslessValueCodable: Hashable where Strategy.Value: Hashable } } -extension OptionalLosslessValueCodable: Sendable where Strategy.Value: Sendable {} +// `type` is an immutable metatype (no shared mutable state); the where-clause keeps `wrappedValue` safe. +extension OptionalLosslessValueCodable: @unchecked Sendable where Strategy.Value: Sendable {} /// Decodes Optional Codable values into their respective preferred types. /// diff --git a/Sources/KarrotCodableKit/PolymorphicCodable/Error/PolymorphicCodableError.swift b/Sources/KarrotCodableKit/PolymorphicCodable/Error/PolymorphicCodableError.swift index e93dfca..d65b77f 100644 --- a/Sources/KarrotCodableKit/PolymorphicCodable/Error/PolymorphicCodableError.swift +++ b/Sources/KarrotCodableKit/PolymorphicCodable/Error/PolymorphicCodableError.swift @@ -34,3 +34,6 @@ public enum PolymorphicCodableError: LocalizedError { } } } + +// Immutable error value; the `decoded` payload is only read for diagnostics. +extension PolymorphicCodableError: @unchecked Sendable {} diff --git a/Sources/KarrotCodableKit/Resilient/ArrayDecodingError.swift b/Sources/KarrotCodableKit/Resilient/ArrayDecodingError.swift index 214cba0..96803fe 100644 --- a/Sources/KarrotCodableKit/Resilient/ArrayDecodingError.swift +++ b/Sources/KarrotCodableKit/Resilient/ArrayDecodingError.swift @@ -40,3 +40,8 @@ extension ResilientDecodingOutcome { } } #endif + +#if DEBUG +// DEBUG-only immutable diagnostic holder; `results` is read-only after init. +extension ResilientDecodingOutcome.ArrayDecodingError: @unchecked Sendable {} +#endif diff --git a/Sources/KarrotCodableKit/Resilient/DictionaryDecodingError.swift b/Sources/KarrotCodableKit/Resilient/DictionaryDecodingError.swift index 3b9928f..48ef4ab 100644 --- a/Sources/KarrotCodableKit/Resilient/DictionaryDecodingError.swift +++ b/Sources/KarrotCodableKit/Resilient/DictionaryDecodingError.swift @@ -38,3 +38,8 @@ extension ResilientDecodingOutcome { } } #endif + +#if DEBUG +// DEBUG-only immutable diagnostic holder; `results` is read-only after init. +extension ResilientDecodingOutcome.DictionaryDecodingError: @unchecked Sendable {} +#endif diff --git a/Sources/KarrotCodableKit/Resilient/ErrorReporting.swift b/Sources/KarrotCodableKit/Resilient/ErrorReporting.swift index 580decb..eb34ee7 100644 --- a/Sources/KarrotCodableKit/Resilient/ErrorReporting.swift +++ b/Sources/KarrotCodableKit/Resilient/ErrorReporting.swift @@ -236,3 +236,6 @@ public struct UnknownNovelValueError: Error { self.novelValue = novelValue } } + +// Immutable; `novelValue` is captured once and only read for diagnostics. +extension UnknownNovelValueError: @unchecked Sendable {} diff --git a/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicCodableMacro.swift b/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicCodableMacro.swift index e05f2d3..3efa1f4 100644 --- a/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicCodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicCodableMacro.swift @@ -12,8 +12,8 @@ import SwiftSyntaxBuilder import SwiftSyntaxMacros public enum UnnestedPolymorphicCodableMacro: MemberMacro, UnnestedPolymorphicMacroType { - public static let protocolType = PolymorphicExtensionFactory.PolymorphicProtocolType.codable - public static let macroType = UnnestedPolymorphicCodeGenerator.MacroType.codable + public nonisolated(unsafe) static let protocolType = PolymorphicExtensionFactory.PolymorphicProtocolType.codable + public nonisolated(unsafe) static let macroType = UnnestedPolymorphicCodeGenerator.MacroType.codable public static let macroName = "UnnestedPolymorphicCodable" public static func expansion( diff --git a/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicDecodableMacro.swift b/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicDecodableMacro.swift index edd0655..6df11a4 100644 --- a/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicDecodableMacro.swift +++ b/Sources/KarrotCodableKitMacros/UnnestedPolymorphicCodableMacro/UnnestedPolymorphicDecodableMacro.swift @@ -12,8 +12,8 @@ import SwiftSyntaxBuilder import SwiftSyntaxMacros public enum UnnestedPolymorphicDecodableMacro: MemberMacro, UnnestedPolymorphicMacroType { - public static let protocolType = PolymorphicExtensionFactory.PolymorphicProtocolType.decodable - public static let macroType = UnnestedPolymorphicCodeGenerator.MacroType.decodable + public nonisolated(unsafe) static let protocolType = PolymorphicExtensionFactory.PolymorphicProtocolType.decodable + public nonisolated(unsafe) static let macroType = UnnestedPolymorphicCodeGenerator.MacroType.decodable public static let macroName = "UnnestedPolymorphicDecodable" public static func expansion( From 2c38148174bcd9afe5aeac2a0c4d885edd67c404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=ED=9A=A8=EC=84=B1?= Date: Mon, 6 Jul 2026 19:15:40 +0900 Subject: [PATCH 2/2] =?UTF-8?q?ci:=20Swift=206=20language=20mode=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EA=B2=8C=EC=9D=B4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit swift build -Xswiftc -swift-version -Xswiftc 6 를 실행하는 CI 잡을 추가해 Swift 6 언어 모드 에러 발생 시 실패하도록 한다. --- .github/workflows/ci.yml | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 181ab37..8ca86b0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,4 +39,16 @@ jobs: uses: actions/checkout@v4 - name: Run Swift Macro Compatibility Check - uses: Matejkob/swift-macro-compatibility-check@v1 \ No newline at end of file + uses: Matejkob/swift-macro-compatibility-check@v1 + + swift6-mode: + name: Swift 6 Language Mode + runs-on: macos-15 + steps: + - uses: actions/checkout@v4 + + - name: Select Xcode 16.4 + run: sudo xcode-select -s /Applications/Xcode_16.4.app + + - name: Build in Swift 6 language mode + run: swift build -Xswiftc -swift-version -Xswiftc 6 \ No newline at end of file