From 97a7511142c819b755fe51f2ca226cd22a5d2163 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Sat, 28 Dec 2024 16:00:58 +1300 Subject: [PATCH 1/5] Explicitly declare root instantiables --- .../Views/ExampleApp.swift | 2 +- .../Sources/RootModule/Root.swift | 2 +- .../Views/ExampleApp.swift | 2 +- README.md | 10 +- .../PropertyDecoration/Instantiable.swift | 2 + .../AttributeSyntaxExtensions.swift | 18 +- .../Generators/DependencyTreeGenerator.swift | 37 +-- Sources/SafeDICore/Models/Instantiable.swift | 4 + Sources/SafeDICore/Models/Property.swift | 10 +- .../Visitors/InstantiableVisitor.swift | 34 ++- .../Macros/InstantiableMacro.swift | 37 +++ Sources/SafeDITool/SafeDITool.swift | 1 + Tests/SafeDICoreTests/FileVisitorTests.swift | 12 + .../InstantiableMacroTests.swift | 131 ++++++++++ .../SafeDIToolCodeGenerationErrorTests.swift | 162 +++++++++--- .../SafeDIToolCodeGenerationTests.swift | 230 +++++++++--------- .../SafeDIToolDOTGenerationTests.swift | 102 ++++---- 17 files changed, 538 insertions(+), 258 deletions(-) diff --git a/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift b/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift index 8704aa03..9d86a954 100644 --- a/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift +++ b/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift @@ -24,7 +24,7 @@ import Subproject import SwiftUI // @Instantiable macro marks this type as capable of being instantiated by SafeDI. -@Instantiable +@Instantiable(isRoot: true) @MainActor @main public struct NotesApp: Instantiable, App { diff --git a/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift b/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift index 3393883e..e468fdce 100644 --- a/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift +++ b/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift @@ -25,7 +25,7 @@ import Foundation import SafeDI import SharedModule -@Instantiable @MainActor +@Instantiable(isRoot: true) @MainActor public final class Root: Instantiable { public init(childA: ChildA, childB: ChildB, childC: ChildC, shared: SharedThing, userDefaults: UserDefaults) { self.childA = childA diff --git a/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift b/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift index 1c2b95de..afb60212 100644 --- a/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift +++ b/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift @@ -23,7 +23,7 @@ import SafeDI import SwiftUI // @Instantiable macro marks this type as capable of being instantiated by SafeDI. -@Instantiable +@Instantiable(isRoot: true) @MainActor @main public struct NotesApp: Instantiable, App { diff --git a/README.md b/README.md index 589d12be..50b0c79f 100644 --- a/README.md +++ b/README.md @@ -298,7 +298,7 @@ When you want to instantiate a dependency after `init(…)`, you need to declare The [`Instantiator`](Sources/SafeDI/DelayedInstantiation/Instantiator.swift) type is how SafeDI enables deferred instantiation of an `@Instantiable` type. `Instantiator` has a single generic that matches the type of the to-be-instantiated instance. Creating an `Instantiator` property is as simple as creating any other property in the SafeDI ecosystem: ```swift -@Instantiable +@Instantiable(isRoot: true) public struct MyApp: App, Instantiable { public init(contentViewInstantiator: Instantiator) { self.contentViewInstantiator = contentViewInstantiator @@ -353,13 +353,7 @@ public struct ParentView: View, Instantiable { ### Creating the root of your dependency tree -SafeDI automatically finds the root(s) of your dependency tree, and creates an extension on each root that contains a `public init()` function that instantiates the dependency tree. - -An `@Instantiable` type qualifies as the root of a dependency tree if and only if: - -1. The type‘s SafeDI-injected properties are all `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)` -2. The type‘s `@Received(fulfilledByDependencyNamed:ofType:)` properties can be fulfilled by `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)` properties declared on this type -3. The type is not instantiated by another `@Instantiable` type +Any type decorated `@Instantiable(isRoot: true)` is a root of a SafeDI dependency tree. SafeDI creates a `public init()` initializer that instantiates the dependency tree in an extension on each root type. ### Comparing SafeDI and Manual Injection: Key Differences diff --git a/Sources/SafeDI/PropertyDecoration/Instantiable.swift b/Sources/SafeDI/PropertyDecoration/Instantiable.swift index 552d5505..d4234b34 100644 --- a/Sources/SafeDI/PropertyDecoration/Instantiable.swift +++ b/Sources/SafeDI/PropertyDecoration/Instantiable.swift @@ -52,10 +52,12 @@ /// } /// /// - Parameters: +/// - isRoot: Whether the decorated type represents a root of a dependency tree. /// - additionalTypes: The types (in addition to the type decorated with this macro) of properties that can be decorated with `@Instantiated` and yield a result of this type. The types provided *must* be either superclasses of this type or protocols to which this type conforms. /// - conformsElsewhere: Whether the decorated type already conforms to the `Instantiable` protocol elsewhere. If set to `true`, the macro does not enforce that this declaration conforms to `Instantiable`. @attached(member, names: arbitrary) public macro Instantiable( + isRoot: Bool = false, fulfillingAdditionalTypes additionalTypes: [Any.Type] = [], conformsElsewhere: Bool = false ) = #externalMacro(module: "SafeDIMacros", type: "InstantiableMacro") diff --git a/Sources/SafeDICore/Extensions/AttributeSyntaxExtensions.swift b/Sources/SafeDICore/Extensions/AttributeSyntaxExtensions.swift index e12f3e3b..148e6e47 100644 --- a/Sources/SafeDICore/Extensions/AttributeSyntaxExtensions.swift +++ b/Sources/SafeDICore/Extensions/AttributeSyntaxExtensions.swift @@ -21,11 +21,25 @@ import SwiftSyntax extension AttributeSyntax { - public var fulfillingAdditionalTypes: ExprSyntax? { + public var isRoot: ExprSyntax? { guard let arguments, let labeledExpressionList = LabeledExprListSyntax(arguments), let firstLabeledExpression = labeledExpressionList.first, - firstLabeledExpression.label?.text == "fulfillingAdditionalTypes" + firstLabeledExpression.label?.text == "isRoot" + else { + return nil + } + + return firstLabeledExpression.expression + } + + public var fulfillingAdditionalTypes: ExprSyntax? { + guard let arguments, + let labeledExpressionList = LabeledExprListSyntax(arguments), + let firstLabeledExpression = labeledExpressionList.first(where: { + // In `@Instantiatable`, the `fulfillingAdditionalTypes` parameter is the second parameter, though the first parameter has a default. + $0.label?.text == "fulfillingAdditionalTypes" + }) else { return nil } diff --git a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift index f41c8b5d..19dbdc9d 100644 --- a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift +++ b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift @@ -199,12 +199,11 @@ public actor DependencyTreeGenerator { .joined(separator: "\n") } - /// A collection of `@Instantiable`-decorated types that do not explicitly receive dependencies. - /// - Note: These are not necessarily roots in the build graph, since these types may be instantiated by another `@Instantiable`. - private lazy var possibleRootInstantiables: Set = Set( + /// A collection of `@Instantiable`-decorated types that are at the roots of their respective dependency trees. + private lazy var rootInstantiables: Set = Set( typeDescriptionToFulfillingInstantiableMap .values - .filter(\.dependencies.couldRepresentRoot) + .filter(\.isRoot) .map(\.concreteInstantiable) ) @@ -233,26 +232,13 @@ public actor DependencyTreeGenerator { } } - for reachableTypeDescription in possibleRootInstantiables { + for reachableTypeDescription in rootInstantiables { recordReachableTypeDescription(reachableTypeDescription) } return reachableTypeDescriptions }() - /// A collection of `@Instantiable`-decorated types that are at the roots of their respective dependency trees. - private lazy var rootInstantiables: Set = possibleRootInstantiables - // Remove all `@Instantiable`-decorated types that are instantiated by another - // `@Instantiable`-decorated type. - .subtracting(Set( - reachableTypeDescriptions - .compactMap { typeDescriptionToFulfillingInstantiableMap[$0] } - .flatMap(\.dependencies) - .filter(\.isInstantiated) - .map(\.asInstantiatedType) - .compactMap { typeDescriptionToFulfillingInstantiableMap[$0]?.concreteInstantiable } - )) - private func createTypeDescriptionToScopeMapping() throws -> [TypeDescription: Scope] { // Create the mapping. let typeDescriptionToScopeMap: [TypeDescription: Scope] = reachableTypeDescriptions @@ -486,21 +472,6 @@ extension Dependency { } } -// MARK: - Collection - -extension Collection { - fileprivate var couldRepresentRoot: Bool { - first(where: { - switch $0.source { - case .instantiated, .aliased: - false - case .forwarded, .received: - true - } - }) == nil - } -} - // MARK: - Set extension Set { diff --git a/Sources/SafeDICore/Models/Instantiable.swift b/Sources/SafeDICore/Models/Instantiable.swift index 586fdeca..6856925d 100644 --- a/Sources/SafeDICore/Models/Instantiable.swift +++ b/Sources/SafeDICore/Models/Instantiable.swift @@ -23,12 +23,14 @@ public struct Instantiable: Codable, Hashable, Sendable { public init( instantiableType: TypeDescription, + isRoot: Bool, initializer: Initializer?, additionalInstantiables: [TypeDescription]?, dependencies: [Dependency], declarationType: DeclarationType ) { instantiableTypes = [instantiableType] + (additionalInstantiables ?? []) + self.isRoot = isRoot self.initializer = initializer self.dependencies = dependencies self.declarationType = declarationType @@ -43,6 +45,8 @@ public struct Instantiable: Codable, Hashable, Sendable { instantiableTypes[0] } + /// Whether the instantiable type is a root of a dependency graph. + public let isRoot: Bool /// A memberwise initializer for the concrete instantiable type. /// If `nil`, the Instanitable type is incorrectly configured. public let initializer: Initializer? diff --git a/Sources/SafeDICore/Models/Property.swift b/Sources/SafeDICore/Models/Property.swift index 5742cbfe..b6acad02 100644 --- a/Sources/SafeDICore/Models/Property.swift +++ b/Sources/SafeDICore/Models/Property.swift @@ -47,6 +47,11 @@ public struct Property: Codable, Hashable, Comparable, Sendable { ) } + /// The property represented as source code. + public var asSource: String { + "\(label): \(typeDescription.asSource)" + } + // MARK: Hashable public static func < (lhs: Property, rhs: Property) -> Bool { @@ -55,11 +60,6 @@ public struct Property: Codable, Hashable, Comparable, Sendable { // MARK: Internal - /// The property represented as source code. - var asSource: String { - "\(label): \(typeDescription.asSource)" - } - var asFunctionParamter: FunctionParameterSyntax { switch typeDescription { case .closure: diff --git a/Sources/SafeDICore/Visitors/InstantiableVisitor.swift b/Sources/SafeDICore/Visitors/InstantiableVisitor.swift index 57f25ec7..97b20283 100644 --- a/Sources/SafeDICore/Visitors/InstantiableVisitor.swift +++ b/Sources/SafeDICore/Visitors/InstantiableVisitor.swift @@ -188,6 +188,7 @@ public final class InstantiableVisitor: SyntaxVisitor { if let instantiableType = node.signature.returnClause?.type.typeDescription { extensionInstantiables.append(.init( instantiableType: instantiableType, + isRoot: false, initializer: initializer, additionalInstantiables: additionalInstantiables, dependencies: initializer.arguments.map { @@ -280,6 +281,7 @@ public final class InstantiableVisitor: SyntaxVisitor { // MARK: Public + public private(set) var isRoot = false public private(set) var dependencies = [Dependency]() public private(set) var initializers = [Initializer]() public private(set) var instantiableType: TypeDescription? @@ -324,6 +326,7 @@ public final class InstantiableVisitor: SyntaxVisitor { [ Instantiable( instantiableType: instantiableType, + isRoot: isRoot, initializer: initializers.first(where: { $0.isValid(forFulfilling: dependencies) }), additionalInstantiables: additionalInstantiables, dependencies: dependencies, @@ -378,16 +381,31 @@ public final class InstantiableVisitor: SyntaxVisitor { } private func processAttributes(_: AttributeListSyntax, on macro: AttributeSyntax) { - guard let fulfillingAdditionalTypesExpression = macro.fulfillingAdditionalTypes, - let fulfillingAdditionalTypesArray = ArrayExprSyntax(fulfillingAdditionalTypesExpression) - else { - // Nothing to do here. - return + func processIsRoot() { + guard let isRootExpression = macro.isRoot, + let boolExpression = BooleanLiteralExprSyntax(isRootExpression) + else { + // Nothing to do here. + return + } + + isRoot = boolExpression.literal.tokenKind == .keyword(.true) + } + func processFulfillingAdditionalTypesParameter() { + guard let fulfillingAdditionalTypesExpression = macro.fulfillingAdditionalTypes, + let fulfillingAdditionalTypesArray = ArrayExprSyntax(fulfillingAdditionalTypesExpression) + else { + // Nothing to do here. + return + } + + additionalInstantiables = fulfillingAdditionalTypesArray + .elements + .map(\.expression.typeDescription.asInstantiatedType) } - additionalInstantiables = fulfillingAdditionalTypesArray - .elements - .map(\.expression.typeDescription.asInstantiatedType) + processIsRoot() + processFulfillingAdditionalTypesParameter() } private func processModifiers(_: DeclModifierListSyntax, on node: some ConcreteDeclSyntaxProtocol) { diff --git a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift index 185d12e3..f0433e73 100644 --- a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift +++ b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift @@ -104,6 +104,26 @@ public struct InstantiableMacro: MemberMacro { context.diagnose(diagnostic) } + if visitor.isRoot, let instantiableType = visitor.instantiableType { + let inheritedDependencies = visitor.dependencies.filter { + switch $0.source { + case let .aliased(fulfillingProperty, _): + // Aliased properties must not be inherited from elsewhere. + !visitor.dependencies.contains { $0.property == fulfillingProperty } + case .instantiated: + false + case .forwarded, .received: + true + } + } + guard inheritedDependencies.isEmpty else { + throw InstantiableError.cannotBeRoot( + instantiableType, + violatingDependencies: inheritedDependencies + ) + } + } + let forwardedProperties = visitor .dependencies .filter { $0.source == .forwarded } @@ -235,6 +255,15 @@ public struct InstantiableMacro: MemberMacro { context.diagnose(diagnostic) } + if visitor.isRoot, let instantiableType = visitor.instantiableType { + guard visitor.instantiables.flatMap(\.dependencies).isEmpty else { + throw InstantiableError.cannotBeRoot( + instantiableType, + violatingDependencies: visitor.instantiables.flatMap(\.dependencies) + ) + } + } + let instantiables = visitor.instantiables if instantiables.count > 1 { var concreteInstantiables = Set() @@ -371,6 +400,7 @@ public struct InstantiableMacro: MemberMacro { case fulfillingAdditionalTypesContainsOptional case fulfillingAdditionalTypesArgumentInvalid case tooManyInstantiateMethods(TypeDescription) + case cannotBeRoot(TypeDescription, violatingDependencies: [Dependency]) var description: String { switch self { @@ -382,6 +412,13 @@ public struct InstantiableMacro: MemberMacro { "The argument `fulfillingAdditionalTypes` must be an inlined array" case let .tooManyInstantiateMethods(type): "@\(InstantiableVisitor.macroName)-decorated extension must have a single `\(InstantiableVisitor.instantiateMethodName)(…)` method that returns `\(type.asSource)`" + case let .cannotBeRoot(declaredRootType, violatingDependencies): + """ + Types decorated with `@\(InstantiableVisitor.macroName)(isRoot: true)` must only have dependencies that are all `@\(Dependency.Source.instantiatedRawValue)` or `@\(Dependency.Source.receivedRawValue)(fulfilledByDependencyNamed:ofType:)`, where the latter properties can be fulfilled by `@\(Dependency.Source.instantiatedRawValue)` or `@\(Dependency.Source.receivedRawValue)(fulfilledByDependencyNamed:ofType:)` properties declared on this type. + + The following dependencies were found on \(declaredRootType.asSource) that violated this contract: + \(violatingDependencies.map(\.property.asSource).joined(separator: "\n")) + """ } } } diff --git a/Sources/SafeDITool/SafeDITool.swift b/Sources/SafeDITool/SafeDITool.swift index c3b2c401..98a786df 100644 --- a/Sources/SafeDITool/SafeDITool.swift +++ b/Sources/SafeDITool/SafeDITool.swift @@ -99,6 +99,7 @@ struct SafeDITool: AsyncParsableCommand, Sendable { } return Instantiable( instantiableType: unnormalizedInstantiable.concreteInstantiable, + isRoot: unnormalizedInstantiable.isRoot, initializer: normalizedInitializer, additionalInstantiables: normalizedAdditionalInstantiables, dependencies: normalizedDependencies, diff --git a/Tests/SafeDICoreTests/FileVisitorTests.swift b/Tests/SafeDICoreTests/FileVisitorTests.swift index 7253aea5..f041b07b 100644 --- a/Tests/SafeDICoreTests/FileVisitorTests.swift +++ b/Tests/SafeDICoreTests/FileVisitorTests.swift @@ -48,6 +48,7 @@ final class FileVisitorTests: XCTestCase { [ Instantiable( instantiableType: .simple(name: "LoggedInViewController"), + isRoot: false, initializer: Initializer( arguments: [ .init( @@ -111,6 +112,7 @@ final class FileVisitorTests: XCTestCase { [ Instantiable( instantiableType: .simple(name: "LoggedInViewController"), + isRoot: false, initializer: Initializer( arguments: [ .init( @@ -146,6 +148,7 @@ final class FileVisitorTests: XCTestCase { ), Instantiable( instantiableType: .simple(name: "SomeOtherInstantiable"), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: nil, dependencies: [], @@ -173,6 +176,7 @@ final class FileVisitorTests: XCTestCase { [ Instantiable( instantiableType: .simple(name: "OuterLevel"), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [ .simple(name: "SomeProtocol"), @@ -182,6 +186,7 @@ final class FileVisitorTests: XCTestCase { ), Instantiable( instantiableType: .nested(name: "InnerLevel", parentType: .simple(name: "OuterLevel")), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], @@ -208,6 +213,7 @@ final class FileVisitorTests: XCTestCase { [ Instantiable( instantiableType: .simple(name: "OuterLevel"), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], @@ -215,6 +221,7 @@ final class FileVisitorTests: XCTestCase { ), Instantiable( instantiableType: .nested(name: "InnerLevel", parentType: .simple(name: "OuterLevel")), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], @@ -252,6 +259,7 @@ final class FileVisitorTests: XCTestCase { [ Instantiable( instantiableType: .simple(name: "OuterLevel"), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], @@ -259,6 +267,7 @@ final class FileVisitorTests: XCTestCase { ), Instantiable( instantiableType: .nested(name: "InnerLevel1", parentType: .simple(name: "OuterLevel")), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], @@ -266,6 +275,7 @@ final class FileVisitorTests: XCTestCase { ), Instantiable( instantiableType: .nested(name: "InnerLevel2", parentType: .simple(name: "OuterLevel")), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], @@ -273,6 +283,7 @@ final class FileVisitorTests: XCTestCase { ), Instantiable( instantiableType: .nested(name: "InnerLevel3", parentType: .simple(name: "OuterLevel")), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], @@ -297,6 +308,7 @@ final class FileVisitorTests: XCTestCase { [ Instantiable( instantiableType: .nested(name: "InnerLevel", parentType: .simple(name: "OuterLevel")), + isRoot: false, initializer: Initializer(arguments: []), additionalInstantiables: [], dependencies: [], diff --git a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift index b6bfcff4..97c83065 100644 --- a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift +++ b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift @@ -184,6 +184,97 @@ import SafeDICore } } + func test_declaration_doesNotThrowWhenRootHasInstantiatedAndRenamedDependencies() { + assertMacro { + """ + @Instantiable(isRoot: true) + public final class Foo: Instantiable { + public init(dependency: Dependency, renamedDependency: Dependency, renamed2Dependency: Dependency) { + fatalError("SafeDI doesn't inspect the initializer body") + } + + @Instantiated private let dependency: Dependency + @Received(fulfilledByDependencyNamed: "dependency", ofType: Dependency.self) private let renamedDependency: Dependency + @Received(fulfilledByDependencyNamed: "renamedDependency", ofType: Dependency.self) private let renamed2Dependency: Dependency + } + """ + } expansion: { + """ + public final class Foo: Instantiable { + public init(dependency: Dependency, renamedDependency: Dependency, renamed2Dependency: Dependency) { + fatalError("SafeDI doesn't inspect the initializer body") + } + + private let dependency: Dependency + private let renamedDependency: Dependency + private let renamed2Dependency: Dependency + } + """ + } + } + + func test_declaration_throwsErrorWhenRootHasReceivedDependency() { + assertMacro { + """ + @Instantiable(isRoot: true) + public final class Foo: Instantiable { + public init(dependency: Dependency) { + fatalError("SafeDI doesn't inspect the initializer body") + } + + @Received private let dependency: Dependency + } + """ + } diagnostics: { + """ + @Instantiable(isRoot: true) + ┬────────────────────────── + ╰─ 🛑 Types decorated with `@Instantiable(isRoot: true)` must only have dependencies that are all `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)`, where the latter properties can be fulfilled by `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)` properties declared on this type. + + The following dependencies were found on Foo that violated this contract: + dependency: Dependency + public final class Foo: Instantiable { + public init(dependency: Dependency) { + fatalError("SafeDI doesn't inspect the initializer body") + } + + @Received private let dependency: Dependency + } + """ + } + } + + func test_declaration_throwsErrorWhenRootHasForwardedDependency() { + assertMacro { + """ + @Instantiable(isRoot: true) + public final class Foo: Instantiable { + public init(dependency: Dependency) { + fatalError("SafeDI doesn't inspect the initializer body") + } + + @Forwarded private let dependency: Dependency + } + """ + } diagnostics: { + """ + @Instantiable(isRoot: true) + ┬────────────────────────── + ╰─ 🛑 Types decorated with `@Instantiable(isRoot: true)` must only have dependencies that are all `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)`, where the latter properties can be fulfilled by `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)` properties declared on this type. + + The following dependencies were found on Foo that violated this contract: + dependency: Dependency + public final class Foo: Instantiable { + public init(dependency: Dependency) { + fatalError("SafeDI doesn't inspect the initializer body") + } + + @Forwarded private let dependency: Dependency + } + """ + } + } + func test_extension_throwsErrorWhenFulfillingAdditionalTypesIsAPropertyReference() { assertMacro { """ @@ -248,6 +339,46 @@ import SafeDICore } } + func test_extension_doesNotThrowWhenRootHasNoDependencies() { + assertMacro { + """ + @Instantiable(isRoot: true) + extension Foo: Instantiable { + public static func instantiate() -> Foo { fatalError() } + } + """ + } expansion: { + """ + extension Foo: Instantiable { + public static func instantiate() -> Foo { fatalError() } + } + """ + } + } + + func test_extension_throwsErrorWhenRootHasDependencies() { + assertMacro { + """ + @Instantiable(isRoot: true) + extension Foo: Instantiable { + public static func instantiate(bar: Bar) -> Foo { fatalError() } + } + """ + } diagnostics: { + """ + @Instantiable(isRoot: true) + ┬────────────────────────── + ╰─ 🛑 Types decorated with `@Instantiable(isRoot: true)` must only have dependencies that are all `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)`, where the latter properties can be fulfilled by `@Instantiated` or `@Received(fulfilledByDependencyNamed:ofType:)` properties declared on this type. + + The following dependencies were found on Foo that violated this contract: + bar: Bar + extension Foo: Instantiable { + public static func instantiate(bar: Bar) -> Foo { fatalError() } + } + """ + } + } + // MARK: FixIt tests func test_declaration_fixit_generatesRequiredInitializerWithoutAnyDependenciesOnStruct() { diff --git a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift index 4855fcd0..cb36ad2a 100644 --- a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationErrorTests.swift @@ -54,7 +54,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated(fulfilledByType: "DoesNotExist") let networkService: NetworkService @@ -81,7 +81,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let childBuilder: Instantiator } @@ -125,7 +125,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let childBuilder: Instantiator } @@ -178,7 +178,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated let networkService: NetworkService @@ -214,7 +214,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated let networkService: NetworkService @@ -241,7 +241,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated(fulfilledByType: "SomeErasedType") let erasedType: ErasedType @Instantiated let child: Child @@ -284,7 +284,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated(fulfilledByType: "SomeErasedType") let erasedType: any ErasedType @Instantiated let child: Child @@ -327,7 +327,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let thing: Thing @Instantiated let child: Child @@ -368,7 +368,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let thing: Thing! @Instantiated let child: Child @@ -409,7 +409,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let thing: Thing @Instantiated let child: Child @@ -450,7 +450,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let thing: Thing? @Instantiated let child: Child @@ -491,7 +491,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let thing: Thing @Instantiated let child: Child @@ -536,7 +536,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import SafeDI - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let thing: Thing @Instantiated let otherThing: OtherThing @@ -590,7 +590,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated let networkService: NetworkService @@ -613,7 +613,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let childA: ChildA @@ -669,7 +669,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let childA: ChildA @@ -755,7 +755,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -816,7 +816,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -859,7 +859,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated let networkService: NetworkService @@ -917,7 +917,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated let networkService: NetworkService @@ -993,7 +993,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -1034,7 +1034,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated let urlSessionWrapper: URLSessionWrapper @@ -1048,7 +1048,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { } @MainActor - func test_run_onCodeWithDuplicateInstantiable_throwsError() async { + func test_run_onCodeWithDuplicateInstantiableNames_throwsError() async { await assertThrowsError( """ @Instantiable-decorated types and extensions must have globally unique type names and fulfill globally unqiue types. Found multiple types or extensions fulfilling `RootViewController` @@ -1076,7 +1076,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { } @MainActor - func test_run_onCodeWithDuplicateInstantiableAndInstantiable_throwsError() async { + func test_run_onCodeWithDuplicateInstantiableNamesWhereOneIsRoot_throwsError() async { await assertThrowsError( """ @Instantiable-decorated types and extensions must have globally unique type names and fulfill globally unqiue types. Found multiple types or extensions fulfilling `RootViewController` @@ -1087,9 +1087,69 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit + @Instantiable(isRoot: true) + public final class RootViewController: UIViewController {} + """, + """ + import UIKit + @Instantiable public final class RootViewController: UIViewController {} """, + ], + buildDependencyTreeOutput: true, + filesToDelete: &filesToDelete + ) + } + } + + @MainActor + func test_run_onCodeWithDuplicateInstantiableNamesViaDeclarationAndExtension_throwsError() async { + await assertThrowsError( + """ + @Instantiable-decorated types and extensions must have globally unique type names and fulfill globally unqiue types. Found multiple types or extensions fulfilling `RootViewController` + """ + ) { + try await executeSafeDIToolTest( + swiftFileContent: [ + """ + import UIKit + + @Instantiable + public final class RootViewController: UIViewController {} + """, + """ + import UIKit + + @Instantiable + extension RootViewController: Instantiable { + public static func instantiate() -> RootViewController { + RootViewController() + } + } + """, + ], + buildDependencyTreeOutput: true, + filesToDelete: &filesToDelete + ) + } + } + + @MainActor + func test_run_onCodeWithDuplicateInstantiableNamesViaDeclarationAndExtensionWhereDeclarationIsRoot_throwsError() async { + await assertThrowsError( + """ + @Instantiable-decorated types and extensions must have globally unique type names and fulfill globally unqiue types. Found multiple types or extensions fulfilling `RootViewController` + """ + ) { + try await executeSafeDIToolTest( + swiftFileContent: [ + """ + import UIKit + + @Instantiable(isRoot: true) + public final class RootViewController: UIViewController {} + """, """ import UIKit @@ -1108,7 +1168,39 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { } @MainActor - func test_run_onCodeWithDuplicateInstantiableViaExtension_throwsError() async { + func test_run_onCodeWithDuplicateInstantiableNamesViaDeclarationAndExtensionWhereExtensionIsRoot_throwsError() async { + await assertThrowsError( + """ + @Instantiable-decorated types and extensions must have globally unique type names and fulfill globally unqiue types. Found multiple types or extensions fulfilling `RootViewController` + """ + ) { + try await executeSafeDIToolTest( + swiftFileContent: [ + """ + import UIKit + + @Instantiable + public final class RootViewController: UIViewController {} + """, + """ + import UIKit + + @Instantiable(isRoot: true) + extension RootViewController: Instantiable { + public static func instantiate() -> RootViewController { + RootViewController() + } + } + """, + ], + buildDependencyTreeOutput: true, + filesToDelete: &filesToDelete + ) + } + } + + @MainActor + func test_run_onCodeWithDuplicateInstantiableNamesViaExtension_throwsError() async { await assertThrowsError( """ @Instantiable-decorated types and extensions must have globally unique type names and fulfill globally unqiue types. Found multiple types or extensions fulfilling `UserDefaults` @@ -1155,7 +1247,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable(fulfillingAdditionalTypes: [UIViewController.self]) + @Instantiable(isRoot: true, fulfillingAdditionalTypes: [UIViewController.self]) public final class RootViewController: UIViewController {} """, """ @@ -1182,7 +1274,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let a: A @@ -1227,7 +1319,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let a: A } @@ -1268,7 +1360,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let aBuilder: Instantiator } @@ -1309,7 +1401,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let a: A @@ -1354,7 +1446,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated private let a: A @@ -1403,7 +1495,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated private let a: A @@ -1454,7 +1546,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated private let a: A @@ -1530,7 +1622,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator) { self.authService = authService @@ -1616,7 +1708,7 @@ final class SafeDIToolCodeGenerationErrorTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator) { self.authService = authService diff --git a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift index 939aee73..48d4b031 100644 --- a/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolCodeGenerationTests.swift @@ -71,7 +71,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { public protocol NetworkService {} - @Instantiable(fulfillingAdditionalTypes: [NetworkService.self]) + @Instantiable(isRoot: true, fulfillingAdditionalTypes: [NetworkService.self]) public final class DefaultNetworkService: NetworkService { let urlSession: URLSession = .shared } @@ -114,7 +114,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(networkService: NetworkService) { fatalError("SafeDI doesn't inspect the initializer body") @@ -166,7 +166,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public actor Root { public init(networkService: NetworkService) { fatalError("SafeDI doesn't inspect the initializer body") @@ -218,7 +218,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(networkService: NetworkService) { fatalError("SafeDI doesn't inspect the initializer body") @@ -270,7 +270,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public struct Root1 { public init(networkService: NetworkService) { fatalError("SafeDI doesn't inspect the initializer body") @@ -280,7 +280,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public struct Root2 { public init(networkService: NetworkService) { fatalError("SafeDI doesn't inspect the initializer body") @@ -327,7 +327,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(userService: any UserService) { fatalError("SafeDI doesn't inspect the initializer body") @@ -394,7 +394,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class SomeInstantiated: Instantiable { public init(someClass: any SomeProtocol) { fatalError("SafeDI doesn't inspect the initializer body") @@ -433,7 +433,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(userService: UserService?) { fatalError("SafeDI doesn't inspect the initializer body") @@ -518,7 +518,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator) { self.authService = authService @@ -628,7 +628,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -731,7 +731,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(networkServiceBuilder: Instantiator) { self.networkServiceBuilder = networkServiceBuilder @@ -811,7 +811,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator<(userID: String, userName: String), UIViewController>) { self.authService = authService @@ -941,7 +941,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator) { self.authService = authService @@ -1039,7 +1039,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import SwiftUI - @Instantiable + @Instantiable(isRoot: true) public struct RootView: View { public init(splashScreenView: AnyView) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1093,7 +1093,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import SwiftUI - @Instantiable + @Instantiable(isRoot: true) public struct RootView: View { public init(splashScreenViewBuilder: ErasedInstantiator<(), AnyView>) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1181,7 +1181,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -1303,7 +1303,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -1398,7 +1398,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1408,7 +1408,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class Child { // This Child is incorrectly configured! It is missing the required initializer. @@ -1418,7 +1418,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class Grandchild { public init() {} } @@ -1454,7 +1454,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1464,7 +1464,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class Child { public init(grandchild: Grandchild, nonInjectedProperty: Int = 5) { self.grandchild = grandchild @@ -1477,7 +1477,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class Grandchild { public init() {} } @@ -1513,7 +1513,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1523,7 +1523,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable final class Child { public init(grandchild: Grandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1533,7 +1533,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class Grandchild { public init() {} } @@ -1569,7 +1569,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1579,7 +1579,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable final class Child { public init(grandchild: Grandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1589,7 +1589,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class Grandchild { public init() {} } @@ -1625,7 +1625,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(childA: ChildA, childB: ChildB, greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1637,7 +1637,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildA { public init(grandchildAA: GrandchildAA, grandchildAB: GrandchildAB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1648,7 +1648,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAA { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1658,7 +1658,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAB { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1668,7 +1668,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildB { public init(grandchildBA: GrandchildBA, grandchildBB: GrandchildBB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1679,7 +1679,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBA { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1689,7 +1689,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBB { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1699,7 +1699,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GreatGrandchild { public init() {} } @@ -1744,7 +1744,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(childA: ChildA, childB: ChildB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1755,7 +1755,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildA { public init(grandchildAA: GrandchildAA, grandchildAB: GrandchildAB, greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1767,7 +1767,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAA { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1777,7 +1777,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAB { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1787,7 +1787,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildB { public init(grandchildBA: GrandchildBA, grandchildBB: GrandchildBB, greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1799,7 +1799,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBA { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1809,7 +1809,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBB { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1819,7 +1819,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GreatGrandchild { public init() {} } @@ -1865,7 +1865,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -1950,7 +1950,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child, recreated: Recreated) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2037,7 +2037,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child, recreated: Recreated) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2138,7 +2138,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2259,7 +2259,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(childABuilder: SendableErasedInstantiator, childB: ChildB, recreated: Recreated) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2391,7 +2391,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(childA: ChildA, childB: ChildB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2402,7 +2402,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildA { public init(grandchildAA: GrandchildAA, grandchildAB: GrandchildAB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2413,7 +2413,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAA { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2423,7 +2423,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAB { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2433,7 +2433,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildB { public init(grandchildBA: GrandchildBA, grandchildBB: GrandchildBB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2444,7 +2444,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBA { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2454,7 +2454,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBB { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2464,7 +2464,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GreatGrandchild { public init() {} } @@ -2524,7 +2524,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { public init(child: Child, keyValueStore: KeyValueStore) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2535,7 +2535,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class Child { public init(keyValueStore: KeyValueStore) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2630,7 +2630,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator) { self.authService = authService @@ -2734,7 +2734,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let greatGrandchildModuleOutput = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable public final class GreatGrandchild: Sendable { public init() {} } @@ -2749,7 +2749,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildAA { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2761,7 +2761,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildAB { public init(greatGrandchild: GreatGrandchild) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2773,7 +2773,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildBA { public init(greatGrandchildInstantiator: SendableInstantiator) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2785,7 +2785,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildBB { public init(greatGrandchildInstantiator: SendableInstantiator) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2820,7 +2820,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ @preconcurrency import GrandchildModule - @Instantiable() + @Instantiable public final class ChildB { public init(grandchildBA: GrandchildBA, grandchildBB: GrandchildBB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2845,7 +2845,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { import ChildModule @MainActor - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(childA: ChildA, childB: ChildB) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2930,7 +2930,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(defaultUserService: DefaultUserService, userService: any UserService) { fatalError("SafeDI doesn't inspect the initializer body") @@ -2989,26 +2989,34 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ @Instantiable public struct NotRoot { - @Instantiated - private let defaultUserService: DefaultUserService - - // This received property's alias is improperly configured, meaning that this type is not a root. - @Received(fulfilledByDependencyNamed: "userService", ofType: DefaultUserService.self) - private let userService: any UserService + public init() {} } """, - """ - import Foundation + ], + buildDependencyTreeOutput: true, + filesToDelete: &filesToDelete + ) - public protocol UserService { - var userName: String? { get set } - } + 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. - @Instantiable(fulfillingAdditionalTypes: [UserService.self]) - public final class DefaultUserService: UserService { - public init() {} + // No root @Instantiable-decorated types found, or root types already had a `public init()` method. + """ + ) + } - public var userName: String? + @MainActor + func test_run_successfullyGeneratesOutputFileWhenIsRootIsFalse() async throws { + let output = try await executeSafeDIToolTest( + swiftFileContent: [ + """ + @Instantiable(isRoot: false) + public struct NotRoot { + public init() {} } """, ], @@ -3023,10 +3031,6 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { // Any modifications made to this file will be overwritten on subsequent builds. // Please refrain from editing this file directly. - #if canImport(Foundation) - import Foundation - #endif - // No root @Instantiable-decorated types found, or root types already had a `public init()` method. """ ) @@ -3084,7 +3088,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -3196,7 +3200,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(childBuilder: Instantiator) { fatalError("SafeDI doesn't inspect the initializer body") @@ -3314,7 +3318,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -3466,7 +3470,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService) { self.authService = authService @@ -3546,7 +3550,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService) { self.authService = authService @@ -3643,7 +3647,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -3802,7 +3806,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -3912,7 +3916,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -3988,7 +3992,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public 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) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4374,7 +4378,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(childBuilder: Instantiator?) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4419,7 +4423,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(aBuilder: Instantiator) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4497,7 +4501,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(a: A) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4575,7 +4579,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(a: A) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4629,7 +4633,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(aBuilder: Instantiator) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4684,7 +4688,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(stringContainer: Container, intContainer: Container, floatContainer: Container, voidContainer: Container) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4746,7 +4750,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { public init(stringContainer: MyModule.Container, intContainer: MyModule.Container, floatContainer: MyModule.Container, voidContainer: MyModule.Container) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4825,7 +4829,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -4910,7 +4914,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -5004,7 +5008,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -5099,7 +5103,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -5167,7 +5171,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -5230,7 +5234,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { public init(child: Child) { fatalError("SafeDI doesn't inspect the initializer body") @@ -5283,7 +5287,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class TypeWithDependency { public init(erasedType: ErasedType ) { fatalError("SafeDI doesn't inspect the initializer body") @@ -5336,7 +5340,7 @@ final class SafeDIToolCodeGenerationTests: XCTestCase { // Extension defined before an @Instantiable should not make a difference. } - @Instantiable + @Instantiable(isRoot: true) public final class TypeWithDependency { public init(erasedType: ErasedType ) { fatalError("SafeDI doesn't inspect the initializer body") diff --git a/Tests/SafeDIToolTests/SafeDIToolDOTGenerationTests.swift b/Tests/SafeDIToolTests/SafeDIToolDOTGenerationTests.swift index 341892ef..83c67cf1 100644 --- a/Tests/SafeDIToolTests/SafeDIToolDOTGenerationTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolDOTGenerationTests.swift @@ -76,7 +76,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { @Instantiated let networkService: NetworkService @@ -113,14 +113,14 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable + @Instantiable(isRoot: true) public struct Root1 { @Instantiated let networkService: NetworkService } """, """ - @Instantiable + @Instantiable(isRoot: true) public struct Root2 { @Instantiated let networkService: NetworkService @@ -175,7 +175,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator) { self.authService = authService @@ -264,7 +264,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -366,7 +366,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator<(userID: String, userName: String), UIViewController>) { self.authService = authService @@ -476,7 +476,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: ErasedInstantiator) { self.authService = authService @@ -583,7 +583,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -684,7 +684,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -760,7 +760,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { @Instantiated let childA: ChildA @@ -771,7 +771,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildA { @Instantiated let grandchildAA: GrandchildAA @@ -780,21 +780,21 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAA { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GrandchildAB { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class ChildB { @Instantiated let grandchildBA: GrandchildBA @@ -803,21 +803,21 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBA { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GrandchildBB { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GreatGrandchild {} """, @@ -848,7 +848,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { @Instantiated let childA: ChildA @@ -857,7 +857,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildA { @Instantiated let grandchildAA: GrandchildAA @@ -868,21 +868,21 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAA { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GrandchildAB { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class ChildB { @Instantiated let grandchildBA: GrandchildBA @@ -893,21 +893,21 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBA { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GrandchildBB { @Received let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GreatGrandchild {} """, @@ -939,7 +939,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let child: Child @@ -1000,7 +1000,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable(isRoot: true) public final class Root { @Instantiated let childA: ChildA @@ -1009,7 +1009,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class ChildA { @Instantiated let grandchildAA: GrandchildAA @@ -1018,21 +1018,21 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildAA { @Instantiated let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GrandchildAB { @Instantiated let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class ChildB { @Instantiated let grandchildBA: GrandchildBA @@ -1041,21 +1041,21 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { } """, """ - @Instantiable() + @Instantiable public final class GrandchildBA { @Instantiated let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GrandchildBB { @Instantiated let greatGrandchild: GreatGrandchild } """, """ - @Instantiable() + @Instantiable public final class GreatGrandchild {} """, @@ -1089,7 +1089,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let greatGrandchildModuleOutput = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable() + @Instantiable public final class GreatGrandchild: Sendable {} """, ], @@ -1102,7 +1102,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildAA { @Instantiated let greatGrandchild: GreatGrandchild @@ -1111,7 +1111,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildAB { @Instantiated let greatGrandchild: GreatGrandchild @@ -1120,7 +1120,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildBA { @Instantiated var greatGrandchildInstantiator: SendableInstantiator @@ -1129,7 +1129,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import GreatGrandchildModule - @Instantiable() + @Instantiable public final class GrandchildBB { @Instantiated greatGrandchildInstantiator: SendableInstantiator @@ -1159,7 +1159,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ @preconcurrency import GrandchildModule - @Instantiable() + @Instantiable public final class ChildB { @Instantiated let grandchildBA: GrandchildBA @@ -1182,7 +1182,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { import ChildModule @MainActor - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let childA: ChildA @@ -1225,7 +1225,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated private let defaultUserService: DefaultUserService @@ -1302,7 +1302,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { """ import UIKit - @Instantiable + @Instantiable(isRoot: true) public final class RootViewController: UIViewController { public init(authService: AuthService, networkService: NetworkService, loggedInViewControllerBuilder: Instantiator) { self.authService = authService @@ -1342,7 +1342,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let child: Child @@ -1398,7 +1398,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public final class Root { @Instantiated let a: A @@ -1772,7 +1772,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let aBuilder: Instantiator @@ -1823,7 +1823,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let a: A @@ -1874,7 +1874,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let a: A @@ -1910,7 +1910,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let aBuilder: Instantiator @@ -1949,7 +1949,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let stringContainer: Container @Instantiated let intContainer: Container @@ -2001,7 +2001,7 @@ final class SafeDIToolDOTGenerationTests: XCTestCase { let output = try await executeSafeDIToolTest( swiftFileContent: [ """ - @Instantiable + @Instantiable(isRoot: true) public struct Root { @Instantiated let stringContainer: MyModule.Container @Instantiated let intContainer: MyModule.Container From 71e09bce1e55278b160edf9ff64451b64d2df38e Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Sat, 28 Dec 2024 18:14:19 +1300 Subject: [PATCH 2/5] Remove dead code --- Sources/SafeDICore/Generators/DependencyTreeGenerator.swift | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift index 19dbdc9d..6b5c74c2 100644 --- a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift +++ b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift @@ -358,10 +358,7 @@ public actor DependencyTreeGenerator { let parentContainsProperty = receivableProperties.contains(receivedProperty) let propertyIsCreatedAtThisScope = createdProperties.contains(receivedProperty) if !parentContainsProperty, !propertyIsCreatedAtThisScope { - if property == nil { - // This property's scope is not a real root instantiable! Remove it from the list. - rootInstantiables.remove(scope.instantiable.concreteInstantiable) - } else { + if property != nil { // This property is in a dependency tree and is unfulfillable. Record the problem. unfulfillableProperties.insert(.init( property: receivedProperty, From 1a16687893cd639a55dd911be10e992351d433e6 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Sat, 28 Dec 2024 22:34:13 +1300 Subject: [PATCH 3/5] Increase code coverage --- .../Generators/DependencyTreeGenerator.swift | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift index 6b5c74c2..1a896b35 100644 --- a/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift +++ b/Sources/SafeDICore/Generators/DependencyTreeGenerator.swift @@ -243,15 +243,13 @@ public actor DependencyTreeGenerator { // Create the mapping. let typeDescriptionToScopeMap: [TypeDescription: Scope] = reachableTypeDescriptions .reduce(into: [TypeDescription: Scope]()) { partialResult, typeDescription in - guard let instantiable = typeDescriptionToFulfillingInstantiableMap[typeDescription], - partialResult[instantiable.concreteInstantiable] == nil - else { - // We've already created a scope for this `instantiable`. Skip. - return - } - let scope = Scope(instantiable: instantiable) - for instantiableType in instantiable.instantiableTypes { - partialResult[instantiableType] = scope + if let instantiable = typeDescriptionToFulfillingInstantiableMap[typeDescription], + partialResult[instantiable.concreteInstantiable] == nil + { + let scope = Scope(instantiable: instantiable) + for instantiableType in instantiable.instantiableTypes { + partialResult[instantiableType] = scope + } } } From 0eb4ed0e614fc76e861c725b03289ac87c0eb054 Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Sun, 29 Dec 2024 07:27:47 +1300 Subject: [PATCH 4/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 50b0c79f..de89257d 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ public struct ParentView: View, Instantiable { ### Creating the root of your dependency tree -Any type decorated `@Instantiable(isRoot: true)` is a root of a SafeDI dependency tree. SafeDI creates a `public init()` initializer that instantiates the dependency tree in an extension on each root type. +Any type decorated with `@Instantiable(isRoot: true)` is a root of a SafeDI dependency tree. SafeDI creates a no-parameter `public init()` initializer that instantiates the dependency tree in an extension on each root type. ### Comparing SafeDI and Manual Injection: Key Differences From 10284703965684d63af8440d3ab8fe17c84b956b Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Sun, 29 Dec 2024 08:06:07 +1300 Subject: [PATCH 5/5] Better comments in sample projects --- .../ExampleMultiProjectIntegration/Views/ExampleApp.swift | 2 +- .../ExampleProjectIntegration/Views/ExampleApp.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift b/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift index 9d86a954..1996fdb2 100644 --- a/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift +++ b/Examples/ExampleMultiProjectIntegration/ExampleMultiProjectIntegration/Views/ExampleApp.swift @@ -23,7 +23,7 @@ import SafeDI import Subproject import SwiftUI -// @Instantiable macro marks this type as capable of being instantiated by SafeDI. +// @Instantiable macro marks this type as capable of being instantiated by SafeDI. The `isRoot` parameter marks this type as being the root of the dependency tree. @Instantiable(isRoot: true) @MainActor @main diff --git a/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift b/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift index afb60212..dfde07af 100644 --- a/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift +++ b/Examples/ExampleProjectIntegration/ExampleProjectIntegration/Views/ExampleApp.swift @@ -22,7 +22,7 @@ import Combine import SafeDI import SwiftUI -// @Instantiable macro marks this type as capable of being instantiated by SafeDI. +// @Instantiable macro marks this type as capable of being instantiated by SafeDI. The `isRoot` parameter marks this type as being the root of the dependency tree. @Instantiable(isRoot: true) @MainActor @main