diff --git a/Sources/SafeDICore/Generators/ScopeGenerator.swift b/Sources/SafeDICore/Generators/ScopeGenerator.swift index 00e78c5f..4e37a84b 100644 --- a/Sources/SafeDICore/Generators/ScopeGenerator.swift +++ b/Sources/SafeDICore/Generators/ScopeGenerator.swift @@ -18,8 +18,10 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. +import Collections + /// A model capable of generating code for a scope’s dependency tree. -actor ScopeGenerator { +actor ScopeGenerator: CustomStringConvertible, Hashable { // MARK: Initialization @@ -39,8 +41,10 @@ actor ScopeGenerator { .map(\.property) ) ) + description = instantiable.concreteInstantiableType.asSource } else { scopeData = .root(instantiable: instantiable) + description = instantiable.concreteInstantiableType.asSource } self.property = property self.propertiesToGenerate = propertiesToGenerate @@ -78,11 +82,29 @@ actor ScopeGenerator { ) { scopeData = .alias(property: property, fulfillingProperty: fulfillingProperty) requiredReceivedProperties = [fulfillingProperty] + description = property.asSource propertiesToGenerate = [] propertiesToDeclare = [] self.property = property } + // MARK: CustomStringConvertible + + let description: String + + // MARK: Equatable + + static func == (lhs: ScopeGenerator, rhs: ScopeGenerator) -> Bool { + lhs.scopeData == rhs.scopeData + } + + // MARK: Hashable + + nonisolated + func hash(into hasher: inout Hasher) { + hasher.combine(scopeData) + } + // MARK: Internal func generateCode(leadingWhitespace: String = "") async throws -> String { @@ -202,7 +224,7 @@ actor ScopeGenerator { // MARK: Private - private enum ScopeData { + private enum ScopeData: Hashable { case root(instantiable: Instantiable) case property( instantiable: Instantiable, @@ -236,42 +258,75 @@ actor ScopeGenerator { private var generateCodeTask: Task? private var orderedPropertiesToGenerate: [ScopeGenerator] { - guard var orderedPropertiesToGenerate = List(self.propertiesToGenerate) else { return [] } - for propertyToGenerate in orderedPropertiesToGenerate { - let hasDependenciesGeneratedByCurrentScope = !propertyToGenerate - .requiredReceivedProperties - .isDisjoint(with: propertiesToDeclare) - guard hasDependenciesGeneratedByCurrentScope else { - // This property does not have received dependencies generated by this scope, therefore its ordering is irrelevant. - continue + get throws { + // Step 1: Build dependency graph + var scopeToIncomingDependencyScopes = [ScopeGenerator: [ScopeGenerator]]() + var scopeToOutgoingUnresolvedDependenciesCount = OrderedDictionary() + + for scope in propertiesToGenerate { + // Find dependencies. + for otherScope in propertiesToGenerate { + if + let property = otherScope.property, + scope.requiredReceivedProperties.contains(property) + { + scopeToOutgoingUnresolvedDependenciesCount[scope, default: 0] += 1 + scopeToIncomingDependencyScopes[otherScope, default: []] += [scope] + } + } } - if let lastDependencyToGenerate = propertyToGenerate - // Ignore our own property. - .dropFirst() - // Reverse the list to find the last property we depend on first. - .reversed() - .first(where: { futurePropertyToGenerate in - if let futureProperty = futurePropertyToGenerate.property, - propertyToGenerate - .requiredReceivedProperties - .contains(futureProperty) - { true } else { false } - }) - { - // We depend on (at least) one item further ahead in the list! - // Make sure we are created after our dependencies. - lastDependencyToGenerate.insert(propertyToGenerate.value) - if let head = propertyToGenerate.remove() { - orderedPropertiesToGenerate = head + + // Step 2: Topological Sort + var nextPropertiesToGenerate = propertiesToGenerate + .filter { scopeToOutgoingUnresolvedDependenciesCount[$0] == nil } + + var orderedPropertiesToGenerate = [ScopeGenerator]() + while !nextPropertiesToGenerate.isEmpty { + let nextProperty = nextPropertiesToGenerate.removeFirst() + orderedPropertiesToGenerate.append(nextProperty) + + if let incomingDependencyScopes = scopeToIncomingDependencyScopes[nextProperty] { + for dependantScope in incomingDependencyScopes { + if let dependenciesCount = scopeToOutgoingUnresolvedDependenciesCount[dependantScope] { + let newDependenciesCount = dependenciesCount - 1 + scopeToOutgoingUnresolvedDependenciesCount[dependantScope] = newDependenciesCount + + if newDependenciesCount == 0 { + nextPropertiesToGenerate.append(dependantScope) + } + } + } + } + } + + // Step 3: Check for cycle + if !scopeToOutgoingUnresolvedDependenciesCount.isEmpty { + func detectCycle(from scope: ScopeGenerator, stack: OrderedSet = []) throws { + var stack = stack + if let property = scope.property { + if stack.contains(property) { + throw GenerationError.dependencyCycleDetected( + stack.drop(while: { $0 != property }) + [property], + scope: self + ) + } + stack.append(property) + for nextScope in scopeToIncomingDependencyScopes[scope, default: []] { + try detectCycle(from: nextScope, stack: stack) + } + } + } + for scope in scopeToOutgoingUnresolvedDependenciesCount.keys { + try detectCycle(from: scope) } } + return orderedPropertiesToGenerate } - return orderedPropertiesToGenerate.map(\.value) } private func generateProperties(leadingMemberWhitespace: String) async throws -> [String] { var generatedProperties = [String]() - for childGenerator in orderedPropertiesToGenerate { + for childGenerator in try orderedPropertiesToGenerate { generatedProperties.append( try await childGenerator .generateCode(leadingWhitespace: leadingMemberWhitespace) @@ -284,11 +339,17 @@ actor ScopeGenerator { private enum GenerationError: Error, CustomStringConvertible { case forwardingInstantiatorGenericDoesNotMatch(property: Property, instantiable: Instantiable) + case dependencyCycleDetected(any Collection, scope: ScopeGenerator) var description: String { switch self { case let .forwardingInstantiatorGenericDoesNotMatch(property, instantiable): "Property `\(property.asSource)` on \(instantiable.concreteInstantiableType.asSource) incorrectly configured. Property should instead be of type `\(Dependency.forwardingInstantiatorType)<\(instantiable.concreteInstantiableType.asSource).ForwardedArguments, \(property.typeDescription.asInstantiatedType.asSource)>`." + case let .dependencyCycleDetected(properties, scope): + """ + Dependency cycle detected on \(scope)! + \(properties.map(\.asSource).joined(separator: " -> ")) + """ } } } diff --git a/Sources/SafeDICore/Models/List.swift b/Sources/SafeDICore/Models/List.swift deleted file mode 100644 index 7b9f2721..00000000 --- a/Sources/SafeDICore/Models/List.swift +++ /dev/null @@ -1,99 +0,0 @@ -// Distributed under the MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -@dynamicMemberLookup -public final class List: Sequence { - - // MARK: Initialization - - public init(value: Element, previous: List? = nil, next: List? = nil) { - self.value = value - self.previous = previous - self.next = next - previous?.next = self - next?.previous = self - } - - public convenience init?(_ collection: some Collection) { - guard let first = collection.first else { return nil } - self.init(first: first, remaining: collection.dropFirst()) - } - - public convenience init(first: Element, remaining: some Collection) { - self.init(value: first) - var next = self - for element in remaining { - next = next.insert(element) - } - } - - // MARK: Public - - public let value: Element - - public subscript(dynamicMember keyPath: KeyPath) -> T { - value[keyPath: keyPath] - } - - /// Inserts the value after the current element. - /// - Parameter value: The value to insert into the list. - /// - Returns: The inserted element in the list. - @discardableResult - public func insert(_ value: Element) -> List { - List( - value: value, - previous: self, - next: next - ) - } - - /// Removes the receiver from the list. - /// - Returns: The next element in the list, if the current element is the head of the list. - @discardableResult - public func remove() -> List? { - previous?.next = next - next?.previous = previous - return previous == nil ? next : nil - } - - // MARK: Sequence - - public func makeIterator() -> Iterator { - Iterator(node: self) - } - - public struct Iterator: IteratorProtocol { - init(node: List?) { - self.node = node - } - - public mutating func next() -> List? { - defer { node = node?.next } - return node - } - - private var node: List? - } - - // MARK: Private - - private var next: List? = nil - private var previous: List? = nil -} diff --git a/Tests/SafeDICoreTests/ListTests.swift b/Tests/SafeDICoreTests/ListTests.swift deleted file mode 100644 index 5ad9b081..00000000 --- a/Tests/SafeDICoreTests/ListTests.swift +++ /dev/null @@ -1,167 +0,0 @@ -// Distributed under the MIT License -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import XCTest - -@testable import SafeDICore - -final class ListTests: XCTestCase { - - func test_nonEmptyInit_createsListFromCollection() throws { - XCTAssertEqual( - try XCTUnwrap(List([1, 2, 3, 4, 5])).map(\.value), - [1, 2, 3, 4, 5] - ) - } - - func test_insert_onFirstElementInList_insertsElementAfterFirstElement() throws { - let systemUnderTest = try XCTUnwrap(List([1, 3, 4, 5])) - systemUnderTest.insert(2) - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 4, 5] - ) - } - - func test_insert_onLaterItemsInList_insertsElementAfterCurrentElement() { - let systemUnderTest = List(value: 1) - var last = systemUnderTest.insert(2) - last = last.insert(3) - last = last.insert(4) - last = last.insert(5) - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 4, 5] - ) - } - - func test_remove_onFirstElementInList_removesFirstElementAndReturnsNewFirstElement() throws { - let systemUnderTest = try XCTUnwrap(List([1, 2, 3, 4, 5])) - XCTAssertEqual( - systemUnderTest.remove()?.map(\.value), - [2, 3, 4, 5] - ) - } - - func test_remove_onItemThatWasInsertedAfterListCreation_removesItem() { - let systemUnderTest = List(value: 1) - let two = systemUnderTest.insert(2) - let four = two.insert(4) - four.insert(5) - two.insert(3).remove() - - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 4, 5] - ) - } - - func test_remove_onItemBeforeItemInsertedAfterListCreation_removesItem() { - let systemUnderTest = List(value: 1) - let two = systemUnderTest.insert(2) - let four = two.insert(4) - four.insert(5) - two.insert(3) - two.remove() - - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 3, 4, 5] - ) - } - - func test_remove_onItemAfterItemInsertedAfterListCreation_removesItem() { - let systemUnderTest = List(value: 1) - let two = systemUnderTest.insert(2) - let four = two.insert(4) - four.insert(5) - two.insert(3) - four.remove() - - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 5] - ) - } - - func test_remove_onLaterItemsInList_removesElementAndReturnsNil() { - let systemUnderTest = List(value: 1) - let two = systemUnderTest.insert(2) - let three = two.insert(3) - let four = three.insert(4) - four.insert(5) - XCTAssertNil(four.remove()) - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 5] - ) - } - - func test_remove_onLastInList_removesElement() throws { - let systemUnderTest = try XCTUnwrap(List([1, 2, 3, 4])) - let lastElement = systemUnderTest.insert(5) - lastElement.remove() - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 4] - ) - } - - func test_insert_andThenRemoveItemBeforeInsertion_insertsAndThenRemoves() { - let systemUnderTest = List(value: 1) - let two = systemUnderTest.insert(2) - let three = two.insert(3) - let four = three.insert(4) - let secondFour = four.insert(4) - secondFour.insert(5) - four.remove() - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 4, 5] - ) - } - - func test_insert_andThenRemoveItem_insertsAndThenRemoves() { - let systemUnderTest = List(value: 1) - let two = systemUnderTest.insert(2) - let three = two.insert(3) - let four = three.insert(4) - four.remove() - three.insert(5) - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 5] - ) - } - - func test_insert_andThenRemoveItemAfterInsertion_insertsAndThenRemoves() { - let systemUnderTest = List(value: 1) - let two = systemUnderTest.insert(2) - let three = two.insert(3) - let four = three.insert(4) - four.insert(5) - four.remove() - three.insert(4) - XCTAssertEqual( - systemUnderTest.map(\.value), - [1, 2, 3, 4, 5] - ) - } -} diff --git a/Tests/SafeDIToolTests/SafeDIToolTests.swift b/Tests/SafeDIToolTests/SafeDIToolTests.swift index 6983bf58..c562c337 100644 --- a/Tests/SafeDIToolTests/SafeDIToolTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolTests.swift @@ -1285,16 +1285,16 @@ final class SafeDIToolTests: XCTestCase { extension Root { public convenience init() { let greatGrandchild = GreatGrandchild() - let childB = { - let grandchildBA = GrandchildBA(greatGrandchild: greatGrandchild) - let grandchildBB = GrandchildBB(greatGrandchild: greatGrandchild) - return ChildB(grandchildBA: grandchildBA, grandchildBB: grandchildBB) - }() let childA = { let grandchildAA = GrandchildAA(greatGrandchild: greatGrandchild) let grandchildAB = GrandchildAB(greatGrandchild: greatGrandchild) return ChildA(grandchildAA: grandchildAA, grandchildAB: grandchildAB) }() + let childB = { + let grandchildBA = GrandchildBA(greatGrandchild: greatGrandchild) + let grandchildBB = GrandchildBB(greatGrandchild: greatGrandchild) + return ChildB(grandchildBA: grandchildBA, grandchildBB: grandchildBB) + }() self.init(childA: childA, childB: childB, greatGrandchild: greatGrandchild) } } @@ -1384,14 +1384,14 @@ final class SafeDIToolTests: XCTestCase { public convenience init() { let childA = { let greatGrandchild = GreatGrandchild() - let grandchildAB = GrandchildAB(greatGrandchild: greatGrandchild) let grandchildAA = GrandchildAA(greatGrandchild: greatGrandchild) + let grandchildAB = GrandchildAB(greatGrandchild: greatGrandchild) return ChildA(grandchildAA: grandchildAA, grandchildAB: grandchildAB, greatGrandchild: greatGrandchild) }() let childB = { let greatGrandchild = GreatGrandchild() - let grandchildBB = GrandchildBB(greatGrandchild: greatGrandchild) let grandchildBA = GrandchildBA(greatGrandchild: greatGrandchild) + let grandchildBB = GrandchildBB(greatGrandchild: greatGrandchild) return ChildB(grandchildBA: grandchildBA, grandchildBB: grandchildBB, greatGrandchild: greatGrandchild) }() self.init(childA: childA, childB: childB) @@ -2411,7 +2411,6 @@ final class SafeDIToolTests: XCTestCase { extension RootViewController { public convenience init() { let networkService: NetworkService = DefaultNetworkService() - let authService: AuthService = DefaultAuthService(networkService: networkService) let loggedInViewControllerBuilder = ForwardingInstantiator { userManager in let profileViewControllerBuilder = Instantiator { let userVendor: UserVendor = userManager @@ -2422,6 +2421,7 @@ final class SafeDIToolTests: XCTestCase { } return LoggedInViewController(userManager: userManager, profileViewControllerBuilder: profileViewControllerBuilder) } + let authService: AuthService = DefaultAuthService(networkService: networkService) self.init(authService: authService, networkService: networkService, loggedInViewControllerBuilder: loggedInViewControllerBuilder) } } @@ -2863,7 +2863,6 @@ final class SafeDIToolTests: XCTestCase { extension RootViewController { public convenience init() { let networkService: NetworkService = DefaultNetworkService() - let authService: AuthService = DefaultAuthService(networkService: networkService) let loggedInViewControllerBuilder = ForwardingInstantiator { userManager in let userVendor: UserVendor = userManager let profileViewControllerBuilder = Instantiator { @@ -2874,6 +2873,7 @@ final class SafeDIToolTests: XCTestCase { } return LoggedInViewController(userManager: userManager, userVendor: userVendor, profileViewControllerBuilder: profileViewControllerBuilder) } + let authService: AuthService = DefaultAuthService(networkService: networkService) self.init(authService: authService, networkService: networkService, loggedInViewControllerBuilder: loggedInViewControllerBuilder) } } @@ -3005,7 +3005,6 @@ final class SafeDIToolTests: XCTestCase { extension RootViewController { public convenience init() { let networkService: NetworkService = DefaultNetworkService() - let authService: AuthService = DefaultAuthService(networkService: networkService) let loggedInViewControllerBuilder = ForwardingInstantiator { userManager in let profileViewControllerBuilder = Instantiator { let editProfileViewControllerBuilder = Instantiator { @@ -3016,6 +3015,7 @@ final class SafeDIToolTests: XCTestCase { } return LoggedInViewController(userManager: userManager, profileViewControllerBuilder: profileViewControllerBuilder) } + let authService: AuthService = DefaultAuthService(networkService: networkService) self.init(authService: authService, networkService: networkService, loggedInViewControllerBuilder: loggedInViewControllerBuilder) } } @@ -3085,6 +3085,338 @@ final class SafeDIToolTests: XCTestCase { ) } + func test_run_writesConvenienceExtensionOnRootOfTree_whenRootHasLotsOfDependenciesThatDependOnOneAnother() async throws { + let output = try await executeSystemUnderTest( + swiftFileContent: [ + """ + @Instantiable + public final class Root { + @Instantiated + let a: A + @Instantiated + let b: B + @Instantiated + let c: C + @Instantiated + let d: D + @Instantiated + let e: E + @Instantiated + let f: F + @Instantiated + let g: G + @Instantiated + let h: H + @Instantiated + let i: I + @Instantiated + let j: J + @Instantiated + let k: K + @Instantiated + let l: L + @Instantiated + let m: M + @Instantiated + let n: N + @Instantiated + let o: O + @Instantiated + let p: P + @Instantiated + let q: Q + @Instantiated + let r: R + @Instantiated + let s: S + @Instantiated + let t: T + @Instantiated + let u: U + @Instantiated + let v: V + @Instantiated + let w: W + @Instantiated + let x: X + @Instantiated + let y: Y + @Instantiated + let z: Z + } + """, + """ + @Instantiable + public final class A { + @Received + let x: X + } + """, + """ + @Instantiable + public final class B { + @Received + let a: A + @Received + let d: D + } + """, + """ + @Instantiable + public final class C { + @Received + let u: U + } + """, + """ + @Instantiable + public final class D {} + """, + """ + @Instantiable + public final class E { + @Received + let g: G + } + """, + """ + @Instantiable + public final class F { + @Received + let a: A + @Received + let x: X + } + """, + """ + @Instantiable + public final class G {} + """, + """ + @Instantiable + public final class H { + @Received + let i: I + } + """, + """ + @Instantiable + public final class I { + @Received + let f: F + } + """, + """ + @Instantiable + public final class J {} + """, + """ + @Instantiable + public final class K { + @Received + let i: I + @Received + let t: T + } + """, + """ + @Instantiable + public final class L { + @Received + let o: O + @Received + let v: V + @Received + let e: E + } + """, + """ + @Instantiable + public final class M { + @Received + let e: E + } + """, + """ + @Instantiable + public final class N { + @Received + let o: O + @Received + let p: P + @Received + let e: E + } + """, + """ + @Instantiable + public final class O { + @Received + let m: M + } + """, + """ + @Instantiable + public final class P { + @Received + let i: I + @Received + let x: X + } + """, + """ + @Instantiable + public final class Q { + @Received + let u: U + } + """, + """ + @Instantiable + public final class R { + @Received + let a: A + @Received + let m: M + @Received + let o: O + @Received + let n: N + @Received + let e: E + } + """, + """ + @Instantiable + public final class S { + @Received + let a: A + @Received + let t: T + @Received + let o: O + @Received + let r: R + } + """, + """ + @Instantiable + public final class T { + @Received + let e: E + @Received + let n: N + } + """, + """ + @Instantiable + public final class U { + @Received + let p: P + } + """, + """ + @Instantiable + public final class V { + @Received + let a: A + @Received + let t: T + @Received + let o: O + @Received + let f: F + @Received + let c: C + @Received + let i: I + @Received + let d: D + } + """, + """ + @Instantiable + public final class W { + @Received + let a: A + @Received + let x: X + } + """, + """ + @Instantiable + public final class X {} + """, + """ + @Instantiable + public final class Y { + @Received + let u: U + @Received + let p: P + } + """, + """ + @Instantiable + public final class Z { + @Received + let e: E + @Received + let p: P + @Received + let l: L + @Received + let i: I + @Received + let n: N + } + """, + ], + buildDependencyTreeOutput: true + ) + + XCTAssertEqual( + try XCTUnwrap(output.dependencyTree), + """ + // This file was generated by the SafeDIGenerateDependencyTree build tool plugin. + // Any modifications made to this file will be overwritten on subsequent builds. + // Please refrain from editing this file directly. + + extension Root { + public convenience init() { + let d = D() + let g = G() + let j = J() + let x = X() + let e = E(g: g) + let a = A(x: x) + let m = M(e: e) + let b = B(a: a, d: d) + let f = F(a: a, x: x) + let w = W(a: a, x: x) + let o = O(m: m) + let i = I(f: f) + let h = H(i: i) + let p = P(i: i, x: x) + let n = N(o: o, p: p, e: e) + let u = U(p: p) + let r = R(a: a, m: m, o: o, n: n, e: e) + let t = T(e: e, n: n) + let c = C(u: u) + let q = Q(u: u) + let y = Y(u: u, p: p) + let k = K(i: i, t: t) + let s = S(a: a, t: t, o: o, r: r) + let v = V(a: a, t: t, o: o, f: f, c: c, i: i, d: d) + let l = L(o: o, v: v, e: e) + let z = Z(e: e, p: p, l: l, i: i, n: n) + self.init(a: a, b: b, c: c, d: d, e: e, f: f, g: g, h: h, i: i, j: j, k: k, l: l, m: m, n: n, o: o, p: p, q: q, r: r, s: s, t: t, u: u, v: v, w: w, x: x, y: y, z: z) + } + } + """ + ) + } + // MARK: Error Tests func test_run_onCodeWithPropertyWithUnknownFulfilledType_throwsError() async { @@ -3738,7 +4070,101 @@ final class SafeDIToolTests: XCTestCase { ) } } - + + func test_run_onCodeWithCircularReceivedDependencies_throwsError() async { + await assertThrowsError( + """ + Dependency cycle detected on Root! + a: A -> c: C -> b: B -> a: A + """ + ) { + try await executeSystemUnderTest( + swiftFileContent: [ + """ + @Instantiable + public struct Root { + @Instantiated + private let a: A + @Instantiated + private let b: B + @Instantiated + private let c: C + } + """, + """ + @Instantiable + public struct A { + @Received + private let b: B + } + """, + """ + @Instantiable + public struct B { + @Received + private let c: C + } + """, + """ + @Instantiable + public struct C { + @Received + private let a: A + } + """, + ], + buildDependencyTreeOutput: true + ) + } + } + + func test_run_onCodeWithCircularReceivedRenamedDependencies_throwsError() async { + await assertThrowsError( + """ + Dependency cycle detected on A! + b: B -> renamedB: B -> c: C -> b: B + """ + ) { + try await executeSystemUnderTest( + swiftFileContent: [ + """ + @Instantiable + public struct Root { + @Instantiated + private let a: A + } + """, + """ + @Instantiable + public struct A { + @Instantiated + private let b: B + @Received(fulfilledByDependencyNamed: "b", ofType: B.self) + private let renamedB: B + @Instantiated + private let c: C + } + """, + """ + @Instantiable + public struct B { + @Received + private let c: C + } + """, + """ + @Instantiable + public struct C { + @Received + private let renamedB: B + } + """, + ], + buildDependencyTreeOutput: true + ) + } + } + func test_run_onCodeWithForwardingInstantiatorSecondGeneric_hasNoForwardedProperty_throwsError() async throws { await assertThrowsError( """