Repository: Matejkob/swift-spyable (swift)

Issue:
Using macro in test only
**Is your feature request related to a problem? Please describe.**
How to use the macro only in test code ? I don't want to use the annotation on production code.

**Describe the solution you'd like**
I'd like to be able to use the macro only in test targets.

**Describe alternatives you've considered**
 I tried multiple things like using a typealias or a second protocol refining the one I'd like to spy but I can't make it work.

Pull request:
See https://github.com/Matejkob/swift-spyable/issues/41 for conversation leading up to this PR

Patch:
diff --git a/README.md b/README.md
index 86b12ca..a00a226 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,49 @@ func testFetchConfig() async throws {
 }
 ```
 
+## Advanced Usage
+
+### Restricting the Availability of Spies
+
+If you wish, you can limit where Spyable's generated code can be used from. This can be useful if you want to prevent spies from being used in user-facing production code.
+
+To enforce this restriction, supply the `behindPreprocessorFlag: String?` parameter of Spyable, like this:
+
+```swift
+@Spyable(behindPreprocessorFlag: "DEBUG")
+protocol MyService {
+    func fetchData() async
+}
+```
+With `behindPreprocessorFlag` specified as DEBUG, the macro expansion will be wrapped in an #if DEBUG preprocessor macro, preventing its use anywhere that the DEBUG flag is not defined:
+
+```swift
+#if DEBUG
+class MyServiceSpy: MyService {
+    var fetchDataCallsCount = 0
+    var fetchDataCalled: Bool {
+        return fetchDataCallsCount > 0
+    }
+    var fetchDataClosure: (() async -> Void)?
+        func fetchData() async {
+        fetchDataCallsCount += 1
+        await fetchDataClosure?()
+    }
+}
+#endif
+```
+This example uses "DEBUG", but you can specify any flags you wish. For example, you could use "TESTS", and then make the macro available to your test targets by adding the "TESTS" flag to them under the "Active Compilation Conditions" custom build flags.
+
+#### A Caveat Regarding Xcode Previews
+Limiting spy availability may become restrictive if you intend to use spies in your Xcode Previews. If you intend to use spies with previews and you also want to prevent spies from being used in production code, the advised course of action is to split off previews into their own target where you can define a custom flag (Ex: "SPIES_ENABLED"), like this:
+
+```
+-- MyFeature (`SPIES_ENABLED = 0`)
+---- MyFeatureTests (`SPIES_ENABLED = 1`)
+---- MyFeaturePreviews (`SPIES_ENABLED = 1`)
+```
+Like the example before, you would specify this custom build flag under "Active Compilation Conditions" of both the `MyFeatureTests` and `MyFeaturePreviews` targets.
+
 ## Examples
 
 This repo comes with an example of how to use Spyable. You can find it [here](./Examples).
diff --git a/Sources/Spyable/Spyable.swift b/Sources/Spyable/Spyable.swift
index aef24d7..3045c32 100644
--- a/Sources/Spyable/Spyable.swift
+++ b/Sources/Spyable/Spyable.swift
@@ -48,11 +48,12 @@
 ///     }
 /// }
 /// ```
+/// - Parameter behindPreprocessorFlag: This defaults to nil, and can be optionally supplied to wrap the generated code in a preprocessor flag like `#if DEBUG`.
 ///
 /// - NOTE: The `@Spyable` macro should only be applied to protocols. Applying it to other
 ///         declarations will result in an error.
 @attached(peer, names: suffixed(Spy))
-public macro Spyable() =
+public macro Spyable(behindPreprocessorFlag: String? = nil) =
   #externalMacro(
     module: "SpyableMacro",
     type: "SpyableMacro"
diff --git a/Sources/SpyableMacro/Macro/SpyableMacro.swift b/Sources/SpyableMacro/Macro/SpyableMacro.swift
index af3b7ae..abcaf87 100644
--- a/Sources/SpyableMacro/Macro/SpyableMacro.swift
+++ b/Sources/SpyableMacro/Macro/SpyableMacro.swift
@@ -11,6 +11,8 @@ import SwiftSyntaxMacros
 /// that implements the given protocol and records interactions with its methods and properties. The resulting
 /// class is added to the source file, thus "expanding" the `@Spyable` attribute into this new declaration.
 ///
+/// Additionally, if a `String` value is passed via the `behindPreprocessorFlag` parameter, this will be used to wrap the entire declaration in an preprocessor `IfConfigDeclSyntax`, to allow users to restrict the exposure of their generated spies.
+///
 /// Example:
 /// ```swift
 /// @Spyable
@@ -32,6 +34,40 @@ public enum SpyableMacro: PeerMacro {
 
     let spyClassDeclaration = try spyFactory.classDeclaration(for: protocolDeclaration)
 
-    return [DeclSyntax(spyClassDeclaration)]
+    if let flag = declaration.preprocessorFlag {
+      return [
+        DeclSyntax(
+          IfConfigDeclSyntax(
+            clauses: IfConfigClauseListSyntax {
+              IfConfigClauseSyntax(
+                poundKeyword: .poundIfToken(),
+                condition: ExprSyntax(stringLiteral: flag),
+                elements: .statements(
+                  CodeBlockItemListSyntax {
+                    DeclSyntax(spyClassDeclaration)
+                  }
+                )
+              )
+            }
+          )
+        )
+      ]
+    } else {
+      return [DeclSyntax(spyClassDeclaration)]
+    }
+  }
+}
+
+private extension DeclSyntaxProtocol {
+  /// - Returns: The preprocessor `flag` parameter that can be optionally provided via `@Spyable(flag:)`.
+  var preprocessorFlag: String? {
+    self.as(ProtocolDeclSyntax.self)?.attributes.first?
+      .as(AttributeSyntax.self)?.arguments?
+      .as(LabeledExprListSyntax.self)?.first {
+        $0.label?.text == "behindPreprocessorFlag"
+      }?
+      .as(LabeledExprSyntax.self)?.expression
+      .as(StringLiteralExprSyntax.self)?.segments.first?
+      .as(StringSegmentSyntax.self)?.content.text
   }
 }


Test patch:
diff --git a/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift b/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift
index 102a1b6..80606f5 100644
--- a/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift
+++ b/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift
@@ -138,4 +138,29 @@ final class UT_SpyableMacro: XCTestCase {
       macros: sut
     )
   }
+
+  func testMacroWithFlag() {
+    let protocolDeclaration = """
+      public protocol ServiceProtocol {
+          var variable: Bool? { get set }
+      }
+      """
+    assertMacroExpansion(
+      """
+      @Spyable(behindPreprocessorFlag: "CUSTOM")
+      \(protocolDeclaration)
+      """,
+      expandedSource: """
+
+      \(protocolDeclaration)
+
+      #if CUSTOM
+      class ServiceProtocolSpy: ServiceProtocol {
+          var variable: Bool?
+      }
+      #endif
+      """,
+      macros: sut
+    )
+  }
 }