From 1f8f1855e2d47c0b90c2e075d93cfc7e334afc29 Mon Sep 17 00:00:00 2001 From: Dan Date: Sun, 5 Apr 2026 07:14:05 -0700 Subject: [PATCH] Accept fulfillingAdditionalTypes as valid mock return types in macro The macro's return type validation now checks additionalInstantiables in both the concrete and extension branches, matching the generator's behavior. Previously only the generator accepted additional types, causing the macro to emit an incorrect return type error. Co-Authored-By: Claude Opus 4.6 (1M context) --- Documentation/Manual.md | 2 +- .../Macros/InstantiableMacro.swift | 4 ++ .../InstantiableMacroTests.swift | 52 +++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) 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(