From 19b77191ff56464e76c310ec9adaaedf4d58b09c Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 5 Jan 2024 10:41:55 -0800 Subject: [PATCH 01/11] wip --- Sources/Dependencies/Dependency.swift | 18 ++++++++++++++++++ Sources/Dependencies/DependencyValues.swift | 11 +++++++++++ 2 files changed, 29 insertions(+) diff --git a/Sources/Dependencies/Dependency.swift b/Sources/Dependencies/Dependency.swift index 73ca3d76..15d1f41c 100644 --- a/Sources/Dependencies/Dependency.swift +++ b/Sources/Dependencies/Dependency.swift @@ -1,3 +1,21 @@ +struct TrivialHashable: Hashable, DependencyKey { + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(T.self)) + } + static func == (lhs: Self, rhs: Self) -> Bool { + true + } + static var liveValue: T { + fatalError() + } +} + +extension Dependency { + init(_ type: Value.Type) { + self.init(\DependencyValues.[TrivialHashable()]) + } +} + /// A property wrapper for accessing dependencies. /// /// All dependencies are stored in ``DependencyValues`` and one uses this property wrapper to gain diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index a73374f3..9fc5b8ad 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -100,6 +100,17 @@ public struct DependencyValues: Sendable { #endif } + subscript( + key: TrivialHashable + ) -> Value { + get { + self.storage[ObjectIdentifier(TrivialHashable.self)]!.base as! Value + } + set { + self.storage[ObjectIdentifier(TrivialHashable.self)] = AnySendable(newValue) + } + } + /// Accesses the dependency value associated with a custom key. /// /// This subscript is typically only used when adding a computed property to ``DependencyValues`` From cc167a2d9565091a300b9a3b308220038cdc4a14 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 5 Jan 2024 11:10:21 -0800 Subject: [PATCH 02/11] wip --- Sources/Dependencies/Dependency.swift | 22 ++++--------------- Sources/Dependencies/DependencyValues.swift | 17 +++++++------- Tests/DependenciesTests/DependencyTests.swift | 12 ++++++++++ 3 files changed, 24 insertions(+), 27 deletions(-) diff --git a/Sources/Dependencies/Dependency.swift b/Sources/Dependencies/Dependency.swift index 15d1f41c..b41e836a 100644 --- a/Sources/Dependencies/Dependency.swift +++ b/Sources/Dependencies/Dependency.swift @@ -1,21 +1,3 @@ -struct TrivialHashable: Hashable, DependencyKey { - func hash(into hasher: inout Hasher) { - hasher.combine(ObjectIdentifier(T.self)) - } - static func == (lhs: Self, rhs: Self) -> Bool { - true - } - static var liveValue: T { - fatalError() - } -} - -extension Dependency { - init(_ type: Value.Type) { - self.init(\DependencyValues.[TrivialHashable()]) - } -} - /// A property wrapper for accessing dependencies. /// /// All dependencies are stored in ``DependencyValues`` and one uses this property wrapper to gain @@ -115,6 +97,10 @@ public struct Dependency: @unchecked Sendable, _HasInitialValues { self.line = line } + public init(_ type: Value.Type) { + self.init(\DependencyValues.[ObjectIdentifier(Value.self)]) + } + /// The current value of the dependency property. public var wrappedValue: Value { #if DEBUG diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 9fc5b8ad..dae6b56b 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -100,15 +100,14 @@ public struct DependencyValues: Sendable { #endif } - subscript( - key: TrivialHashable - ) -> Value { - get { - self.storage[ObjectIdentifier(TrivialHashable.self)]!.base as! Value - } - set { - self.storage[ObjectIdentifier(TrivialHashable.self)] = AnySendable(newValue) - } + public subscript(type: Value.Type) -> Value { + get { self[ObjectIdentifier(type)] } + set { self[ObjectIdentifier(type)] = newValue } + } + + subscript(id: ObjectIdentifier) -> Value { + get { self.storage[id]?.base as! Value } + set { self.storage[id] = AnySendable(newValue) } } /// Accesses the dependency value associated with a custom key. diff --git a/Tests/DependenciesTests/DependencyTests.swift b/Tests/DependenciesTests/DependencyTests.swift index 33861440..357976f9 100644 --- a/Tests/DependenciesTests/DependencyTests.swift +++ b/Tests/DependenciesTests/DependencyTests.swift @@ -206,6 +206,18 @@ final class DependencyTests: XCTestCase { XCTAssertEqual(user1.id, UUID(0)) XCTAssertEqual(user2.id, UUID(1)) } + + func testOptionalDependency() { + @Dependency(Int?.self) var int + + XCTAssertNil(int) + + withDependencies { + $0[Int?.self] = 42 + } operation: { + XCTAssertEqual(int, 42) + } + } } private class Model { From bd590a6d5832fe8cac36a23236206914ea918c05 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Fri, 5 Jan 2024 13:18:56 -0800 Subject: [PATCH 03/11] wip --- Sources/Dependencies/DependencyValues.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index dae6b56b..3ae87deb 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -100,14 +100,24 @@ public struct DependencyValues: Sendable { #endif } + @_disfavoredOverload public subscript(type: Value.Type) -> Value { - get { self[ObjectIdentifier(type)] } - set { self[ObjectIdentifier(type)] = newValue } + get { + self[ObjectIdentifier(type)] + } + set { + self[ObjectIdentifier(type)] = newValue + } } subscript(id: ObjectIdentifier) -> Value { - get { self.storage[id]?.base as! Value } - set { self.storage[id] = AnySendable(newValue) } + get { + // TODO: better error messaging for type mismatch + self.storage[id]?.base as! Value + } + set { + self.storage[id] = AnySendable(newValue) + } } /// Accesses the dependency value associated with a custom key. From 932558a87af41dff1a9fac62c7e6f6a3763f2f1f Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 5 Jan 2024 14:00:45 -0800 Subject: [PATCH 04/11] wip --- Sources/Dependencies/Dependency.swift | 55 ++++++++++++++++++- Sources/Dependencies/DependencyValues.swift | 20 +------ Tests/DependenciesTests/DependencyTests.swift | 11 ++-- 3 files changed, 63 insertions(+), 23 deletions(-) diff --git a/Sources/Dependencies/Dependency.swift b/Sources/Dependencies/Dependency.swift index b41e836a..fe6e666f 100644 --- a/Sources/Dependencies/Dependency.swift +++ b/Sources/Dependencies/Dependency.swift @@ -97,8 +97,40 @@ public struct Dependency: @unchecked Sendable, _HasInitialValues { self.line = line } - public init(_ type: Value.Type) { - self.init(\DependencyValues.[ObjectIdentifier(Value.self)]) + /// Creates a dependency property to read a dependency object. + /// + /// Don't call this initializer directly. Instead, declare a property with the `Dependency` + /// property wrapper, and provide the dependency key of the value that the property should + /// reflect. + /// + /// For example, given a dependency key: + /// + /// ```swift + /// final class Settings: DependencyKey { + /// static let liveValue = Settings() + /// + /// // ... + /// } + /// ``` + /// + /// One can access the dependency using this property wrapper: + /// + /// ```swift + /// final class FeatureModel: ObservableObject { + /// @Dependency(Settings.self) var settings + /// + /// // ... + /// } + /// ``` + /// + /// - Parameter key: A dependency key to a specific resulting value. + public init( + _ key: Key.Type, + file: StaticString = #file, + fileID: StaticString = #fileID, + line: UInt = #line + ) where Key.Value == Value { + self.init(\DependencyValues.[\Key.self], file: file, fileID: fileID, line: line) } /// The current value of the dependency property. @@ -123,6 +155,25 @@ public struct Dependency: @unchecked Sendable, _HasInitialValues { } } +extension Dependency: Equatable where Value: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.wrappedValue == rhs.wrappedValue + } +} + +extension Dependency: Hashable where Value: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.wrappedValue) + } +} + +fileprivate extension DependencyValues { + subscript(_: KeyPath) -> Key.Value { + get { self[Key.self] } + set { self[Key.self] = newValue } + } +} + protocol _HasInitialValues { var initialValues: DependencyValues { get } } diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 3ae87deb..c8cdd930 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -101,23 +101,9 @@ public struct DependencyValues: Sendable { } @_disfavoredOverload - public subscript(type: Value.Type) -> Value { - get { - self[ObjectIdentifier(type)] - } - set { - self[ObjectIdentifier(type)] = newValue - } - } - - subscript(id: ObjectIdentifier) -> Value { - get { - // TODO: better error messaging for type mismatch - self.storage[id]?.base as! Value - } - set { - self.storage[id] = AnySendable(newValue) - } + public subscript(type: Key.Type) -> Key.Value { + get { self[type] } + set { self[type] = newValue } } /// Accesses the dependency value associated with a custom key. diff --git a/Tests/DependenciesTests/DependencyTests.swift b/Tests/DependenciesTests/DependencyTests.swift index 357976f9..1db17d21 100644 --- a/Tests/DependenciesTests/DependencyTests.swift +++ b/Tests/DependenciesTests/DependencyTests.swift @@ -207,13 +207,16 @@ final class DependencyTests: XCTestCase { XCTAssertEqual(user2.id, UUID(1)) } - func testOptionalDependency() { - @Dependency(Int?.self) var int + func testDependencyType() { + struct MyDependency: TestDependencyKey { + static var testValue: Int { 0 } + } + @Dependency(MyDependency.self) var int - XCTAssertNil(int) + XCTAssertEqual(int, 0) withDependencies { - $0[Int?.self] = 42 + $0[MyDependency.self] = 42 } operation: { XCTAssertEqual(int, 42) } From 7a7d7de7673ccd12d876447a86ab7c26ef37b57e Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 8 Jan 2024 15:58:46 -0800 Subject: [PATCH 05/11] wip --- Sources/Dependencies/DependencyKey.swift | 4 ++-- Sources/Dependencies/DependencyValues.swift | 6 +++++- Tests/DependenciesTests/DependencyValuesTests.swift | 6 +----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Sources/Dependencies/DependencyKey.swift b/Sources/Dependencies/DependencyKey.swift index 417b939d..70378981 100644 --- a/Sources/Dependencies/DependencyKey.swift +++ b/Sources/Dependencies/DependencyKey.swift @@ -58,7 +58,7 @@ /// implementation module. /// /// See the article for more information. -public protocol DependencyKey: TestDependencyKey { +public protocol DependencyKey: TestDependencyKey { /// The live value for the dependency key. /// /// This is the value used by default when running the application in a simulator or on a device. @@ -118,7 +118,7 @@ public protocol DependencyKey: TestDependencyKey { /// return a default value suitable for Xcode previews, or the ``testValue``, if left unimplemented. /// /// See ``DependencyKey`` to define a static, default value for the live application. -public protocol TestDependencyKey { +public protocol TestDependencyKey { /// The associated type representing the type of the dependency key's value. associatedtype Value: Sendable = Self diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index c8cdd930..e09b200e 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -344,7 +344,11 @@ private final class CachedValues: @unchecked Sendable { ) } #endif - return Key.testValue + let value = Key.testValue + if !DependencyValues.isSetting { + self.cached[cacheKey] = AnySendable(value) + } + return value } self.cached[cacheKey] = AnySendable(value) diff --git a/Tests/DependenciesTests/DependencyValuesTests.swift b/Tests/DependenciesTests/DependencyValuesTests.swift index 2df246c8..0f22102b 100644 --- a/Tests/DependenciesTests/DependencyValuesTests.swift +++ b/Tests/DependenciesTests/DependencyValuesTests.swift @@ -177,11 +177,7 @@ final class DependencyValuesTests: XCTestCase { #endif XCTAssertEqual(reuseClient.count(), 0) reuseClient.setCount(-42) - XCTAssertEqual( - reuseClient.count(), - 0, - "Don't cache dependency when using a test value in a live context" - ) + XCTAssertEqual(reuseClient.count(), -42) } XCTAssertEqual(reuseClient.count(), 42) From 74c8be15db468c1e2faa4e4ed816d0e4839d734d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Jan 2024 11:34:45 -0800 Subject: [PATCH 06/11] wip --- Sources/Dependencies/Dependency.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/Sources/Dependencies/Dependency.swift b/Sources/Dependencies/Dependency.swift index fe6e666f..209cba4c 100644 --- a/Sources/Dependencies/Dependency.swift +++ b/Sources/Dependencies/Dependency.swift @@ -155,18 +155,6 @@ public struct Dependency: @unchecked Sendable, _HasInitialValues { } } -extension Dependency: Equatable where Value: Equatable { - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.wrappedValue == rhs.wrappedValue - } -} - -extension Dependency: Hashable where Value: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(self.wrappedValue) - } -} - fileprivate extension DependencyValues { subscript(_: KeyPath) -> Key.Value { get { self[Key.self] } From 1b7d13f4a0db6f9f4b66db9bb1e516321993e023 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 9 Jan 2024 13:18:39 -0800 Subject: [PATCH 07/11] wip --- Sources/Dependencies/DependencyValues.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index e09b200e..4b9d9566 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -325,10 +325,14 @@ private final class CachedValues: @unchecked Sendable { """ ) + var argument: String { + "\(function)" == "subscript(_:)" ? "\(typeName(Key.self)).self" : "\\.\(function)" + } + runtimeWarn( """ - "@Dependency(\\.\(function))" has no live implementation, but was accessed from a \ - live context. + "@Dependency(\(argument))" has no live implementation, but was accessed from a live \ + context. \(dependencyDescription) From f0929a4e7612bfaec65cf7aeb5900ca2f208dc47 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 22 Jan 2024 10:52:23 -0800 Subject: [PATCH 08/11] wip --- Sources/Dependencies/DependencyKey.swift | 25 ++++++---- Sources/Dependencies/DependencyValues.swift | 6 +-- .../DependencyKeyTests.swift | 35 ++++++++++++++ .../DependencyValuesTests.swift | 46 ++++++++++++++++--- 4 files changed, 93 insertions(+), 19 deletions(-) diff --git a/Sources/Dependencies/DependencyKey.swift b/Sources/Dependencies/DependencyKey.swift index 70378981..07995e50 100644 --- a/Sources/Dependencies/DependencyKey.swift +++ b/Sources/Dependencies/DependencyKey.swift @@ -210,25 +210,30 @@ extension DependencyKey { \(typeName(Value.self)) """ ) - let dependencyName = + + let (argument, override) = DependencyValues.currentDependency.name - .map { "@Dependency(\\.\($0))" } - ?? "A dependency" + .map { + "\($0)" == "subscript(_:)" + ? ("@Dependency(\(typeName(Self.self)).self)", "'\(typeName(Self.self)).self'") + : ("@Dependency(\\.\($0))", "'\($0)'") + } + ?? ("A dependency", "the dependency") + XCTFail( """ - \(dependencyName) has no test implementation, but was accessed from a test context: + \(argument) has no test implementation, but was accessed from a test context: \(dependencyDescription) Dependencies registered with the library are not allowed to use their default, live \ implementations when run from tests. - To fix, override \ - \(DependencyValues.currentDependency.name.map { "'\($0)'" } ?? "the dependency") with a \ - test value. If you are using the Composable Architecture, mutate the 'dependencies' \ - property on your 'TestStore'. Otherwise, use 'withDependencies' to define a scope for the \ - override. If you'd like to provide a default value for all tests, implement the \ - 'testValue' requirement of the 'DependencyKey' protocol. + To fix, override \(override) with a test value. If you are using the \ + Composable Architecture, mutate the 'dependencies' property on your 'TestStore'. \ + Otherwise, use 'withDependencies' to define a scope for the override. If you'd like to \ + provide a default value for all tests, implement the 'testValue' requirement of the \ + 'DependencyKey' protocol. """ ) #endif diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 4b9d9566..bd5e3e0c 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -331,15 +331,15 @@ private final class CachedValues: @unchecked Sendable { runtimeWarn( """ - "@Dependency(\(argument))" has no live implementation, but was accessed from a live \ + @Dependency(\(argument)) has no live implementation, but was accessed from a live \ context. \(dependencyDescription) - Every dependency registered with the library must conform to "DependencyKey", and \ + Every dependency registered with the library must conform to 'DependencyKey', and \ that conformance must be visible to the running application. - To fix, make sure that "\(typeName(Key.self))" conforms to "DependencyKey" by \ + To fix, make sure that '\(typeName(Key.self))' conforms to 'DependencyKey' by \ providing a live implementation of your dependency, and make sure that the \ conformance is linked with this current application. """, diff --git a/Tests/DependenciesTests/DependencyKeyTests.swift b/Tests/DependenciesTests/DependencyKeyTests.swift index a23fc427..796a4545 100644 --- a/Tests/DependenciesTests/DependencyKeyTests.swift +++ b/Tests/DependenciesTests/DependencyKeyTests.swift @@ -154,6 +154,41 @@ final class DependencyKeyTests: XCTestCase { } #endif } + + func testDependencyKeyCascading_ImplementOnlyLive_NamedType() { + #if DEBUG && !os(Linux) && !os(WASI) && !os(Windows) + withDependencies { + $0.context = .test + } operation: { + @Dependency(LiveKey.self) var missingTestDependency: Int + let line = #line - 1 + XCTExpectFailure { + XCTAssertEqual(42, missingTestDependency) + } issueMatcher: { issue in + issue.compactDescription == """ + @Dependency(LiveKey.self) has no test implementation, but was accessed from a test \ + context: + + Location: + DependenciesTests/DependencyKeyTests.swift:\(line) + Key: + LiveKey + Value: + Int + + Dependencies registered with the library are not allowed to use their default, live \ + implementations when run from tests. + + To fix, override 'LiveKey.self' with a test value. If you are using the \ + Composable Architecture, mutate the 'dependencies' property on your 'TestStore'. \ + Otherwise, use 'withDependencies' to define a scope for the override. If you'd \ + like to provide a default value for all tests, implement the 'testValue' requirement \ + of the 'DependencyKey' protocol. + """ + } + } + #endif + } } private enum LiveKey: DependencyKey { diff --git a/Tests/DependenciesTests/DependencyValuesTests.swift b/Tests/DependenciesTests/DependencyValuesTests.swift index 4bb7c354..e76f7db4 100644 --- a/Tests/DependenciesTests/DependencyValuesTests.swift +++ b/Tests/DependenciesTests/DependencyValuesTests.swift @@ -27,7 +27,7 @@ final class DependencyValuesTests: XCTestCase { } } issueMatcher: { $0.compactDescription == """ - "@Dependency(\\.missingLiveDependency)" has no live implementation, but was accessed \ + @Dependency(\\.missingLiveDependency) has no live implementation, but was accessed \ from a live context. Location: @@ -37,10 +37,44 @@ final class DependencyValuesTests: XCTestCase { Value: Int - Every dependency registered with the library must conform to "DependencyKey", and that \ + Every dependency registered with the library must conform to 'DependencyKey', and that \ conformance must be visible to the running application. - To fix, make sure that "TestKey" conforms to "DependencyKey" by providing a live \ + To fix, make sure that 'TestKey' conforms to 'DependencyKey' by providing a live \ + implementation of your dependency, and make sure that the conformance is linked with \ + this current application. + """ + } + #endif + } + + func testMissingLiveValue_Type() { + #if DEBUG && !os(Linux) && !os(WASI) && !os(Windows) + var line = 0 + XCTExpectFailure { + withDependencies { + $0.context = .live + } operation: { + line = #line + 1 + @Dependency(TestKey.self) var missingLiveDependency: Int + _ = missingLiveDependency + } + } issueMatcher: { + $0.compactDescription == """ + @Dependency(TestKey.self) has no live implementation, but was accessed from a live \ + context. + + Location: + DependenciesTests/DependencyValuesTests.swift:\(line) + Key: + TestKey + Value: + Int + + Every dependency registered with the library must conform to 'DependencyKey', and that \ + conformance must be visible to the running application. + + To fix, make sure that 'TestKey' conforms to 'DependencyKey' by providing a live \ implementation of your dependency, and make sure that the conformance is linked with \ this current application. """ @@ -169,7 +203,7 @@ final class DependencyValuesTests: XCTestCase { XCTExpectFailure { $0.compactDescription.contains( """ - @Dependency(\\.reuseClient)" has no live implementation, but was accessed from a \ + @Dependency(\\.reuseClient) has no live implementation, but was accessed from a \ live context. """ ) @@ -201,8 +235,8 @@ final class DependencyValuesTests: XCTestCase { XCTExpectFailure { $0.compactDescription.contains( """ - @Dependency(\\.reuseClient)" has no live implementation, but was accessed from a live \ - context. + @Dependency(\\.reuseClient) has no live implementation, but was accessed from a \ + live context. """ ) } From fa2f50ceeece7d566dcc934e91da4c38721c594d Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 22 Jan 2024 10:58:55 -0800 Subject: [PATCH 09/11] wip --- Tests/DependenciesTests/DependencyTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/DependenciesTests/DependencyTests.swift b/Tests/DependenciesTests/DependencyTests.swift index 1db17d21..4ccc03eb 100644 --- a/Tests/DependenciesTests/DependencyTests.swift +++ b/Tests/DependenciesTests/DependencyTests.swift @@ -211,7 +211,7 @@ final class DependencyTests: XCTestCase { struct MyDependency: TestDependencyKey { static var testValue: Int { 0 } } - @Dependency(MyDependency.self) var int + @Dependency(MyDependency.self) var int: Int XCTAssertEqual(int, 0) From 826d2d96abe96902b9c7129943a06692f29a1779 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 22 Jan 2024 11:19:30 -0800 Subject: [PATCH 10/11] fixes --- Sources/Dependencies/Dependency.swift | 24 +++- Sources/Dependencies/DependencyValues.swift | 139 ++++++++++---------- 2 files changed, 91 insertions(+), 72 deletions(-) diff --git a/Sources/Dependencies/Dependency.swift b/Sources/Dependencies/Dependency.swift index 209cba4c..ffb329dd 100644 --- a/Sources/Dependencies/Dependency.swift +++ b/Sources/Dependencies/Dependency.swift @@ -130,7 +130,12 @@ public struct Dependency: @unchecked Sendable, _HasInitialValues { fileID: StaticString = #fileID, line: UInt = #line ) where Key.Value == Value { - self.init(\DependencyValues.[\Key.self], file: file, fileID: fileID, line: line) + self.init( + \DependencyValues.[HashableType(file: file, line: line)], + file: file, + fileID: fileID, + line: line + ) } /// The current value of the dependency property. @@ -155,10 +160,21 @@ public struct Dependency: @unchecked Sendable, _HasInitialValues { } } +private struct HashableType: Hashable { + let file: StaticString + let line: UInt + static func == (lhs: Self, rhs: Self) -> Bool { + true + } + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(T.self)) + } +} + fileprivate extension DependencyValues { - subscript(_: KeyPath) -> Key.Value { - get { self[Key.self] } - set { self[Key.self] = newValue } + subscript(key: HashableType) -> Key.Value { + get { self[Key.self, file: key.file, line: key.line] } + set { self[Key.self, file: key.file, line: key.line] = newValue } } } diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index bd5e3e0c..0a585101 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -1,4 +1,5 @@ import Foundation +import XCTestDynamicOverlay /// A collection of dependencies that is globally available. /// @@ -129,9 +130,9 @@ public struct DependencyValues: Sendable { /// property wrapper. public subscript( key: Key.Type, - file: StaticString = #file, - function: StaticString = #function, - line: UInt = #line + file file: StaticString = #file, + function function: StaticString = #function, + line line: UInt = #line ) -> Key.Value where Key.Value: Sendable { get { guard let base = self.storage[ObjectIdentifier(key)]?.base, @@ -279,87 +280,89 @@ private final class CachedValues: @unchecked Sendable { function: StaticString = #function, line: UInt = #line ) -> Key.Value where Key.Value: Sendable { - self.lock.lock() - defer { self.lock.unlock() } - - let cacheKey = CacheKey(id: ObjectIdentifier(key), context: context) - guard let base = self.cached[cacheKey]?.base, let value = base as? Key.Value - else { - let value: Key.Value? - switch context { - case .live: - value = _liveValue(key) as? Key.Value - case .preview: - value = Key.previewValue - case .test: - value = Key.testValue - } + XCTFailContext.$current.withValue(XCTFailContext(file: file, line: line)) { + self.lock.lock() + defer { self.lock.unlock() } - guard let value = value + let cacheKey = CacheKey(id: ObjectIdentifier(key), context: context) + guard let base = self.cached[cacheKey]?.base, let value = base as? Key.Value else { - #if DEBUG - if !DependencyValues.isSetting { - var dependencyDescription = "" - if let fileID = DependencyValues.currentDependency.fileID, - let line = DependencyValues.currentDependency.line - { - dependencyDescription.append( - """ - Location: - \(fileID):\(line) + let value: Key.Value? + switch context { + case .live: + value = _liveValue(key) as? Key.Value + case .preview: + value = Key.previewValue + case .test: + value = Key.testValue + } - """ + guard let value = value + else { + #if DEBUG + if !DependencyValues.isSetting { + var dependencyDescription = "" + if let fileID = DependencyValues.currentDependency.fileID, + let line = DependencyValues.currentDependency.line + { + dependencyDescription.append( + """ + Location: + \(fileID):\(line) + + """ + ) + } + dependencyDescription.append( + Key.self == Key.Value.self + ? """ + Dependency: + \(typeName(Key.Value.self)) + """ + : """ + Key: + \(typeName(Key.self)) + Value: + \(typeName(Key.Value.self)) + """ ) - } - dependencyDescription.append( - Key.self == Key.Value.self - ? """ - Dependency: - \(typeName(Key.Value.self)) - """ - : """ - Key: - \(typeName(Key.self)) - Value: - \(typeName(Key.Value.self)) - """ - ) - var argument: String { - "\(function)" == "subscript(_:)" ? "\(typeName(Key.self)).self" : "\\.\(function)" - } + var argument: String { + "\(function)" == "subscript(_:)" ? "\(typeName(Key.self)).self" : "\\.\(function)" + } - runtimeWarn( - """ - @Dependency(\(argument)) has no live implementation, but was accessed from a live \ - context. + runtimeWarn( + """ + @Dependency(\(argument)) has no live implementation, but was accessed from a live \ + context. - \(dependencyDescription) + \(dependencyDescription) - Every dependency registered with the library must conform to 'DependencyKey', and \ - that conformance must be visible to the running application. + Every dependency registered with the library must conform to 'DependencyKey', and \ + that conformance must be visible to the running application. - To fix, make sure that '\(typeName(Key.self))' conforms to 'DependencyKey' by \ - providing a live implementation of your dependency, and make sure that the \ - conformance is linked with this current application. - """, - file: DependencyValues.currentDependency.file ?? file, - line: DependencyValues.currentDependency.line ?? line - ) + To fix, make sure that '\(typeName(Key.self))' conforms to 'DependencyKey' by \ + providing a live implementation of your dependency, and make sure that the \ + conformance is linked with this current application. + """, + file: DependencyValues.currentDependency.file ?? file, + line: DependencyValues.currentDependency.line ?? line + ) + } + #endif + let value = Key.testValue + if !DependencyValues.isSetting { + self.cached[cacheKey] = AnySendable(value) } - #endif - let value = Key.testValue - if !DependencyValues.isSetting { - self.cached[cacheKey] = AnySendable(value) + return value } + + self.cached[cacheKey] = AnySendable(value) return value } - self.cached[cacheKey] = AnySendable(value) return value } - - return value } } From 941aba775f5cc0272dded2992c7dafafd1d3882b Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Mon, 22 Jan 2024 11:24:03 -0800 Subject: [PATCH 11/11] wip --- Sources/Dependencies/DependencyValues.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index 0a585101..7a99ca88 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -82,9 +82,7 @@ import XCTestDynamicOverlay /// Read the article for more information. public struct DependencyValues: Sendable { @TaskLocal public static var _current = Self() - #if DEBUG - @TaskLocal static var isSetting = false - #endif + @TaskLocal static var isSetting = false @TaskLocal static var currentDependency = CurrentDependency() fileprivate var cachedValues = CachedValues()