diff --git a/Documentation/Manual.md b/Documentation/Manual.md index 51af55bc..c33dd269 100644 --- a/Documentation/Manual.md +++ b/Documentation/Manual.md @@ -508,7 +508,7 @@ Set `mockConditionalCompilation` to `nil` to generate mocks without conditional Each `@Instantiable` type gets a `mock()` static method that builds its full dependency subtree. Types with `generateMock: true` must not also contain a hand-written `mock()` method — the macro will emit an error with a fix-it to remove `generateMock: true`. If you provide your own `mock()` method (without `generateMock: true`), parent types that instantiate the child will call `ChildType.mock(...)` instead of `ChildType(...)` when constructing it, threading mock parameters through your custom method. Note that mocks defined in separate extensions are not detected; the method must be in the `@Instantiable`-decorated declaration body. -Your user-defined `mock()` method must be `public` (or `open`) and must accept parameters for each of the type's `@Instantiated`, `@Received`, and `@Forwarded` dependencies. It may also accept additional parameters with default values. On concrete type declarations the return type must be `Self` or the type name; on extension-based `@Instantiable` types the return type must match the extended type (e.g. `-> Container`), mirroring the corresponding `instantiate()` method. The `@Instantiable` macro validates these requirements and provides fix-its for any issues. +Your user-defined `mock()` method must be `public` (or `open`) and must accept parameters for each of the type's `@Instantiated`, `@Received`, and `@Forwarded` dependencies. It may also accept additional parameters with default values. On concrete type declarations the return type must be `Self`, the type name, or a type listed in `fulfillingAdditionalTypes`; on extension-based `@Instantiable` types the return type must match the extended type (e.g. `-> Container`) or a `fulfillingAdditionalTypes` entry, mirroring the corresponding `instantiate()` method. The `@Instantiable` macro validates these requirements and provides fix-its for any issues. ```swift #if DEBUG diff --git a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift index 2490640e..c71a9402 100644 --- a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift +++ b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift @@ -432,9 +432,11 @@ public struct InstantiableMacro: MemberMacro { let typeName = concreteDeclaration.name.text let instantiableTypeStrippingGenerics = visitor.instantiableType?.strippingGenerics let mockReturnType = mockSyntax.signature.returnClause?.type.typeDescription + let additionalTypesStrippingGenerics = (visitor.additionalInstantiables ?? []).map(\.strippingGenerics) let isSelfReturnType = mockReturnType == .simple(name: "Self", generics: []) let returnTypeMatchesTypeName = isSelfReturnType || mockReturnType?.strippingGenerics == instantiableTypeStrippingGenerics + || additionalTypesStrippingGenerics.contains(where: { $0 == mockReturnType?.strippingGenerics }) if !returnTypeMatchesTypeName { var fixedMockSyntax = mockSyntax if let existingReturnClause = mockSyntax.signature.returnClause { @@ -620,7 +622,9 @@ public struct InstantiableMacro: MemberMacro { var seenMockReturnTypes = [TypeDescription: FunctionDeclSyntax]() for mockFunction in allMockFunctions { let mockReturnType = mockFunction.signature.returnClause?.type.typeDescription + let additionalTypesStrippingGenerics = (visitor.additionalInstantiables ?? []).map(\.strippingGenerics) let returnTypeMatchesExtendedType = mockReturnType?.strippingGenerics == extendedTypeStrippingGenerics + || additionalTypesStrippingGenerics.contains(where: { $0 == mockReturnType?.strippingGenerics }) if !returnTypeMatchesExtendedType { var fixedMockFunction = mockFunction if let existingReturnClause = mockFunction.signature.returnClause { diff --git a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift index 19347fe8..2e8f19cc 100644 --- a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift +++ b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift @@ -4933,6 +4933,58 @@ import Testing ) } + @Test + func mockMethodReturningFulfillingAdditionalTypeProducesNoDiagnostic() { + assertMacroExpansion( + """ + @Instantiable(fulfillingAdditionalTypes: [MyServiceProtocol.self]) + public struct MyService: MyServiceProtocol, Instantiable { + public init() {} + + public static func mock() -> MyServiceProtocol { + MyService() + } + } + """, + expandedSource: """ + public struct MyService: MyServiceProtocol, Instantiable { + public init() {} + + public static func mock() -> MyServiceProtocol { + MyService() + } + } + """, + macros: instantiableTestMacros, + ) + } + + @Test + func extension_mockMethodReturningFulfillingAdditionalTypeProducesNoDiagnostic() { + assertMacroExpansion( + """ + @Instantiable(fulfillingAdditionalTypes: [MyServiceProtocol.self]) + extension MyService: Instantiable { + public static func instantiate() -> MyService { MyService() } + + public static func mock() -> MyServiceProtocol { + MyService() + } + } + """, + expandedSource: """ + extension MyService: Instantiable { + public static func instantiate() -> MyService { MyService() } + + public static func mock() -> MyServiceProtocol { + MyService() + } + } + """, + macros: instantiableTestMacros, + ) + } + @Test func extension_mockMethodNotPublicProducesDiagnostic() { assertMacroExpansion(