Skip to content

Commit d3ac85c

Browse files
committed
fix: conforms KeyPath to Sendable if Root and Value conform to.
Ensures compatibility with Swift 6 projects.
1 parent ef4070d commit d3ac85c

2 files changed

Lines changed: 67 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
extension KeyPath: @retroactive @unchecked Sendable where Root: Sendable, Value: Sendable {}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import Testing
2+
@testable import ThemeKit
3+
4+
@Suite("KeyPath+Sendable")
5+
struct KeyPathSendableTests {
6+
7+
// MARK: - Compile-time Sendable conformance
8+
9+
/// Helper that requires its argument to be Sendable.
10+
/// If KeyPath did not conform to Sendable, calls to this function
11+
/// with KeyPath arguments would fail to compile.
12+
private func requireSendable<T: Sendable>(_ value: T) -> T { value }
13+
14+
@Test func keyPath_withSendableRootAndValue_conformsToSendable() {
15+
// SendableRoot and SendableValue are both Sendable,
16+
// so KeyPath<SendableRoot, String> should satisfy Sendable.
17+
let kp = requireSendable(\SendableRoot.name)
18+
#expect(kp == \SendableRoot.name)
19+
}
20+
21+
@Test func keyPath_nestedProperty_conformsToSendable() {
22+
let kp = requireSendable(\SendableRoot.count)
23+
#expect(kp == \SendableRoot.count)
24+
}
25+
26+
// MARK: - Cross-actor sending
27+
28+
private actor Collector {
29+
func read<Root: Sendable, Value: Sendable>(
30+
_ keyPath: KeyPath<Root, Value>, from root: Root
31+
) -> Value {
32+
root[keyPath: keyPath]
33+
}
34+
}
35+
36+
@Test func keyPath_canBeSentAcrossActorBoundary() async {
37+
let collector = Collector()
38+
let root = SendableRoot(name: "theme", count: 42)
39+
40+
let name = await collector.read(\.name, from: root)
41+
let count = await collector.read(\.count, from: root)
42+
43+
#expect(name == "theme")
44+
#expect(count == 42)
45+
}
46+
47+
// MARK: - Sendable closure capture
48+
49+
@Test func keyPath_canBeCapturedInSendableClosure() async {
50+
let kp: KeyPath<SendableRoot, String> = \.name
51+
let root = SendableRoot(name: "captured", count: 0)
52+
53+
let result = await Task { @Sendable in
54+
root[keyPath: kp]
55+
}.value
56+
57+
#expect(result == "captured")
58+
}
59+
}
60+
61+
// MARK: - Test helpers
62+
63+
private struct SendableRoot: Sendable {
64+
let name: String
65+
let count: Int
66+
}

0 commit comments

Comments
 (0)