Repository: Matejkob/swift-spyable (swift)

Issue:
Support `any`
Thank you for creating this library 🤝

<br>

**Describe the bug**

Regarding `any`, which will definitely be implemented in swift in the future
-> https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md

<br>

We are unable to conduct tests because the code generated by Spyable macro lacks the 'any' attachment on the protocol.
If a new Xcode 15.3~ or flag is turned on...

<img width="1125" alt="Screenshot 2024-04-02 at 18 32 25" src="https://github.com/Matejkob/swift-spyable/assets/24838521/22f392fc-6aca-487f-b027-3f7bbe2cca54">


<br><br>

**To Reproduce**

When we use 

- `Xcode 15.3~`

or 

- `-enable-upcoming-feature ExistentialAny` flag


<br>

**Expected behavior**

Can run tests

<br>

**Additional context**

We are rewriting mock code using `Spyable` in a large-scale project. 
Your support with this task would be greatly appreciated in the nearly future.
Thanks 👍

Pull request:
Related #90

Hello, thank you for developing a great OSS!
I fixed a bug that occurred when function arguments contained an existential type.

The following code causes an error.

```swift
@Spyable
protocol ViewModelProtocol {
    func foo(model: any ModelProtocol)
}

class ViewModelProtocolSpy: ViewModelProtocol {
    var fooModelCallsCount = 0
    var fooModelCalled: Bool {
        return fooModelCallsCount > 0
    }
    var fooModelReceivedModel: any ModelProtocol? // ❌ Optional 'any' type must be written '(any ModelProtocol)?'
    var fooModelReceivedInvocations: [any ModelProtocol] = []
    var fooModelClosure: ((any ModelProtocol) -> Void)?
    func foo(model: any ModelProtocol) {
        fooModelCallsCount += 1
        fooModelReceivedModel = (model)
        fooModelReceivedInvocations.append((model))
        fooModelClosure?(model)
    }
}
```

It needs to be like this.

```swift
class ViewModelProtocolSpy: ViewModelProtocol {
    var fooModelCallsCount = 0
    var fooModelCalled: Bool {
        return fooModelCallsCount > 0
    }
    var fooModelReceivedModel: (any ModelProtocol)? // ✅
    var fooModelReceivedInvocations: [any ModelProtocol] = []
    var fooModelClosure: ((any ModelProtocol) -> Void)?
    func foo(model: any ModelProtocol) {
        fooModelCallsCount += 1
        fooModelReceivedModel = (model)
        fooModelReceivedInvocations.append((model))
        fooModelClosure?(model)
    }
}
```

Patch:
diff --git a/Sources/SpyableMacro/Factories/ReceivedArgumentsFactory.swift b/Sources/SpyableMacro/Factories/ReceivedArgumentsFactory.swift
index 3a43f6b..9ee89b4 100644
--- a/Sources/SpyableMacro/Factories/ReceivedArgumentsFactory.swift
+++ b/Sources/SpyableMacro/Factories/ReceivedArgumentsFactory.swift
@@ -72,6 +72,15 @@ struct ReceivedArgumentsFactory {
           ),
           questionMark: .postfixQuestionMarkToken()
         )
+      } else if onlyParameterType.is(SomeOrAnyTypeSyntax.self) {
+        variableType = OptionalTypeSyntax(
+          wrappedType: TupleTypeSyntax(
+            elements: TupleTypeElementListSyntax {
+              TupleTypeElementSyntax(type: onlyParameterType)
+            }
+          ),
+          questionMark: .postfixQuestionMarkToken()
+        )
       } else {
         variableType = OptionalTypeSyntax(
           wrappedType: onlyParameterType,


Test patch:
diff --git a/Tests/SpyableMacroTests/Factories/UT_ReceivedArgumentsFactory.swift b/Tests/SpyableMacroTests/Factories/UT_ReceivedArgumentsFactory.swift
index 4189d55..949e530 100644
--- a/Tests/SpyableMacroTests/Factories/UT_ReceivedArgumentsFactory.swift
+++ b/Tests/SpyableMacroTests/Factories/UT_ReceivedArgumentsFactory.swift
@@ -23,6 +23,14 @@ final class UT_ReceivedArgumentsFactory: XCTestCase {
     )
   }
 
+  func testVariableDeclarationSingleExistentialTypeArgument() throws {
+    try assertProtocolFunction(
+      withFunctionDeclaration: "func foo(bar: any BarProtocol)",
+      prefixForVariable: "_prefix_",
+      expectingVariableDeclaration: "var _prefix_ReceivedBar: (any BarProtocol)?"
+    )
+  }
+
   func testVariableDeclarationSingleArgumentDoubleParameterName() throws {
     try assertProtocolFunction(
       withFunctionDeclaration: "func foo(firstName secondName: (String, Int))",
diff --git a/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift b/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
index 4a6ffb4..6845ca9 100644
--- a/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
+++ b/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
@@ -65,6 +65,33 @@ final class UT_SpyFactory: XCTestCase {
     )
   }
 
+  func testDeclarationExistentialTypeArguments() throws {
+    try assertProtocol(
+      withDeclaration: """
+        protocol ViewModelProtocol {
+            func foo(model: any ModelProtocol)
+        }
+        """,
+      expectingClassDeclaration: """
+        class ViewModelProtocolSpy: ViewModelProtocol {
+            var fooModelCallsCount = 0
+            var fooModelCalled: Bool {
+                return fooModelCallsCount > 0
+            }
+            var fooModelReceivedModel: (any ModelProtocol)?
+            var fooModelReceivedInvocations: [any ModelProtocol] = []
+            var fooModelClosure: ((any ModelProtocol) -> Void)?
+            func foo(model: any ModelProtocol) {
+                fooModelCallsCount += 1
+                fooModelReceivedModel = (model)
+                fooModelReceivedInvocations.append((model))
+                fooModelClosure?(model)
+            }
+        }
+        """
+    )
+  }
+
   func testDeclarationEscapingAutoClosureArgument() throws {
     try assertProtocol(
       withDeclaration: """