Sourcery icon indicating copy to clipboard operation
Sourcery copied to clipboard

Unstable AutoMockable output when protocols have associated type

Open magauran opened this issue 6 months ago • 4 comments

Hey 👋, thank you for this fantastic tool! Sourcery has significantly improved our workflow.

Issue

I've encountered an issue that first appeared in Sourcery version 2.2.5. The problem occurs during mock generation with AutoMockable for protocols that contain associatedtype. The generated mocks are inconsistent — sometimes the mock is generated correctly, and other times it isn’t generated at all.

Below, you can found a small code example that illustrates this behavior.

protocol ProtocolWithAssociatedType {
    associatedtype Value: AnotherProtocol

    var value: Value { get }
}

// sourcery:AutoMockable
protocol AnotherProtocol {
    func foo() -> String
}

Sometimes, the mock is generated correctly:

class AnotherProtocolMock: AnotherProtocol {
    //MARK: - foo

    var fooStringCallsCount = 0
    var fooStringCalled: Bool {
        return fooStringCallsCount > 0
    }
    var fooStringReturnValue: String!
    var fooStringClosure: (() -> String)?

    func foo() -> String {
        fooStringCallsCount += 1
        if let fooStringClosure = fooStringClosure {
            return fooStringClosure()
        } else {
            return fooStringReturnValue
        }
    }
}

but in about 50% of runs (without any changes to the code) mock is not generated.

Investigation

This issue appeared after commit 7153768c655c5e2435323cd08f01ed8b2d13f765, specifically due to changes in ParserResultsComposed.swift

/// Map associated types
associatedTypes.forEach {
    typeMap[$0.key] = $0.value.type
}

These lines insert associated types to typeMap in a random order, as the order in Dictionary is not guaranteed. And it seems likely that this code conflicts with unifyTypes function, which removes types with duplicate globalName from the typeMap.

If the original type is listed before the associated type in the typeMap, the generation works correctly:

[
    "AnotherProtocol": AnotherProtocol.self, // type with sourcery annotation
    "ProtocolWithAssociatedType": ProtocolWithAssociatedType.self,
    "Value": AnotherProtocol.self // associated type
]

However, if the associated type is listed before the original type, the original type gets removed from the typeMap, and the mock is not generated.

[
    "ProtocolWithAssociatedType": ProtocolWithAssociatedType.self,
    "Value": AnotherProtocol.self, // associated type
    "AnotherProtocol": AnotherProtocol.self // type with sourcery annotation
]

magauran avatar Aug 14 '24 08:08 magauran