Skip to content

[Feature] Support Anyany Sendable conversion for @unchecked Sendable protocols mocks #133

@gage-halverson-fetch

Description

@gage-halverson-fetch

Use Case

I'm trying to use @MockedMembers to generate mocks for protocols that: - Conform to Sendable - Have methods that return or accept Any or [String: Any] When @MockedMembers generates the mock, it creates MockReturningNonParameterizedMethod<[String: Any]>, which fails to compile because Any doesn't conform to Sendable.

Feature Proposal

I would like to see @MockedMembers be able to convert Any types to any Sendable when:

  1. The mock class conforms to @unchecked Sendable
  2. The protocol method uses Any, [Any], or [String: Any]

Example Protocol

public protocol UserDefaultsProtocol: Sendable {
    func dictionaryRepresentation() -> [String: Any]
}

Current Behavior (Test code Fails to Compile not the mock itself)

Generated mock code:

@MockedMembers
public final class UserDefaultsMock: UserDefaultsProtocol, @unchecked Sendable {
    func dictionaryRepresentation() -> [String: Any]
}

What @MockedMembers generates internally:
private let __dictionaryRepresentation = MockReturningNonParameterizedMethod<
    [String: Any]  // ❌ Problem: Any is not Sendable
>.makeMethod(
    exposedMethodDescription: MockImplementationDescription(
        type: UserDefaultsMock.self,
        member: "_dictionaryRepresentation"
    )
)

Public interface generated:
public var _dictionaryRepresentation: MockReturningNonParameterizedMethod<
    [String: Any]
> {
    self.__dictionaryRepresentation.method
}

public func dictionaryRepresentation() -> [String: Any] {
    let returnValue = self.__dictionaryRepresentation.invoke()
    return returnValue
}

Test code that fails:
let userDefaults = UserDefaultsMock()
let testDefaults: [String: Sendable] = [:]
userDefaults._dictionaryRepresentation.implementation = .returns(testDefaults)
// ❌ Error: Type 'Any' does not conform to the 'Sendable' protocol

Desired Behavior

Mock declaration (same as current):

@MockedMembers
public final class UserDefaultsMock: UserDefaultsProtocol, @unchecked Sendable {
}

What @MockedMembers should generate internally:
private let __dictionaryRepresentation = MockReturningNonParameterizedMethod<
    [String: any Sendable]  // ✅ Automatically replaced Any with any Sendable
>.makeMethod(
    exposedMethodDescription: MockImplementationDescription(
        type: UserDefaultsMock.self,
        member: "_dictionaryRepresentation"
    )
)

Public interface with Sendable type:
public var _dictionaryRepresentation: MockReturningNonParameterizedMethod<
    [String: any Sendable]  // ✅ Uses any Sendable
> {
    self.__dictionaryRepresentation.method
}

Protocol implementation with cast:
public func dictionaryRepresentation() -> [String: Any] {
    let returnValue = self.__dictionaryRepresentation.invoke()
    return returnValue as [String: Any]  // ✅ Cast back to protocol signature
}

Test code that works:
let userDefaults = UserDefaultsMock()
let testDefaults: [String: Sendable] = ["key": "value"]
userDefaults._dictionaryRepresentation.implementation = .returns(testDefaults)
// ✅ Compiles successfully

Proposed Solutions

Option 1: Automatic Detection (Recommended)

Automatically convert Any → any Sendable when:

  • The mock class has @unchecked Sendable conformance
  • A protocol method uses Any, [Any], or [String: Any]
@MockedMembers  // No configuration needed
public final class UserDefaultsMock: UserDefaultsProtocol, @unchecked Sendable {
    // Automatically generates Sendable-safe mock code
}

Pros:

  • No additional configuration
  • Works automatically for the common case
  • Clear convention: @unchecked Sendable signals the macro to use any Sendable

Cons:

  • Less explicit control

Option 2: Macro Parameter

Add explicit control via macro parameter:
This would most likely be bubbled up to the top @Mocked macro.

@MockedMembers(convertAnyToSendable: true)
public final class UserDefaultsMock: UserDefaultsProtocol, @unchecked Sendable {
    // Explicitly requests Any → any Sendable conversion
}

Pros:

  • Explicit opt-in
  • Clear intent in code
  • Could default to false for backward compatibility

Cons:

  • Extra boilerplate
  • Easy to forget

Alternatives Considered

I've tried:

  1. Manually implementing the entire mock - This defeats the purpose of @MockedMembers
  2. Adding @preconcurrency to protocol methods - Doesn't work with generated mock code
  3. Current workaround - Manually implementing just the problematic method:

This requires manually expanding the entire macro, which is tedious and error-prone.

Additional Context

No response

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions