From 4506d0523345b94a61c5fa2689bb96c7ee0b04de Mon Sep 17 00:00:00 2001 From: Dan Federman Date: Mon, 22 Jul 2024 23:02:16 -0600 Subject: [PATCH] Generate required initializer via FixIt instead of code-generation (#104) --- .../Package.resolved | 5 +- .../Sources/ChildAModule/ChildA.swift | 5 + .../Sources/ChildBModule/ChildB.swift | 5 + .../Sources/ChildCModule/ChildC.swift | 5 + .../GrandchildrenModule/GrandchildA.swift | 4 + .../GrandchildrenModule/GrandchildB.swift | 4 + .../GrandchildrenModule/GrandchildC.swift | 4 + .../Sources/RootModule/Root.swift | 8 + .../Sources/SharedModule/SharedThing.swift | 4 +- README.md | 35 +- .../Errors/FixableInstantiableError.swift | 17 +- Sources/SafeDICore/Models/Initializer.swift | 5 +- Sources/SafeDICore/Models/Property.swift | 5 +- .../Visitors/InstantiableVisitor.swift | 7 +- .../Macros/InstantiableMacro.swift | 39 +- .../InstantiableMacroTests.swift | 1108 +++++++++++------ 16 files changed, 829 insertions(+), 431 deletions(-) diff --git a/Examples/ExamplePackageIntegration/Package.resolved b/Examples/ExamplePackageIntegration/Package.resolved index 026a6bec..ab8b8de0 100644 --- a/Examples/ExamplePackageIntegration/Package.resolved +++ b/Examples/ExamplePackageIntegration/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "4828c1967a8becd3449664c8ced8013fc851b1b749c97f36a6b6e17c68977693", "pins" : [ { "identity" : "jjliso8601dateformatter", @@ -30,7 +31,7 @@ { "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-syntax.git", + "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { "revision" : "fa8f95c2d536d6620cc2f504ebe8a6167c9fc2dd", "version" : "510.0.1" @@ -55,5 +56,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/Examples/ExamplePackageIntegration/Sources/ChildAModule/ChildA.swift b/Examples/ExamplePackageIntegration/Sources/ChildAModule/ChildA.swift index b918eec4..fbd2f61c 100644 --- a/Examples/ExamplePackageIntegration/Sources/ChildAModule/ChildA.swift +++ b/Examples/ExamplePackageIntegration/Sources/ChildAModule/ChildA.swift @@ -24,6 +24,11 @@ import SharedModule @Instantiable public final class ChildA: Instantiable { + public init(shared: SharedThing, grandchildA: GrandchildA) { + self.shared = shared + self.grandchildA = grandchildA + } + @Received let shared: SharedThing @Instantiated let grandchildA: GrandchildA diff --git a/Examples/ExamplePackageIntegration/Sources/ChildBModule/ChildB.swift b/Examples/ExamplePackageIntegration/Sources/ChildBModule/ChildB.swift index 960d1613..2905979f 100644 --- a/Examples/ExamplePackageIntegration/Sources/ChildBModule/ChildB.swift +++ b/Examples/ExamplePackageIntegration/Sources/ChildBModule/ChildB.swift @@ -24,6 +24,11 @@ import SharedModule @Instantiable public final class ChildB: Instantiable { + public init(shared: SharedThing, grandchildB: GrandchildB) { + self.shared = shared + self.grandchildB = grandchildB + } + @Received let shared: SharedThing @Instantiated let grandchildB: GrandchildB diff --git a/Examples/ExamplePackageIntegration/Sources/ChildCModule/ChildC.swift b/Examples/ExamplePackageIntegration/Sources/ChildCModule/ChildC.swift index b6192b83..c5e5143c 100644 --- a/Examples/ExamplePackageIntegration/Sources/ChildCModule/ChildC.swift +++ b/Examples/ExamplePackageIntegration/Sources/ChildCModule/ChildC.swift @@ -24,6 +24,11 @@ import SharedModule @Instantiable public final class ChildC: Instantiable { + public init(shared: SharedThing, grandchildC: GrandchildC) { + self.shared = shared + self.grandchildC = grandchildC + } + @Received let shared: SharedThing @Instantiated let grandchildC: GrandchildC diff --git a/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildA.swift b/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildA.swift index a805cec9..5a838f15 100644 --- a/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildA.swift +++ b/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildA.swift @@ -23,5 +23,9 @@ import SharedModule @Instantiable public final class GrandchildA: Instantiable { + public init(shared: SharedThing) { + self.shared = shared + } + @Received let shared: SharedThing } diff --git a/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildB.swift b/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildB.swift index 0f46c667..b2153733 100644 --- a/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildB.swift +++ b/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildB.swift @@ -23,5 +23,9 @@ import SharedModule @Instantiable public actor GrandchildB: Instantiable { + public init(shared: SharedThing) { + self.shared = shared + } + @Received let shared: SharedThing } diff --git a/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildC.swift b/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildC.swift index cc57018a..2ac8879a 100644 --- a/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildC.swift +++ b/Examples/ExamplePackageIntegration/Sources/GrandchildrenModule/GrandchildC.swift @@ -23,5 +23,9 @@ import SharedModule @Instantiable public struct GrandchildC: Instantiable { + public init(shared: SharedThing) { + self.shared = shared + } + @Received let shared: SharedThing } diff --git a/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift b/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift index 8be573ce..7addb5c8 100644 --- a/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift +++ b/Examples/ExamplePackageIntegration/Sources/RootModule/Root.swift @@ -27,6 +27,14 @@ import SharedModule @Instantiable public final class Root: Instantiable { + public init(childA: ChildA, childB: ChildB, childC: ChildC, shared: SharedThing, userDefaults: UserDefaults) { + self.childA = childA + self.childB = childB + self.childC = childC + self.shared = shared + self.userDefaults = userDefaults + } + static let shared = Root() @Instantiated let childA: ChildA diff --git a/Examples/ExamplePackageIntegration/Sources/SharedModule/SharedThing.swift b/Examples/ExamplePackageIntegration/Sources/SharedModule/SharedThing.swift index 13d9a466..6a47b4a1 100644 --- a/Examples/ExamplePackageIntegration/Sources/SharedModule/SharedThing.swift +++ b/Examples/ExamplePackageIntegration/Sources/SharedModule/SharedThing.swift @@ -21,4 +21,6 @@ import SafeDI @Instantiable -public final class SharedThing: Instantiable {} +public final class SharedThing: Instantiable { + public init() {} +} diff --git a/README.md b/README.md index 7f9449d6..0700ef8d 100644 --- a/README.md +++ b/README.md @@ -72,9 +72,7 @@ import SafeDI @Instantiable public final class UserService: Instantiable { - /// Public, memberwise initializer that takes each injected property. - /// This initializer can be auto-generated by SafeDI if it isn‘t manually written. public init(authService: AuthService, securePersistentStorage: SecurePersistentStorage) { self.authService = authService self.securePersistentStorage = securePersistentStorage @@ -171,6 +169,11 @@ Here we have a `LoggedInContentView` whose forwarded `user` property is received ```swift @Instantiable public struct LoggedInContentView: View, Instantiable { + public init(user: User, profileViewBuilder: ErasedInstantiator<(), AnyView>) { + self.user = user + self.profileViewBuilder = profileViewBuilder + } + public var body: some View { ... // Instantiates and displays a ProfileView when a button is pressed. } @@ -182,6 +185,10 @@ public struct LoggedInContentView: View, Instantiable { @Instantiable public struct ProfileView: View, Instantiable { + public init(updateUserService: UpdateUserService) { + self.updateUserService = updateUserService + } + public var body: some View { ... // Allows for updating user information. } @@ -238,6 +245,11 @@ import SwiftUI @Instantiable public struct LoggedInView: View, Instantiable { + public init(userManager: UserManager, profileViewBuilder: Instantiator) { + self.userManager = userManager + self.profileViewBuilder = profileViewBuilder + } + public var body: some View { ... // A logged in user experience } @@ -249,6 +261,11 @@ public struct LoggedInView: View, Instantiable { @Instantiable public struct ProfileView: View, Instantiable { + public init(userVendor: UserVendor, editProfileViewBuilder: Instantiator) { + self.userVendor = userVendor + self.editProfileViewBuilder = editProfileViewBuilder + } + public var body: some View { ... // A profile viewing experience } @@ -260,6 +277,10 @@ public struct ProfileView: View, Instantiable { @Instantiable public struct EditProfileView: View, Instantiable { + public init(userVendor: UserVendor) { + self.userVendor = userVendor + } + public var body: some View { ... // A profile editing experience } @@ -279,6 +300,10 @@ The [`Instantiator`](Sources/SafeDI/DelayedInstantiation/Instantiator.swift) typ ```swift @Instantiable public struct MyApp: App, Instantiable { + public init(contentViewInstantiator: Instantiator) { + self.contentViewInstantiator = contentViewInstantiator + } + public var body: some Scene { WindowGroup { // Returns a new instance of a `ContentView`. @@ -308,6 +333,10 @@ import SwiftUI @Instantiable public struct ParentView: View, Instantiable { + public init(childViewBuilder: ErasedInstantiator<(), AnyView>) { + self.childViewBuilder = childViewBuilder + } + public var body: some View { VStack { Text("Child View") @@ -382,7 +411,7 @@ To install the SafeDI framework into your package with [Swift Package Manager](h ```swift dependencies: [ - .package(url: "https://github.com/dfed/SafeDI", from: "0.7.0"), + .package(url: "https://github.com/dfed/SafeDI", from: "0.8.0"), ] ``` diff --git a/Sources/SafeDICore/Errors/FixableInstantiableError.swift b/Sources/SafeDICore/Errors/FixableInstantiableError.swift index 6340cb79..138a2978 100644 --- a/Sources/SafeDICore/Errors/FixableInstantiableError.swift +++ b/Sources/SafeDICore/Errors/FixableInstantiableError.swift @@ -31,7 +31,13 @@ public enum FixableInstantiableError: DiagnosticError { case dependencyHasTooManyAttributes case dependencyHasInitializer case missingPublicOrOpenAttribute - case missingRequiredInitializer(hasInjectableProperties: Bool) + case missingRequiredInitializer(MissingInitializer) + + public enum MissingInitializer { + case hasOnlyInjectableProperties + case hasInjectableAndNotInjectableProperties + case hasNoInjectableProperties + } public var description: String { switch self { @@ -55,10 +61,13 @@ public enum FixableInstantiableError: DiagnosticError { "Dependency must not have hand-written initializer" case .missingPublicOrOpenAttribute: "@\(InstantiableVisitor.macroName)-decorated type must be `public` or `open`" - case let .missingRequiredInitializer(hasInjectableProperties): - if hasInjectableProperties { + case let .missingRequiredInitializer(missingInitializer): + switch missingInitializer { + case .hasOnlyInjectableProperties: + "@\(InstantiableVisitor.macroName)-decorated type must have a `public` or `open` initializer with a parameter for each @\(Dependency.Source.instantiatedRawValue), @\(Dependency.Source.receivedRawValue), or @\(Dependency.Source.forwardedRawValue)-decorated property." + case .hasInjectableAndNotInjectableProperties: "@\(InstantiableVisitor.macroName)-decorated type must have a `public` or `open` initializer with a parameter for each @\(Dependency.Source.instantiatedRawValue), @\(Dependency.Source.receivedRawValue), or @\(Dependency.Source.forwardedRawValue)-decorated property. Parameters in this initializer that do not correspond to a decorated property must have default values." - } else { + case .hasNoInjectableProperties: "@\(InstantiableVisitor.macroName)-decorated type with no @\(Dependency.Source.instantiatedRawValue), @\(Dependency.Source.receivedRawValue), or @\(Dependency.Source.forwardedRawValue)-decorated properties must have a `public` or `open` initializer that either takes no parameters or has a default value for each parameter." } } diff --git a/Sources/SafeDICore/Models/Initializer.swift b/Sources/SafeDICore/Models/Initializer.swift index 4cd10cb0..b4518107 100644 --- a/Sources/SafeDICore/Models/Initializer.swift +++ b/Sources/SafeDICore/Models/Initializer.swift @@ -155,7 +155,10 @@ public struct Initializer: Codable, Hashable, Sendable { leadingTrivia: .space, trailingTrivia: .space ), - rightOperand: DeclReferenceExprSyntax(baseName: TokenSyntax.identifier(dependency.property.label)) + rightOperand: DeclReferenceExprSyntax( + baseName: TokenSyntax.identifier(dependency.property.label), + trailingTrivia: dependency == dependencies.last ? .newline : nil + ) ))) ) } diff --git a/Sources/SafeDICore/Models/Property.swift b/Sources/SafeDICore/Models/Property.swift index 2885bbeb..adda1151 100644 --- a/Sources/SafeDICore/Models/Property.swift +++ b/Sources/SafeDICore/Models/Property.swift @@ -83,7 +83,10 @@ public struct Property: Codable, Hashable, Comparable, Sendable { )) if let attributes { for attribute in attributes { - AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier(attribute))) + AttributeSyntax( + attributeName: IdentifierTypeSyntax(name: .identifier(attribute)), + trailingTrivia: .space + ) } } }, diff --git a/Sources/SafeDICore/Visitors/InstantiableVisitor.swift b/Sources/SafeDICore/Visitors/InstantiableVisitor.swift index 73f283b3..7574b531 100644 --- a/Sources/SafeDICore/Visitors/InstantiableVisitor.swift +++ b/Sources/SafeDICore/Visitors/InstantiableVisitor.swift @@ -33,14 +33,14 @@ public final class InstantiableVisitor: SyntaxVisitor { public override func visit(_ node: VariableDeclSyntax) -> SyntaxVisitorContinueKind { guard - declarationType.isTypeDefinition - && node.modifiers.staticModifier == nil + declarationType.isTypeDefinition, + node.modifiers.staticModifier == nil else { return .skipChildren } // Check attributes and extract dependency source. let dependencySources = node.attributes.dependencySources - guard dependencySources.isEmpty || dependencySources.count == 1 else { + if dependencySources.count > 1 { diagnostics.append(Diagnostic( node: node.attributes, error: FixableInstantiableError.dependencyHasTooManyAttributes, @@ -51,7 +51,6 @@ public final class InstantiableVisitor: SyntaxVisitor { ), ] )) - return .skipChildren } guard let dependencySource = dependencySources.first?.source else { // This dependency is not part of the DI system. diff --git a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift index 7ab61108..d6fba790 100644 --- a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift +++ b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift @@ -106,20 +106,29 @@ public struct InstantiableMacro: MemberMacro { .contains(where: { $0.isValid(forFulfilling: visitor.dependencies) }) guard hasMemberwiseInitializerForInjectableProperties else { if visitor.uninitializedNonOptionalPropertyNames.isEmpty { - var initializer = Initializer.generateRequiredInitializer( - for: visitor.dependencies, - declarationType: concreteDeclaration.declType + var membersWithInitializer = declaration.memberBlock.members + membersWithInitializer.insert( + MemberBlockItemSyntax( + leadingTrivia: .newline, + decl: Initializer.generateRequiredInitializer( + for: visitor.dependencies, + declarationType: concreteDeclaration.declType + ), + trailingTrivia: .newline + ), + at: membersWithInitializer.startIndex ) - initializer.leadingTrivia = Trivia(stringLiteral: """ - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - - """) - return [DeclSyntax(initializer)] - + generateForwardedProperties(from: forwardedProperties) + context.diagnose(Diagnostic( + node: Syntax(declaration.memberBlock), + error: FixableInstantiableError.missingRequiredInitializer(.hasOnlyInjectableProperties), + changes: [ + .replace( + oldNode: Syntax(declaration.memberBlock.members), + newNode: Syntax(membersWithInitializer) + ), + ] + )) + return [] } else { var membersWithInitializer = declaration.memberBlock.members membersWithInitializer.insert( @@ -137,7 +146,9 @@ public struct InstantiableMacro: MemberMacro { // TODO: Create separate fixit if just `public` or `open` are missing. context.diagnose(Diagnostic( node: Syntax(declaration.memberBlock), - error: FixableInstantiableError.missingRequiredInitializer(hasInjectableProperties: !visitor.dependencies.isEmpty), + error: FixableInstantiableError.missingRequiredInitializer( + visitor.dependencies.isEmpty ? .hasNoInjectableProperties : .hasInjectableAndNotInjectableProperties + ), changes: [ .replace( oldNode: Syntax(declaration.memberBlock.members), diff --git a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift index 3597d175..d81b97bc 100644 --- a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift +++ b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift @@ -44,78 +44,272 @@ import SafeDICore } } - // MARK: Generation tests + // MARK: Error tests + + func test_declaration_throwsErrorWhenOnProtocol() { + assertMacro { + """ + @Instantiable + public protocol ExampleService {} + """ + } diagnostics: { + """ + @Instantiable + ┬──────────── + ╰─ 🛑 @Instantiable must decorate an extension on a type or a class, struct, or actor declaration + public protocol ExampleService {} + """ + } + } + + func test_declaration_throwsErrorWhenOnEnum() { + assertMacro { + """ + @Instantiable + public enum ExampleService: Instantiable {} + """ + } diagnostics: { + """ + @Instantiable + ┬──────────── + ╰─ 🛑 @Instantiable must decorate an extension on a type or a class, struct, or actor declaration + public enum ExampleService: Instantiable {} + """ + } + } + + func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIncludesAShortformOptional() { + assertMacro { + """ + @Instantiable(fulfillingAdditionalTypes: [AnyObject?.self]) + public final class ExampleService: Instantiable {} + """ + } diagnostics: { + """ + @Instantiable(fulfillingAdditionalTypes: [AnyObject?.self]) + ┬────────────────────────────────────────────────────────── + ╰─ 🛑 The argument `fulfillingAdditionalTypes` must not include optionals + public final class ExampleService: Instantiable {} + """ + } + } - func test_declaration_generatesRequiredInitializerWithoutAnyDependenciesOnStruct() { + func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIncludesALongformOptional() { assertMacro { + """ + @Instantiable(fulfillingAdditionalTypes: [Optional.self]) + public final class ExampleService: Instantiable {} + """ + } diagnostics: { + """ + @Instantiable(fulfillingAdditionalTypes: [Optional.self]) + ┬─────────────────────────────────────────────────────────────────── + ╰─ 🛑 The argument `fulfillingAdditionalTypes` must not include optionals + public final class ExampleService: Instantiable {} + """ + } + } + + func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIsAPropertyReference() { + assertMacro { + """ + let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] + @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) + public final class ExampleService: Instantiable {} + """ + } diagnostics: { + """ + let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] + @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) + ┬────────────────────────────────────────────────────────────────── + ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array + public final class ExampleService: Instantiable {} + """ + } + } + + func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIsAClosure() { + assertMacro { + """ + @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) + public final class ExampleService: Instantiable {} + """ + } diagnostics: { + """ + @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) + ┬─────────────────────────────────────────────────────────────── + ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array + public final class ExampleService: Instantiable {} + """ + } + } + + func test_extension_throwsErrorWhenFulfillingAdditionalTypesIsAPropertyReference() { + assertMacro { + """ + let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] + @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) + extension ExampleService: Instantiable { + public static func instantiate() -> ExampleService { fatalError() } + } + """ + } diagnostics: { + """ + let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] + @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) + ┬────────────────────────────────────────────────────────────────── + ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array + extension ExampleService: Instantiable { + public static func instantiate() -> ExampleService { fatalError() } + } + """ + } + } + + func test_extension_throwsErrorWhenFulfillingAdditionalTypesIsAClosure() { + assertMacro { + """ + @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) + extension ExampleService: Instantiable { + public static func instantiate() -> ExampleService { fatalError() } + } + """ + } diagnostics: { + """ + @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) + ┬─────────────────────────────────────────────────────────────── + ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array + extension ExampleService: Instantiable { + public static func instantiate() -> ExampleService { fatalError() } + } + """ + } + } + + func test_extension_throwsErrorWhenMoreThanOneInstantiateMethodForSameType() { + assertMacro { + """ + @Instantiable + extension ExampleService: Instantiable { + public static func instantiate() -> ExampleService { fatalError() } + public static func instantiate(user: User) -> ExampleService { fatalError() } + } + """ + } diagnostics: { + """ + @Instantiable + ┬──────────── + ╰─ 🛑 @Instantiable-decorated extension must have a single `instantiate(…)` method that returns `ExampleService` + extension ExampleService: Instantiable { + public static func instantiate() -> ExampleService { fatalError() } + public static func instantiate(user: User) -> ExampleService { fatalError() } + } + """ + } + } + + // MARK: FixIt tests + + func test_declaration_fixit_generatesRequiredInitializerWithoutAnyDependenciesOnStruct() { + assertMacro { + """ + @Instantiable + public struct ExampleService: Instantiable { + } + """ + } diagnostics: { """ @Instantiable public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init() {} + } """ } expansion: { """ public struct ExampleService: Instantiable { + public init() {} - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } } """ } } - func test_declaration_generatesRequiredInitializerWithoutAnyDependenciesOnClass() { + func test_declaration_fixit_generatesRequiredInitializerWithoutAnyDependenciesOnClass() { assertMacro { """ @Instantiable public class ExampleService: Instantiable { + } + """ + } diagnostics: { + """ + @Instantiable + public class ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + } + """ + } fixes: { + """ + @Instantiable + public class ExampleService: Instantiable { + public init() {} + } """ } expansion: { """ public class ExampleService: Instantiable { + public init() {} - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } } """ } } - func test_declaration_generatesRequiredInitializerWithoutAnyDependenciesOnActor() { + func test_declaration_fixit_generatesRequiredInitializerWithoutAnyDependenciesOnActor() { assertMacro { """ @Instantiable public actor ExampleService: Instantiable { + } + """ + } diagnostics: { + """ + @Instantiable + public actor ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + } + """ + } fixes: { + """ + @Instantiable + public actor ExampleService: Instantiable { + public init() {} + } """ } expansion: { """ public actor ExampleService: Instantiable { + public init() {} - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } } """ } } - func test_declaration_doesNotGenerateRequiredInitializerWithoutDependenciesIfItAlreadyExists() { + func test_declaration_doesNotGenerateFixitWithoutDependenciesIfItAlreadyExists() { assertMacro { """ @Instantiable @@ -132,7 +326,7 @@ import SafeDICore } } - func test_declaration_generatesRequiredInitializerWithoutAnyDependenciesAndInitializedVariable() { + func test_declaration_fixit_generatesRequiredInitializerWithoutAnyDependenciesAndInitializedVariable() { assertMacro { """ @Instantiable @@ -140,24 +334,36 @@ import SafeDICore var initializedVariable = "test" } """ - } expansion: { + } diagnostics: { + """ + @Instantiable + public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + var initializedVariable = "test" + } + """ + } fixes: { """ + @Instantiable public struct ExampleService: Instantiable { + public init() {} + var initializedVariable = "test" + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init() {} - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } + var initializedVariable = "test" } """ } } - func test_declaration_generatesRequiredInitializerWithoutAnyDependenciesAndVariableWithAccessor() { + func test_declaration_fixit_generatesRequiredInitializerWithoutAnyDependenciesAndVariableWithAccessor() { assertMacro { """ @Instantiable @@ -165,24 +371,36 @@ import SafeDICore var initializedVariable { "test" } } """ - } expansion: { + } diagnostics: { """ + @Instantiable public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer var initializedVariable { "test" } + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init() {} - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } + var initializedVariable { "test" } + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init() {} + + var initializedVariable { "test" } } """ } } - func test_declaration_generatesRequiredInitializerEvenWhenPropertyDecoratedWithUnknownMacro() { + func test_declaration_fixit_generatesRequiredInitializerEvenWhenPropertyDecoratedWithUnknownMacro() { assertMacro { """ @Instantiable @@ -190,25 +408,40 @@ import SafeDICore @Instantiated @Unknown let instantiatedA: InstantiatedA } """ + } diagnostics: { + """ + @Instantiable + public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated @Unknown let instantiatedA: InstantiatedA + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + + @Instantiated @Unknown let instantiatedA: InstantiatedA + } + """ } expansion: { """ - public struct ExampleService: Instantiable {@Unknown - let instantiatedA: InstantiatedA + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + }@Unknown - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA - } + let instantiatedA: InstantiatedA } """ } } - func test_declaration_generatesRequiredInitializerEvenWhenPropertyDecoratedWithUnknownMacroInIfConfig() { + func test_declaration_fixit_generatesRequiredInitializerEvenWhenPropertyDecoratedWithUnknownMacroInIfConfig() { assertMacro { """ @Instantiable @@ -220,19 +453,41 @@ import SafeDICore let instantiatedA: InstantiatedA } """ - } expansion: { + } diagnostics: { """ + @Instantiable public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated + #if DEBUG + @Unknown + #endif let instantiatedA: InstantiatedA + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA - } + @Instantiated + #if DEBUG + @Unknown + #endif + let instantiatedA: InstantiatedA + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + let instantiatedA: InstantiatedA } """ } @@ -387,50 +642,116 @@ import SafeDICore """ public struct ExampleService: Instantiable { let instantiatedA: InstantiatedA - - let nonInjectedProperty: Int - - public init(instantiatedA: InstantiatedA, nonInjectedProperty: Int = 5) { - self.instantiatedA = instantiatedA - self.nonInjectedProperty = nonInjectedProperty - } + + let nonInjectedProperty: Int + + public init(instantiatedA: InstantiatedA, nonInjectedProperty: Int = 5) { + self.instantiatedA = instantiatedA + self.nonInjectedProperty = nonInjectedProperty + } + } + """ + } + } + + func test_declaration_fixit_generatesRequiredInitializerWithDependencies() { + assertMacro { + """ + @Instantiable + public struct ExampleService: Instantiable { + @Instantiated + let instantiatedA: InstantiatedA + } + """ + } diagnostics: { + """ + @Instantiable + public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated + let instantiatedA: InstantiatedA + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + + @Instantiated + let instantiatedA: InstantiatedA + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + let instantiatedA: InstantiatedA } """ } } - func test_declaration_generatesRequiredInitializerWithDependencies() { + func test_declaration_fixit_generatesRequiredInitializerWithDependenciesWhenNestedTypesHaveUninitializedProperties() { assertMacro { """ @Instantiable - public struct ExampleService: Instantiable { + public final class ExampleService: Instantiable { @Instantiated let instantiatedA: InstantiatedA + + public enum NestedEnum { + // This won't compile but we should still generate an initializer. + let uninitializedProperty: Any + } + public struct NestedStruct { + let uninitializedProperty: Any + } + public actor NestedActor { + let uninitializedProperty: Any + } + public final class NestedClass { + let uninitializedProperty: Any + } } """ - } expansion: { + } diagnostics: { """ - public struct ExampleService: Instantiable { + @Instantiable + public final class ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated let instantiatedA: InstantiatedA - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA + public enum NestedEnum { + // This won't compile but we should still generate an initializer. + let uninitializedProperty: Any + } + public struct NestedStruct { + let uninitializedProperty: Any + } + public actor NestedActor { + let uninitializedProperty: Any + } + public final class NestedClass { + let uninitializedProperty: Any } } """ - } - } - - func test_declaration_generatesRequiredInitializerWithDependenciesWhenNestedTypesHaveUninitializedProperties() { - assertMacro { + } fixes: { """ @Instantiable public final class ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + @Instantiated let instantiatedA: InstantiatedA @@ -452,6 +773,9 @@ import SafeDICore } expansion: { """ public final class ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } let instantiatedA: InstantiatedA public enum NestedEnum { @@ -467,21 +791,12 @@ import SafeDICore public final class NestedClass { let uninitializedProperty: Any } - - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA - } } """ } } - func test_declaration_generatesRequiredInitializerWithDependenciesWhenPropertyHasInitializerAndNoType() { + func test_declaration_fixit_generatesRequiredInitializerWithDependenciesWhenPropertyHasInitializerAndNoType() { assertMacro { """ @Instantiable @@ -492,27 +807,47 @@ import SafeDICore let initializedProperty = 5 } """ - } expansion: { + } diagnostics: { + """ + @Instantiable + public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated + let instantiatedA: InstantiatedA + + let initializedProperty = 5 + } + """ + } fixes: { """ + @Instantiable public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + + @Instantiated let instantiatedA: InstantiatedA let initializedProperty = 5 + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + let instantiatedA: InstantiatedA - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA - } + let initializedProperty = 5 } """ } } - func test_declaration_generatesRequiredInitializerWithDependenciesWhenPropertyHasInitializerAndType() { + func test_declaration_fixit_generatesRequiredInitializerWithDependenciesWhenPropertyHasInitializerAndType() { assertMacro { """ @Instantiable @@ -523,27 +858,47 @@ import SafeDICore let initializedProperty: Int = 5 } """ - } expansion: { + } diagnostics: { + """ + @Instantiable + public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated + let instantiatedA: InstantiatedA + + let initializedProperty: Int = 5 + } + """ + } fixes: { """ + @Instantiable public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + + @Instantiated let instantiatedA: InstantiatedA let initializedProperty: Int = 5 + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + let instantiatedA: InstantiatedA - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA - } + let initializedProperty: Int = 5 } """ } } - func test_declaration_generatesRequiredInitializerWithDependenciesWhenPropertyIsOptional() { + func test_declaration_fixit_generatesRequiredInitializerWithDependenciesWhenPropertyIsOptional() { assertMacro { """ @Instantiable @@ -554,27 +909,47 @@ import SafeDICore var optionalProperty: Int? } """ - } expansion: { + } diagnostics: { """ + @Instantiable public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated let instantiatedA: InstantiatedA var optionalProperty: Int? + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA - } + @Instantiated + let instantiatedA: InstantiatedA + + var optionalProperty: Int? + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + let instantiatedA: InstantiatedA + + var optionalProperty: Int? } """ } } - func test_declaration_generatesRequiredInitializerWithDependenciesWhenPropertyIsStatic() { + func test_declaration_fixit_generatesRequiredInitializerWithDependenciesWhenPropertyIsStatic() { assertMacro { """ @Instantiable @@ -586,28 +961,50 @@ import SafeDICore public static let staticProperty: Int } """ - } expansion: { + } diagnostics: { """ + @Instantiable public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated let instantiatedA: InstantiatedA // This won't compile but we should still generate an initializer. public static let staticProperty: Int + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatedA: InstantiatedA) { - self.instantiatedA = instantiatedA - } + @Instantiated + let instantiatedA: InstantiatedA + + // This won't compile but we should still generate an initializer. + public static let staticProperty: Int + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(instantiatedA: InstantiatedA) { + self.instantiatedA = instantiatedA + } + let instantiatedA: InstantiatedA + + // This won't compile but we should still generate an initializer. + public static let staticProperty: Int } """ } } - func test_declaration_generatesRequiredInitializerWhenDependencyMissingFromInit() { + func test_declaration_fixit_generatesRequiredInitializerWhenDependencyMissingFromInit() { assertMacro { """ @Instantiable @@ -626,28 +1023,67 @@ import SafeDICore let receivedB: ReceivedB } """ - } expansion: { + } diagnostics: { + """ + @Instantiable + public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + public init(forwardedA: ForwardedA, receivedA: ReceivedA) { + self.forwardedA = forwardedA + self.receivedA = receivedA + receivedB = ReceivedB() + } + + @Forwarded + let forwardedA: ForwardedA + @Received + let receivedA: ReceivedA + @Received + let receivedB: ReceivedB + } + """ + } fixes: { """ + @Instantiable public struct ExampleService: Instantiable { + public init(forwardedA: ForwardedA, receivedA: ReceivedA, receivedB: ReceivedB) { + self.forwardedA = forwardedA + self.receivedA = receivedA + self.receivedB = receivedB + } + public init(forwardedA: ForwardedA, receivedA: ReceivedA) { self.forwardedA = forwardedA self.receivedA = receivedA receivedB = ReceivedB() } + + @Forwarded let forwardedA: ForwardedA + @Received let receivedA: ReceivedA + @Received let receivedB: ReceivedB + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(forwardedA: ForwardedA, receivedA: ReceivedA, receivedB: ReceivedB) { + self.forwardedA = forwardedA + self.receivedA = receivedA + self.receivedB = receivedB + } - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(forwardedA: ForwardedA, receivedA: ReceivedA, receivedB: ReceivedB) { + public init(forwardedA: ForwardedA, receivedA: ReceivedA) { self.forwardedA = forwardedA self.receivedA = receivedA - self.receivedB = receivedB + receivedB = ReceivedB() } + let forwardedA: ForwardedA + let receivedA: ReceivedA + let receivedB: ReceivedB public typealias ForwardedProperties = ForwardedA } @@ -655,11 +1091,11 @@ import SafeDICore } } - func test_declaration_generatesForwardedPropertiesWhenThereAreMultipleForwardedProperties() { + func test_declaration_fixit_generatesInitWithForwardedPropertiesWhenThereAreMultipleForwardedProperties() { assertMacro { """ @Instantiable - public final class UserService { + public final class UserService: Instantiable { @Forwarded let userID: String @@ -670,84 +1106,120 @@ import SafeDICore } diagnostics: { """ @Instantiable - ┬──────────── - ╰─ 🛑 @Instantiable-decorated type or extension must declare conformance to `Instantiable` - ✏️ Declare conformance to `Instantiable` - public final class UserService { + public final class UserService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Forwarded + let userID: String + + @Forwarded + let userName: String + } + """ + } fixes: { + """ + @Instantiable + public final class UserService: Instantiable { + public init(userID: String, userName: String) { + self.userID = userID + self.userName = userName + } + + @Forwarded + let userID: String + @Forwarded + let userName: String + } + """ + } expansion: { + """ + public final class UserService: Instantiable { + public init(userID: String, userName: String) { + self.userID = userID + self.userName = userName + } let userID: String + let userName: String + public typealias ForwardedProperties = (userID: String, userName: String) + } + """ + } + } + + func test_declaration_fixit_generatesRequiredInitializerWithClosureDependency() { + assertMacro { + """ + @Instantiable + public struct ExampleService: Instantiable { + @Forwarded + let closure: () -> Void + } + """ + } diagnostics: { + """ + @Instantiable + public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer @Forwarded - let userName: String + let closure: () -> Void } """ } fixes: { """ @Instantiable - public final class UserService: Instantiable { - @Forwarded - let userID: String + public struct ExampleService: Instantiable { + public init(closure: @escaping () -> Void) { + self.closure = closure + } @Forwarded - let userName: String + let closure: () -> Void } """ } expansion: { """ - public final class UserService: Instantiable { - let userID: String - let userName: String - - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(userID: String, userName: String) { - self.userID = userID - self.userName = userName - } + public struct ExampleService: Instantiable { + public init(closure: @escaping () -> Void) { + self.closure = closure + } + let closure: () -> Void - public typealias ForwardedProperties = (userID: String, userName: String) + public typealias ForwardedProperties = () -> Void } """ } } - func test_declaration_generatesRequiredInitializerWithClosureDependency() { + func test_declaration_fixit_generatesFixitForRequiredInitializerWithSendableClosureDependency() { assertMacro { """ @Instantiable public struct ExampleService: Instantiable { @Forwarded - let closure: () -> Void + let closure: @Sendable () -> Void } """ - } expansion: { + } diagnostics: { """ + @Instantiable public struct ExampleService: Instantiable { - let closure: () -> Void - - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(closure: @escaping () -> Void) { - self.closure = closure - } - - public typealias ForwardedProperties = () -> Void + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Forwarded + let closure: @Sendable () -> Void } """ - } - } - - func test_declaration_generatesRequiredInitializerWithSendableClosureDependency() { - assertMacro { + } fixes: { """ @Instantiable public struct ExampleService: Instantiable { + public init(closure: @escaping @Sendable () -> Void) { + self.closure = closure + } + @Forwarded let closure: @Sendable () -> Void } @@ -755,24 +1227,18 @@ import SafeDICore } expansion: { """ public struct ExampleService: Instantiable { + public init(closure: @escaping @Sendable () -> Void) { + self.closure = closure + } let closure: @Sendable () -> Void - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(closure: @escaping @Sendable () -> Void) { - self.closure = closure - } - public typealias ForwardedProperties = @Sendable () -> Void } """ } } - func test_declaration_generatesRequiredInitializerWhenInstantiatorDependencyMissingFromInit() { + func test_declaration_fixit_generatesRequiredInitializerWhenInstantiatorDependencyMissingFromInit() { assertMacro { """ @Instantiable @@ -781,19 +1247,35 @@ import SafeDICore private let instantiatableAInstantiator: Instantiator } """ - } expansion: { + } diagnostics: { """ + @Instantiable public struct ExampleService: Instantiable { + ╰─ 🛑 @Instantiable-decorated type must have a `public` or `open` initializer with a parameter for each @Instantiated, @Received, or @Forwarded-decorated property. + ✏️ Add required initializer + @Instantiated private let instantiatableAInstantiator: Instantiator + } + """ + } fixes: { + """ + @Instantiable + public struct ExampleService: Instantiable { + public init(instantiatableAInstantiator: Instantiator) { + self.instantiatableAInstantiator = instantiatableAInstantiator + } - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init(instantiatableAInstantiator: Instantiator) { - self.instantiatableAInstantiator = instantiatableAInstantiator - } + @Instantiated + private let instantiatableAInstantiator: Instantiator + } + """ + } expansion: { + """ + public struct ExampleService: Instantiable { + public init(instantiatableAInstantiator: Instantiator) { + self.instantiatableAInstantiator = instantiatableAInstantiator + } + private let instantiatableAInstantiator: Instantiator } """ } @@ -847,177 +1329,12 @@ import SafeDICore } } - // MARK: Error tests - - func test_declaration_throwsErrorWhenOnProtocol() { - assertMacro { - """ - @Instantiable - public protocol ExampleService {} - """ - } diagnostics: { - """ - @Instantiable - ┬──────────── - ╰─ 🛑 @Instantiable must decorate an extension on a type or a class, struct, or actor declaration - public protocol ExampleService {} - """ - } - } - - func test_declaration_throwsErrorWhenOnEnum() { - assertMacro { - """ - @Instantiable - public enum ExampleService: Instantiable {} - """ - } diagnostics: { - """ - @Instantiable - ┬──────────── - ╰─ 🛑 @Instantiable must decorate an extension on a type or a class, struct, or actor declaration - public enum ExampleService: Instantiable {} - """ - } - } - - func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIncludesAShortformOptional() { - assertMacro { - """ - @Instantiable(fulfillingAdditionalTypes: [AnyObject?.self]) - public final class ExampleService: Instantiable {} - """ - } diagnostics: { - """ - @Instantiable(fulfillingAdditionalTypes: [AnyObject?.self]) - ┬────────────────────────────────────────────────────────── - ╰─ 🛑 The argument `fulfillingAdditionalTypes` must not include optionals - public final class ExampleService: Instantiable {} - """ - } - } - - func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIncludesALongformOptional() { - assertMacro { - """ - @Instantiable(fulfillingAdditionalTypes: [Optional.self]) - public final class ExampleService: Instantiable {} - """ - } diagnostics: { - """ - @Instantiable(fulfillingAdditionalTypes: [Optional.self]) - ┬─────────────────────────────────────────────────────────────────── - ╰─ 🛑 The argument `fulfillingAdditionalTypes` must not include optionals - public final class ExampleService: Instantiable {} - """ - } - } - - func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIsAPropertyReference() { - assertMacro { - """ - let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] - @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) - public final class ExampleService: Instantiable {} - """ - } diagnostics: { - """ - let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] - @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) - ┬────────────────────────────────────────────────────────────────── - ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array - public final class ExampleService: Instantiable {} - """ - } - } - - func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIsAClosure() { - assertMacro { - """ - @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) - public final class ExampleService: Instantiable {} - """ - } diagnostics: { - """ - @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) - ┬─────────────────────────────────────────────────────────────── - ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array - public final class ExampleService: Instantiable {} - """ - } - } - - func test_extension_throwsErrorWhenFulfillingAdditionalTypesIsAPropertyReference() { - assertMacro { - """ - let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] - @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) - extension ExampleService: Instantiable { - public static func instantiate() -> ExampleService { fatalError() } - } - """ - } diagnostics: { - """ - let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] - @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) - ┬────────────────────────────────────────────────────────────────── - ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array - extension ExampleService: Instantiable { - public static func instantiate() -> ExampleService { fatalError() } - } - """ - } - } - - func test_extension_throwsErrorWhenFulfillingAdditionalTypesIsAClosure() { - assertMacro { - """ - @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) - extension ExampleService: Instantiable { - public static func instantiate() -> ExampleService { fatalError() } - } - """ - } diagnostics: { - """ - @Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }()) - ┬─────────────────────────────────────────────────────────────── - ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array - extension ExampleService: Instantiable { - public static func instantiate() -> ExampleService { fatalError() } - } - """ - } - } - - func test_extension_throwsErrorWhenMoreThanOneInstantiateMethodForSameType() { - assertMacro { - """ - @Instantiable - extension ExampleService: Instantiable { - public static func instantiate() -> ExampleService { fatalError() } - public static func instantiate(user: User) -> ExampleService { fatalError() } - } - """ - } diagnostics: { - """ - @Instantiable - ┬──────────── - ╰─ 🛑 @Instantiable-decorated extension must have a single `instantiate(…)` method that returns `ExampleService` - extension ExampleService: Instantiable { - public static func instantiate() -> ExampleService { fatalError() } - public static func instantiate(user: User) -> ExampleService { fatalError() } - } - """ - } - } - - // MARK: FixIt tests - func test_declaration_fixit_addsFixitWhenNoConformancesDeclared() { assertMacro { """ @Instantiable public final class ExampleService { + public init() {} } """ } diagnostics: { @@ -1027,25 +1344,20 @@ import SafeDICore ╰─ 🛑 @Instantiable-decorated type or extension must declare conformance to `Instantiable` ✏️ Declare conformance to `Instantiable` public final class ExampleService { + public init() {} } """ } fixes: { """ @Instantiable public final class ExampleService: Instantiable { + public init() {} } """ } expansion: { """ public final class ExampleService: Instantiable { - - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } + public init() {} } """ } @@ -1056,6 +1368,7 @@ import SafeDICore """ @Instantiable public final class ExampleService: CustomStringConvertible { + public init() {} public var description: String { "ExampleService" } } """ @@ -1066,6 +1379,7 @@ import SafeDICore ╰─ 🛑 @Instantiable-decorated type or extension must declare conformance to `Instantiable` ✏️ Declare conformance to `Instantiable` public final class ExampleService: CustomStringConvertible { + public init() {} public var description: String { "ExampleService" } } """ @@ -1073,46 +1387,34 @@ import SafeDICore """ @Instantiable public final class ExampleService: CustomStringConvertible, Instantiable { + public init() {} public var description: String { "ExampleService" } } """ } expansion: { """ public final class ExampleService: CustomStringConvertible, Instantiable { + public init() {} public var description: String { "ExampleService" } - - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } } """ } } - func test_declaration_doesNotAddFixitWhenRetroactiveInstantiableConformanceMissing() { + func test_declaration_doesNotAddFixitWhenRetroactiveInstantiableConformanceExists() { assertMacro { """ @Instantiable public final class ExampleService: @retroactive Instantiable, @retroactive CustomStringConvertible { + public init() {} public var description: String { "ExampleService" } } """ } expansion: { """ public final class ExampleService: @retroactive Instantiable, @retroactive CustomStringConvertible { + public init() {} public var description: String { "ExampleService" } - - // A generated initializer that has one argument per SafeDI-injected property. - // Because this initializer is generated by a Swift Macro, it can not be used by other Swift Macros. - // As a result, this initializer can not be used within a #Preview macro closure. - // This initializer is generated only because you have not written an appropriate initializer yourself. - // Copy/pasting this generated initializer into your code will enable this initializer to be used within other Swift Macros. - public init() { - } } """ } @@ -1651,6 +1953,7 @@ import SafeDICore public struct ExampleService: Instantiable { public init(receivedA: ReceivedA) { self.receivedA = receivedA + // If the following properties were decorated with the @Instantiated, @Received, or @Forwarded macros, had default values, or were written as computed properties, this initializer could have been auto-generated by the @Instantiable macro. uninitializedProperty = <#T##assign_uninitializedProperty#> } @@ -1666,6 +1969,7 @@ import SafeDICore public struct ExampleService: Instantiable { public init(receivedA: ReceivedA) { self.receivedA = receivedA + // If the following properties were decorated with the @Instantiated, @Received, or @Forwarded macros, had default values, or were written as computed properties, this initializer could have been auto-generated by the @Instantiable macro. uninitializedProperty = <#T##assign_uninitializedProperty#> } @@ -1710,6 +2014,7 @@ import SafeDICore public struct ExampleService: Instantiable { public init(receivedA: ReceivedA) { self.receivedA = receivedA + // If the following properties were decorated with the @Instantiated, @Received, or @Forwarded macros, had default values, or were written as computed properties, this initializer could have been auto-generated by the @Instantiable macro. uninitializedProperty1 = <#T##assign_uninitializedProperty1#> uninitializedProperty2 = <#T##assign_uninitializedProperty2#> @@ -1730,6 +2035,7 @@ import SafeDICore public struct ExampleService: Instantiable { public init(receivedA: ReceivedA) { self.receivedA = receivedA + // If the following properties were decorated with the @Instantiated, @Received, or @Forwarded macros, had default values, or were written as computed properties, this initializer could have been auto-generated by the @Instantiable macro. uninitializedProperty1 = <#T##assign_uninitializedProperty1#> uninitializedProperty2 = <#T##assign_uninitializedProperty2#>