Repository: Matejkob/swift-spyable (swift)

Issue:
Missing support for protocol associatedtypes
**Is your feature request related to a problem? Please describe.**
I noticed @spyable does not work on protocols with associated types

**Description**

```swift
@Spyable
protocol Foo {
    associatedtype Key: Hashable
    associatedtype Value

    func setValue(_ value: Value, forKey key: Key)
}
```

Generated Implementation:

```swift
 class FooSpy: Foo {
     var setValueForKeyCallsCount = 0
     var setValueForKeyCalled: Bool {
         return setValueForKeyCallsCount > 0
     }
     var setValueForKeyReceivedArguments: (value: Value, key: Key)?
     var setValueForKeyReceivedInvocations: [(value: Value, key: Key)] = []
     var setValueForKeyClosure: ((Value, Key) -> Void)?
     func setValue(_ value: Value, forKey key: Key) {
         setValueForKeyCallsCount += 1
         setValueForKeyReceivedArguments = (value, key)
         setValueForKeyReceivedInvocations.append((value, key))
         setValueForKeyClosure?(value, key)
     }
 }
```

Generated Implementation throws protocol conformation error

Pull request:
**Description:** 
[Issue-67](https://github.com/Matejkob/swift-spyable/issues/67)

**FIX**
This PR adds support for associatedtypes
- Fixing the conformance issue by updating class declaration with the associatedtypes
- Added Unit tests

Patch:
diff --git a/Sources/SpyableMacro/Factories/AssociatedtypeFactory.swift b/Sources/SpyableMacro/Factories/AssociatedtypeFactory.swift
new file mode 100644
index 0000000..379b836
--- /dev/null
+++ b/Sources/SpyableMacro/Factories/AssociatedtypeFactory.swift
@@ -0,0 +1,43 @@
+import SwiftSyntax
+import SwiftSyntaxBuilder
+
+/// The `AssociatedtypeFactory` struct is responsible for creating GenericParameterClauseSyntax
+///
+/// The factory constructs the representation by using associatedtype name and inheritedType
+/// of the associatedtypeDecl to GenericParameterSyntax
+///
+/// For example, given the associatedtype declarations:
+/// ```swift
+/// associatedtype Key: Hashable
+/// associatedtype Value
+/// ```
+/// the `AssociatedtypeFactory` generates the following text:
+/// ```
+/// <Key: Hashable, Value>
+/// ```
+
+struct AssociatedtypeFactory {
+  func constructGenericParameterClause(associatedtypeDeclList: [AssociatedTypeDeclSyntax]) -> GenericParameterClauseSyntax? {
+    guard !associatedtypeDeclList.isEmpty else { return nil }
+
+    var genericParameterList = [GenericParameterSyntax]()
+    for (i, associatedtypeDecl) in associatedtypeDeclList.enumerated() {
+      let associatedtypeName = associatedtypeDecl.name
+      let typeInheritance: InheritanceClauseSyntax? = associatedtypeDecl.inheritanceClause
+      let inheritedType = typeInheritance?.inheritedTypes.first?.type
+      let hasTrailingComma: Bool = i < associatedtypeDeclList.count - 1
+      let genericParameter = GenericParameterSyntax(
+        name: associatedtypeName,
+        colon: inheritedType != nil ? typeInheritance?.colon : nil,
+        inheritedType: typeInheritance?.inheritedTypes.first?.type,
+        trailingComma: hasTrailingComma ? .commaToken() : nil
+      )
+
+      genericParameterList.append(genericParameter)
+    }
+
+    return GenericParameterClauseSyntax(
+      parameters: GenericParameterListSyntax(genericParameterList)
+    )
+  }
+}
diff --git a/Sources/SpyableMacro/Factories/SpyFactory.swift b/Sources/SpyableMacro/Factories/SpyFactory.swift
index d0b9e90..742a525 100644
--- a/Sources/SpyableMacro/Factories/SpyFactory.swift
+++ b/Sources/SpyableMacro/Factories/SpyFactory.swift
@@ -80,6 +80,7 @@ import SwiftSyntaxBuilder
 /// }
 /// ```
 struct SpyFactory {
+  private let associatedtypeFactory = AssociatedtypeFactory()
   private let variablePrefixFactory = VariablePrefixFactory()
   private let variablesImplementationFactory = VariablesImplementationFactory()
   private let callsCountFactory = CallsCountFactory()
@@ -94,6 +95,11 @@ struct SpyFactory {
   func classDeclaration(for protocolDeclaration: ProtocolDeclSyntax) throws -> ClassDeclSyntax {
     let identifier = TokenSyntax.identifier(protocolDeclaration.name.text + "Spy")
 
+    let assosciatedtypeDeclarations = protocolDeclaration.memberBlock.members.compactMap {
+      $0.decl.as(AssociatedTypeDeclSyntax.self)
+    }
+    let genericParameterClause = associatedtypeFactory.constructGenericParameterClause(associatedtypeDeclList: assosciatedtypeDeclarations)
+
     let variableDeclarations = protocolDeclaration.memberBlock.members
       .compactMap { $0.decl.as(VariableDeclSyntax.self) }
 
@@ -102,6 +108,7 @@ struct SpyFactory {
 
     return try ClassDeclSyntax(
       name: identifier,
+      genericParameterClause: genericParameterClause,
       inheritanceClause: InheritanceClauseSyntax {
         InheritedTypeSyntax(
           type: IdentifierTypeSyntax(name: protocolDeclaration.name)


Test patch:
diff --git a/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift b/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
index e140dff..52f0b27 100644
--- a/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
+++ b/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
@@ -287,3 +287,50 @@ final class UT_SpyFactory: XCTestCase {
     )
   }
 }
+
+// - MARK: Handle Protocol Associated types
+
+extension UT_SpyFactory {
+  func testDeclarationAssociatedtype() throws {
+    let declaration = DeclSyntax(
+      """
+      protocol Foo {
+          associatedtype Key: Hashable
+      }
+      """
+    )
+    let protocolDeclaration = try XCTUnwrap(ProtocolDeclSyntax(declaration))
+
+    let result = try SpyFactory().classDeclaration(for: protocolDeclaration)
+
+    assertBuildResult(
+      result,
+      """
+      class FooSpy<Key: Hashable>: Foo {
+      }
+      """
+    )
+  }
+
+  func testDeclarationAssociatedtypeKeyValue() throws {
+    let declaration = DeclSyntax(
+      """
+      protocol Foo {
+          associatedtype Key: Hashable
+          associatedtype Value
+      }
+      """
+    )
+    let protocolDeclaration = try XCTUnwrap(ProtocolDeclSyntax(declaration))
+
+    let result = try SpyFactory().classDeclaration(for: protocolDeclaration)
+
+    assertBuildResult(
+      result,
+      """
+      class FooSpy<Key: Hashable, Value>: Foo {
+      }
+      """
+    )
+  }
+}