diff --git a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift index b0f63ffa..48537b06 100644 --- a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift +++ b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift @@ -215,7 +215,6 @@ public final class DependencyTreeGenerator { // Populate the propertiesToGenerate on each scope. for scope in Set(typeDescriptionToScopeMap.values) { - var propertiesToGenerate = [Scope.PropertyToGenerate]() for dependency in scope.instantiable.dependencies { switch dependency.source { case .instantiated: @@ -238,12 +237,12 @@ public final class DependencyTreeGenerator { case .constant, .instantiator: break } - propertiesToGenerate.append(.instantiated( + scope.propertiesToGenerate.append(.instantiated( dependency.property, instantiatedScope )) case let .aliased(fulfillingProperty): - propertiesToGenerate.append(.aliased( + scope.propertiesToGenerate.append(.aliased( dependency.property, fulfilledBy: fulfillingProperty )) @@ -251,7 +250,6 @@ public final class DependencyTreeGenerator { continue } } - scope.propertiesToGenerate.append(contentsOf: propertiesToGenerate) } return typeDescriptionToScopeMap } diff --git a/Sources/SafeDICore/Generators/ScopeGenerator.swift b/Sources/SafeDICore/Generators/ScopeGenerator.swift index 837587b0..288f2efc 100644 --- a/Sources/SafeDICore/Generators/ScopeGenerator.swift +++ b/Sources/SafeDICore/Generators/ScopeGenerator.swift @@ -245,16 +245,43 @@ actor ScopeGenerator { private var generateCodeTask: Task? private func generateProperties(leadingMemberWhitespace: String) async throws -> [String] { - var generatedProperties = [String]() - let orderedPropertiesToGenerate = propertiesToGenerate.sorted(by: { lhs, rhs in - guard let lhsProperty = lhs.property else { - return true + guard var orderedPropertiesToGenerate = List(propertiesToGenerate) else { return [] } + let propertiesToGenerate = Set(propertiesToGenerate.compactMap(\.property)) + for propertyToGenerateNode in orderedPropertiesToGenerate { + let hasDependenciesGeneratedByCurrentScope = !propertyToGenerateNode + .value + .requiredReceivedProperties + .isDisjoint(with: propertiesToGenerate) + guard hasDependenciesGeneratedByCurrentScope else { + // This property does not have received dependencies generated by this scope, therefore its ordering is irrelevant. + continue + } + var lastDependency: List? + for nextPropertyToGenerate in propertyToGenerateNode.dropFirst() { + if + let nextProperty = nextPropertyToGenerate.value.property, + // The property to generate depends on the next property! + propertyToGenerateNode + .value + .requiredReceivedProperties + .contains(nextProperty) + { + lastDependency = nextPropertyToGenerate + } + } + + if let lastDependency { + // We depend on a (at least one) item further ahead in the list! + // Make sure we are created after our dependencies. + lastDependency.insert(propertyToGenerateNode.value) + if let head = propertyToGenerateNode.remove() { + orderedPropertiesToGenerate = head + } } - // We must generate properties that are required by other properties first - return rhs.requiredReceivedProperties.contains(lhsProperty) - }) + } - for childGenerator in orderedPropertiesToGenerate { + var generatedProperties = [String]() + for childGenerator in orderedPropertiesToGenerate.map(\.value) { generatedProperties.append( try await childGenerator .generateCode(leadingWhitespace: leadingMemberWhitespace) diff --git a/Sources/SafeDICore/Models/List.swift b/Sources/SafeDICore/Models/List.swift new file mode 100644 index 00000000..f9e3fcd0 --- /dev/null +++ b/Sources/SafeDICore/Models/List.swift @@ -0,0 +1,98 @@ +// 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. + +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 + } + + 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 + + /// 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 { + let next = next + + let nextToInsert = List(value: value) + self.next = nextToInsert + + nextToInsert.next = next + nextToInsert.previous = self + + next?.previous = nextToInsert + + return nextToInsert + } + + /// 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 new file mode 100644 index 00000000..5ad9b081 --- /dev/null +++ b/Tests/SafeDICoreTests/ListTests.swift @@ -0,0 +1,167 @@ +// 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/SafeDICoreTests/UnorderedEquatingCollectionTests.swift b/Tests/SafeDICoreTests/UnorderedEquatingCollectionTests.swift index 1cf8a1d5..36acf477 100644 --- a/Tests/SafeDICoreTests/UnorderedEquatingCollectionTests.swift +++ b/Tests/SafeDICoreTests/UnorderedEquatingCollectionTests.swift @@ -18,9 +18,6 @@ // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. -import Foundation -import SwiftParser -import SwiftSyntax import XCTest @testable import SafeDICore @@ -29,15 +26,13 @@ final class UnorderedEquatingCollectionTests: XCTestCase { func test_makeIterator_iteratesInOrder() { for (index, value) in UnorderedEquatingCollection([1, 2, 3]).enumerated() { - switch index { - case 0: + if index == 0 { XCTAssertEqual(value, 1) - case 1: + } else if index == 1 { XCTAssertEqual(value, 2) - case 2: + } else { + XCTAssertEqual(index, 2) XCTAssertEqual(value, 3) - case _: - XCTFail("Unexpected index \(index)") } } } diff --git a/Tests/SafeDIToolTests/SafeDIToolTests.swift b/Tests/SafeDIToolTests/SafeDIToolTests.swift index fecb5a45..78d5e01b 100644 --- a/Tests/SafeDIToolTests/SafeDIToolTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolTests.swift @@ -1437,16 +1437,16 @@ final class SafeDIToolTests: XCTestCase { extension Root { public convenience init() { let greatGrandchild = GreatGrandchild() - 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) }() + let childA = { + let grandchildAA = GrandchildAA(greatGrandchild: greatGrandchild) + let grandchildAB = GrandchildAB(greatGrandchild: greatGrandchild) + return ChildA(grandchildAA: grandchildAA, grandchildAB: grandchildAB) + }() self.init(childA: childA, childB: childB, greatGrandchild: greatGrandchild) } } @@ -1571,14 +1571,14 @@ final class SafeDIToolTests: XCTestCase { public convenience init() { let childA = { let greatGrandchild = GreatGrandchild() - let grandchildAA = GrandchildAA(greatGrandchild: greatGrandchild) let grandchildAB = GrandchildAB(greatGrandchild: greatGrandchild) + let grandchildAA = GrandchildAA(greatGrandchild: greatGrandchild) return ChildA(grandchildAA: grandchildAA, grandchildAB: grandchildAB, greatGrandchild: greatGrandchild) }() let childB = { let greatGrandchild = GreatGrandchild() - let grandchildBA = GrandchildBA(greatGrandchild: greatGrandchild) let grandchildBB = GrandchildBB(greatGrandchild: greatGrandchild) + let grandchildBA = GrandchildBA(greatGrandchild: greatGrandchild) return ChildB(grandchildBA: grandchildBA, grandchildBB: grandchildBB, greatGrandchild: greatGrandchild) }() self.init(childA: childA, childB: childB) @@ -2944,6 +2944,68 @@ final class SafeDIToolTests: XCTestCase { ) } + func test_run_writesConvenienceExtensionOnRootOfTree_whenFirstPropertyDependsOnLastPropertyAndMiddlePropertyHasNoDependencyEntanglementsWithEither() async throws { + let output = try await executeSystemUnderTest( + swiftFileContent: [ + """ + @Instantiable + public final class Root { + @Instantiated + let child: Child + } + """, + """ + @Instantiable + public final class Unrelated {} + """, + """ + @Instantiable + public final class Child { + @Instantiated + let grandchild: Grandchild + @Instantiated + let unrelated: Unrelated + @Instantiated + let greatGrandchild: GreatGrandchild + } + """, + """ + @Instantiable + public final class Grandchild { + @Received + let greatGrandchild: GreatGrandchild + } + """, + """ + @Instantiable + public final class GreatGrandchild {} + """, + ], + 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 child = { + let unrelated = Unrelated() + let greatGrandchild = GreatGrandchild() + let grandchild = Grandchild(greatGrandchild: greatGrandchild) + return Child(grandchild: grandchild, unrelated: unrelated, greatGrandchild: greatGrandchild) + }() + self.init(child: child) + } + } + """ + ) + } + // MARK: Error Tests func test_run_onCodeWithPropertyWithUnknownFulfilledType_throwsError() async {