Repository: Matejkob/swift-spyable (swift)

Issue:
Use protocol's access control on the spy's declaration
**Is your feature request related to a problem? Please describe.**
If you use the macro on a public protocol declaration from module A and you have a unit test in another module that you'd like to use the protocol spy from module A, you don't have visibility to the Spy in module B since it's declared as internal.

**Describe the solution you'd like**
The generated spy should use the same access control as the attached protocol's.

**Describe alternatives you've considered**
You could import the module as testable

**Additional context**

Pull request:
**This PR introduces access level inheritance for generated spies.**

#### **Key Changes**
Generated spies now inherit the protocol’s access control (`public`, `package`, `internal`, `private`, and `fileprivate`). For `private` protocols, spies are generated with `fileprivate` access to meet Swift’s requirements.

Spies include an initializer with the same access level as the protocol, enabling their creation outside the current module.

#### **Implementation Details**
The core concept of access level inheritance is implemented using a syntax rewriter:

```swift
final class AccessLevelModifierRewriter: SyntaxRewriter {
  let newAccessLevel: DeclModifierSyntax

  init(newAccessLevel: DeclModifierSyntax) {
    // Ensure private protocols result in fileprivate members for compatibility
    if newAccessLevel.name.text == TokenSyntax.keyword(.private).text {
      self.newAccessLevel = DeclModifierSyntax(name: .keyword(.fileprivate))
    } else {
      self.newAccessLevel = newAccessLevel
    }
  }

  override func visit(_ node: DeclModifierListSyntax) -> DeclModifierListSyntax {
    if node.parent?.is(FunctionParameterSyntax.self) == true {
      return node
    }
    return DeclModifierListSyntax { newAccessLevel }
  }
}
```

The `AccessLevelModifierRewriter` is applied after the spy class is created, ensuring that all members inherit the access level consistently:

```swift
if let accessLevel = extractor.extractAccessLevel(from: protocolDeclaration) {
  let accessLevelModifierRewriter = AccessLevelModifierRewriter(newAccessLevel: accessLevel)

  spyClassDeclaration =
    accessLevelModifierRewriter
    .rewrite(spyClassDeclaration)
    .cast(ClassDeclSyntax.self)
}
```

This guarantees that the entire spy class, including all members, always reflects the protocol’s access level.

Patch:
diff --git a/Examples/Sources/AccessLevels.swift b/Examples/Sources/AccessLevels.swift
new file mode 100644
index 0000000..de85dd5
--- /dev/null
+++ b/Examples/Sources/AccessLevels.swift
@@ -0,0 +1,146 @@
+import Spyable
+
+// MARK: - Open
+
+// Only classes and overridable class members can be declared 'open'.
+
+// MARK: - Public
+
+@Spyable
+public protocol PublicServiceProtocol {
+  var name: String { get }
+  var anyProtocol: any Codable { get set }
+  var secondName: String? { get }
+  var address: String! { get }
+  var added: () -> Void { get set }
+  var removed: (() -> Void)? { get set }
+
+  func initialize(name: String, _ secondName: String?)
+  func fetchConfig(arg: UInt8) async throws -> [String: String]
+  func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
+  func save(name: any Codable, surname: any Codable)
+  func insert(name: (any Codable)?, surname: (any Codable)?)
+  func append(name: (any Codable) -> (any Codable)?)
+  func get() async throws -> any Codable
+  func read() -> String!
+  func wrapDataInArray<T>(_ data: T) -> [T]
+}
+
+func testPublicServiceProtocol() {
+  let spy = PublicServiceProtocolSpy()
+
+  spy.name = "Spy"
+}
+
+// MARK: - Package
+
+@Spyable
+package protocol PackageServiceProtocol {
+  var name: String { get }
+  var anyProtocol: any Codable { get set }
+  var secondName: String? { get }
+  var address: String! { get }
+  var added: () -> Void { get set }
+  var removed: (() -> Void)? { get set }
+
+  func initialize(name: String, _ secondName: String?)
+  func fetchConfig(arg: UInt8) async throws -> [String: String]
+  func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
+  func save(name: any Codable, surname: any Codable)
+  func insert(name: (any Codable)?, surname: (any Codable)?)
+  func append(name: (any Codable) -> (any Codable)?)
+  func get() async throws -> any Codable
+  func read() -> String!
+  func wrapDataInArray<T>(_ data: T) -> [T]
+}
+
+func testPackageServiceProtocol() {
+  let spy = PackageServiceProtocolSpy()
+
+  spy.name = "Spy"
+}
+
+// MARK: - Internal
+
+@Spyable
+internal protocol InternalServiceProtocol {
+  var name: String { get }
+  var anyProtocol: any Codable { get set }
+  var secondName: String? { get }
+  var address: String! { get }
+  var added: () -> Void { get set }
+  var removed: (() -> Void)? { get set }
+
+  func initialize(name: String, _ secondName: String?)
+  func fetchConfig(arg: UInt8) async throws -> [String: String]
+  func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
+  func save(name: any Codable, surname: any Codable)
+  func insert(name: (any Codable)?, surname: (any Codable)?)
+  func append(name: (any Codable) -> (any Codable)?)
+  func get() async throws -> any Codable
+  func read() -> String!
+  func wrapDataInArray<T>(_ data: T) -> [T]
+}
+
+func testInternalServiceProtocol() {
+  let spy = InternalServiceProtocolSpy()
+
+  spy.name = "Spy"
+}
+
+// MARK: - Fileprivate
+
+@Spyable
+// swiftformat:disable:next
+private protocol FileprivateServiceProtocol {
+  var name: String { get }
+  var anyProtocol: any Codable { get set }
+  var secondName: String? { get }
+  var address: String! { get }
+  var added: () -> Void { get set }
+  var removed: (() -> Void)? { get set }
+
+  func initialize(name: String, _ secondName: String?)
+  func fetchConfig(arg: UInt8) async throws -> [String: String]
+  func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
+  func save(name: any Codable, surname: any Codable)
+  func insert(name: (any Codable)?, surname: (any Codable)?)
+  func append(name: (any Codable) -> (any Codable)?)
+  func get() async throws -> any Codable
+  func read() -> String!
+  func wrapDataInArray<T>(_ data: T) -> [T]
+}
+
+func testFileprivateServiceProtocol() {
+  let spy = FileprivateServiceProtocolSpy()
+
+  spy.name = "Spy"
+}
+
+// MARK: - Private
+
+@Spyable
+private protocol PrivateServiceProtocol {
+  var name: String { get }
+  var anyProtocol: any Codable { get set }
+  var secondName: String? { get }
+  var address: String! { get }
+  var added: () -> Void { get set }
+  var removed: (() -> Void)? { get set }
+
+  func initialize(name: String, _ secondName: String?)
+  func fetchConfig(arg: UInt8) async throws -> [String: String]
+  func fetchData(_ name: (String, count: Int)) async -> (() -> Void)
+  func save(name: any Codable, surname: any Codable)
+  func insert(name: (any Codable)?, surname: (any Codable)?)
+  func append(name: (any Codable) -> (any Codable)?)
+  func get() async throws -> any Codable
+  func read() -> String!
+  func wrapDataInArray<T>(_ data: T) -> [T]
+}
+
+func testPrivateServiceProtocol() {
+  let spy = PrivateServiceProtocolSpy()
+
+  spy.name = "Spy"
+}
diff --git a/Sources/SpyableMacro/Extractors/Extractor.swift b/Sources/SpyableMacro/Extractors/Extractor.swift
index 84b2fa6..ddfc20a 100644
--- a/Sources/SpyableMacro/Extractors/Extractor.swift
+++ b/Sources/SpyableMacro/Extractors/Extractor.swift
@@ -2,43 +2,40 @@ import SwiftDiagnostics
 import SwiftSyntax
 import SwiftSyntaxMacros
 
-/// A utility  responsible for extracting specific syntax elements from Swift Syntax.
+/// `Extractor` is a utility designed to analyze and extract specific syntax elements from the protocol declartion.
 ///
-/// This struct provides methods to retrieve detailed syntax elements from abstract syntax trees,
-/// such as protocol declarations and arguments from attribute..
+/// This struct provides methods for working with protocol declarations, access levels,
+/// and attributes, simplifying the task of retrieving and validating syntax information.
 struct Extractor {
   /// Extracts a `ProtocolDeclSyntax` instance from a given declaration.
   ///
-  /// This method takes a declaration conforming to `DeclSyntaxProtocol` and attempts
-  /// to downcast it to `ProtocolDeclSyntax`. If the downcast succeeds, the protocol declaration
-  /// is returned. Otherwise, it emits an error indicating that the operation is only applicable
-  /// to protocol declarations.
+  /// This method ensures that the provided declaration conforms to `ProtocolDeclSyntax`.
+  /// If the declaration is not a protocol, an error is thrown.
   ///
-  /// - Parameter declaration: The declaration to be examined, conforming to `DeclSyntaxProtocol`.
-  /// - Returns: A `ProtocolDeclSyntax` instance if the input declaration is a protocol declaration.
-  /// - Throws: `SpyableDiagnostic.onlyApplicableToProtocol` if the input is not a protocol declaration.
+  /// - Parameter declaration: The declaration to examine, conforming to `DeclSyntaxProtocol`.
+  /// - Returns: A `ProtocolDeclSyntax` instance if the input is a protocol declaration.
+  /// - Throws: `SpyableDiagnostic.onlyApplicableToProtocol` if the input is not a protocol.
   func extractProtocolDeclaration(
     from declaration: DeclSyntaxProtocol
   ) throws -> ProtocolDeclSyntax {
     guard let protocolDeclaration = declaration.as(ProtocolDeclSyntax.self) else {
       throw SpyableDiagnostic.onlyApplicableToProtocol
     }
-
     return protocolDeclaration
   }
 
-  /// Extracts a preprocessor flag value from an attribute if present.
+  /// Extracts a preprocessor flag value from an attribute if present and valid.
   ///
-  /// This method analyzes an `AttributeSyntax` to find an argument labeled `behindPreprocessorFlag`.
-  /// If found, it verifies that the argument's value is a static string literal. It then returns
-  /// this string value. If the specific argument is not found, or if its value is not a static string,
-  /// the method provides relevant diagnostics and returns `nil`.
+  /// This method searches for an argument labeled `behindPreprocessorFlag` within the
+  /// given attribute. If the argument is found, its value is validated to ensure it is
+  /// a static string literal.
   ///
   /// - Parameters:
   ///   - attribute: The attribute syntax to analyze.
-  ///   - context: The macro expansion context in which this operation is performed.
-  /// - Returns: The static string literal value of the `behindPreprocessorFlag` argument if present and valid.
-  /// - Throws: Diagnostic errors for various failure cases, such as the absence of the argument or non-static string values.
+  ///   - context: The macro expansion context in which the operation is performed.
+  /// - Returns: The static string literal value of the `behindPreprocessorFlag` argument,
+  ///   or `nil` if the argument is missing or invalid.
+  /// - Throws: Diagnostic errors if the argument is invalid or absent.
   func extractPreprocessorFlag(
     from attribute: AttributeSyntax,
     in context: some MacroExpansionContext
@@ -84,4 +81,44 @@ struct Extractor {
 
     return literalSegment.content.text
   }
+
+  /// Extracts the access level modifier from a protocol declaration.
+  ///
+  /// This method identifies the first access level modifier present in the protocol
+  /// declaration. Supported access levels include `public`, `internal`, `fileprivate`,
+  /// `private`, and `package`.
+  ///
+  /// - Parameter protocolDeclSyntax: The protocol declaration to analyze.
+  /// - Returns: The `DeclModifierSyntax` representing the access level, or `nil` if no
+  ///   valid access level modifier is found.
+  func extractAccessLevel(from protocolDeclSyntax: ProtocolDeclSyntax) -> DeclModifierSyntax? {
+    protocolDeclSyntax.modifiers.first(where: \.name.isAccessLevelSupportedInProtocol)
+  }
+}
+
+extension TokenSyntax {
+  /// Determines if the token represents a supported access level modifier for protocols.
+  ///
+  /// Supported access levels are:
+  /// - `public`
+  /// - `package`
+  /// - `internal`
+  /// - `fileprivate`
+  /// - `private`
+  ///
+  /// - Returns: `true` if the token matches one of the supported access levels; otherwise, `false`.
+  fileprivate var isAccessLevelSupportedInProtocol: Bool {
+    let supportedAccessLevels: [TokenSyntax] = [
+      .keyword(.public),
+      .keyword(.package),
+      .keyword(.internal),
+      .keyword(.fileprivate),
+      .keyword(.private),
+    ]
+
+    return
+      supportedAccessLevels
+      .map { $0.text }
+      .contains(text)
+  }
 }
diff --git a/Sources/SpyableMacro/Factories/SpyFactory.swift b/Sources/SpyableMacro/Factories/SpyFactory.swift
index aa1518f..954b29a 100644
--- a/Sources/SpyableMacro/Factories/SpyFactory.swift
+++ b/Sources/SpyableMacro/Factories/SpyFactory.swift
@@ -116,6 +116,13 @@ struct SpyFactory {
         )
       },
       memberBlockBuilder: {
+        InitializerDeclSyntax(
+          signature: FunctionSignatureSyntax(
+            parameterClause: FunctionParameterClauseSyntax(parameters: [])
+          ),
+          bodyBuilder: {}
+        )
+
         for variableDeclaration in variableDeclarations {
           try variablesImplementationFactory.variablesDeclarations(
             protocolVariableDeclaration: variableDeclaration
diff --git a/Sources/SpyableMacro/Factories/VariablesImplementationFactory.swift b/Sources/SpyableMacro/Factories/VariablesImplementationFactory.swift
index 9865930..c010512 100644
--- a/Sources/SpyableMacro/Factories/VariablesImplementationFactory.swift
+++ b/Sources/SpyableMacro/Factories/VariablesImplementationFactory.swift
@@ -60,10 +60,10 @@ struct VariablesImplementationFactory {
       {
         let accessorRemovalVisitor = AccessorRemovalVisitor()
         accessorRemovalVisitor.visit(protocolVariableDeclaration)
-        /*
-       var name: String
-       */
       } else {
+        /*
+         var name: String
+        */
         try protocolVariableDeclarationWithGetterAndSetter(binding: binding)
 
         try underlyingVariableDeclaration(binding: binding)
diff --git a/Sources/SpyableMacro/Macro/AccessLevelModifierRewriter.swift b/Sources/SpyableMacro/Macro/AccessLevelModifierRewriter.swift
new file mode 100644
index 0000000..b2552f8
--- /dev/null
+++ b/Sources/SpyableMacro/Macro/AccessLevelModifierRewriter.swift
@@ -0,0 +1,24 @@
+import SwiftSyntax
+
+final class AccessLevelModifierRewriter: SyntaxRewriter {
+  let newAccessLevel: DeclModifierSyntax
+
+  init(newAccessLevel: DeclModifierSyntax) {
+    /// Property / method must be declared `fileprivate` because it matches a requirement in `private` protocol.
+    if newAccessLevel.name.text == TokenSyntax.keyword(.private).text {
+      self.newAccessLevel = DeclModifierSyntax(name: .keyword(.fileprivate))
+    } else {
+      self.newAccessLevel = newAccessLevel
+    }
+  }
+
+  override func visit(_ node: DeclModifierListSyntax) -> DeclModifierListSyntax {
+    if node.parent?.is(FunctionParameterSyntax.self) == true {
+      return node
+    }
+
+    return DeclModifierListSyntax {
+      newAccessLevel
+    }
+  }
+}
diff --git a/Sources/SpyableMacro/Macro/SpyableMacro.swift b/Sources/SpyableMacro/Macro/SpyableMacro.swift
index 2e2827f..cf43624 100644
--- a/Sources/SpyableMacro/Macro/SpyableMacro.swift
+++ b/Sources/SpyableMacro/Macro/SpyableMacro.swift
@@ -32,7 +32,16 @@ public enum SpyableMacro: PeerMacro {
   ) throws -> [DeclSyntax] {
     let protocolDeclaration = try extractor.extractProtocolDeclaration(from: declaration)
 
-    let spyClassDeclaration = try spyFactory.classDeclaration(for: protocolDeclaration)
+    var spyClassDeclaration = try spyFactory.classDeclaration(for: protocolDeclaration)
+
+    if let accessLevel = extractor.extractAccessLevel(from: protocolDeclaration) {
+      let accessLevelModifierRewriter = AccessLevelModifierRewriter(newAccessLevel: accessLevel)
+
+      spyClassDeclaration =
+        accessLevelModifierRewriter
+        .rewrite(spyClassDeclaration)
+        .cast(ClassDeclSyntax.self)
+    }
 
     if let flag = try extractor.extractPreprocessorFlag(from: node, in: context) {
       return [


Test patch:
diff --git a/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift b/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
index 5194adf..687d14c 100644
--- a/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
+++ b/Tests/SpyableMacroTests/Factories/UT_SpyFactory.swift
@@ -10,6 +10,8 @@ final class UT_SpyFactory: XCTestCase {
       withDeclaration: "protocol Foo {}",
       expectingClassDeclaration: """
         class FooSpy: Foo {
+            init() {
+            }
         }
         """
     )
@@ -24,6 +26,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceSpy: Service {
+            init() {
+            }
             var fetchCallsCount = 0
             var fetchCalled: Bool {
                 return fetchCallsCount > 0
@@ -47,6 +51,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ViewModelProtocolSpy: ViewModelProtocol {
+            init() {
+            }
             var fooTextCountCallsCount = 0
             var fooTextCountCalled: Bool {
                 return fooTextCountCallsCount > 0
@@ -74,6 +80,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ViewModelProtocolSpy: ViewModelProtocol {
+            init() {
+            }
             var fooModelCallsCount = 0
             var fooModelCalled: Bool {
                 return fooModelCallsCount > 0
@@ -101,6 +109,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ViewModelProtocolSpy: ViewModelProtocol {
+            init() {
+            }
             var fooModelCallsCount = 0
             var fooModelCalled: Bool {
                 return fooModelCallsCount > 0
@@ -128,6 +138,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ViewModelProtocolSpy: ViewModelProtocol {
+            init() {
+            }
             var fooModelCallsCount = 0
             var fooModelCalled: Bool {
                 return fooModelCallsCount > 0
@@ -155,6 +167,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ViewModelProtocolSpy: ViewModelProtocol {
+            init() {
+            }
             var fooModelCallsCount = 0
             var fooModelCalled: Bool {
                 return fooModelCallsCount > 0
@@ -189,6 +203,8 @@ final class UT_SpyFactory: XCTestCase {
       result,
       """
       class ViewModelProtocolSpy: ViewModelProtocol {
+          init() {
+          }
           var fooTextValueCallsCount = 0
           var fooTextValueCalled: Bool {
               return fooTextValueCallsCount > 0
@@ -221,6 +237,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ViewModelProtocolSpy: ViewModelProtocol {
+            init() {
+            }
             var fooActionCallsCount = 0
             var fooActionCalled: Bool {
                 return fooActionCallsCount > 0
@@ -248,6 +266,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ViewModelProtocolSpy: ViewModelProtocol {
+            init() {
+            }
             var fooActionCallsCount = 0
             var fooActionCalled: Bool {
                 return fooActionCallsCount > 0
@@ -271,6 +291,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class BarSpy: Bar {
+            init() {
+            }
             var printCallsCount = 0
             var printCalled: Bool {
                 return printCallsCount > 0
@@ -299,6 +321,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var fooTextCountCallsCount = 0
             var fooTextCountCalled: Bool {
                 return fooTextCountCallsCount > 0
@@ -331,6 +355,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var fooCallsCount = 0
             var fooCalled: Bool {
                 return fooCallsCount > 0
@@ -367,6 +393,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var fooCallsCount = 0
             var fooCalled: Bool {
                 return fooCallsCount > 0
@@ -395,6 +423,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var fooCallsCount = 0
             var fooCalled: Bool {
                 return fooCallsCount > 0
@@ -423,6 +453,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var data: Data {
                 get {
                     underlyingData
@@ -446,6 +478,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var data: Data?
         }
         """
@@ -461,6 +495,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var data: String!
         }
         """
@@ -476,6 +512,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var data: any Codable {
                 get {
                     underlyingData
@@ -499,6 +537,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class ServiceProtocolSpy: ServiceProtocol {
+            init() {
+            }
             var completion: () -> Void {
                 get {
                     underlyingCompletion
@@ -524,6 +564,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class FooSpy<Key: Hashable>: Foo {
+            init() {
+            }
         }
         """
     )
@@ -539,6 +581,8 @@ final class UT_SpyFactory: XCTestCase {
         """,
       expectingClassDeclaration: """
         class FooSpy<Key: Hashable, Value>: Foo {
+            init() {
+            }
         }
         """
     )
diff --git a/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift b/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift
index 7e2a377..f89f7d0 100644
--- a/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift
+++ b/Tests/SpyableMacroTests/Macro/UT_SpyableMacro.swift
@@ -50,8 +50,10 @@ final class UT_SpyableMacro: XCTestCase {
 
         \(protocolDeclaration)
 
-        class ServiceProtocolSpy: ServiceProtocol {
-            var name: String {
+        public class ServiceProtocolSpy: ServiceProtocol {
+            public init() {
+            }
+            public var name: String {
                 get {
                     underlyingName
                 }
@@ -59,8 +61,8 @@ final class UT_SpyableMacro: XCTestCase {
                     underlyingName = newValue
                 }
             }
-            var underlyingName: (String)!
-            var anyProtocol: any Codable {
+            public var underlyingName: (String)!
+            public var anyProtocol: any Codable {
                 get {
                     underlyingAnyProtocol
                 }
@@ -68,9 +70,10 @@ final class UT_SpyableMacro: XCTestCase {
                     underlyingAnyProtocol = newValue
                 }
             }
-            var underlyingAnyProtocol: (any Codable)!
+            public var underlyingAnyProtocol: (any Codable)!
+            public
             var secondName: String?
-            var added: () -> Void {
+            public var added: () -> Void {
                 get {
                     underlyingAdded
                 }
@@ -78,37 +81,40 @@ final class UT_SpyableMacro: XCTestCase {
                     underlyingAdded = newValue
                 }
             }
-            var underlyingAdded: (() -> Void)!
+            public var underlyingAdded: (() -> Void)!
+            public
             var removed: (() -> Void)?
-            var logoutCallsCount = 0
-            var logoutCalled: Bool {
+            public var logoutCallsCount = 0
+            public var logoutCalled: Bool {
                 return logoutCallsCount > 0
             }
-            var logoutClosure: (() -> Void)?
-            func logout() {
+            public var logoutClosure: (() -> Void)?
+            public func logout() {
                 logoutCallsCount += 1
                 logoutClosure?()
             }
-            var initializeNameSecondNameCallsCount = 0
-            var initializeNameSecondNameCalled: Bool {
+            public var initializeNameSecondNameCallsCount = 0
+            public var initializeNameSecondNameCalled: Bool {
                 return initializeNameSecondNameCallsCount > 0
             }
-            var initializeNameSecondNameReceivedArguments: (name: String, secondName: String?)?
-            var initializeNameSecondNameReceivedInvocations: [(name: String, secondName: String?)] = []
-            var initializeNameSecondNameClosure: ((String, String?) -> Void)?
+            public var initializeNameSecondNameReceivedArguments: (name: String, secondName: String?)?
+            public var initializeNameSecondNameReceivedInvocations: [(name: String, secondName: String?)] = []
+            public var initializeNameSecondNameClosure: ((String, String?) -> Void)?
+            public
             func initialize(name: String, secondName: String?) {
                 initializeNameSecondNameCallsCount += 1
                 initializeNameSecondNameReceivedArguments = (name, secondName)
                 initializeNameSecondNameReceivedInvocations.append((name, secondName))
                 initializeNameSecondNameClosure?(name, secondName)
             }
-            var fetchConfigCallsCount = 0
-            var fetchConfigCalled: Bool {
+            public var fetchConfigCallsCount = 0
+            public var fetchConfigCalled: Bool {
                 return fetchConfigCallsCount > 0
             }
-            var fetchConfigThrowableError: (any Error)?
-            var fetchConfigReturnValue: [String: String]!
-            var fetchConfigClosure: (() async throws -> [String: String])?
+            public var fetchConfigThrowableError: (any Error)?
+            public var fetchConfigReturnValue: [String: String]!
+            public var fetchConfigClosure: (() async throws -> [String: String])?
+            public
             func fetchConfig() async throws -> [String: String] {
                 fetchConfigCallsCount += 1
                 if let fetchConfigThrowableError {
@@ -120,14 +126,15 @@ final class UT_SpyableMacro: XCTestCase {
                     return fetchConfigReturnValue
                 }
             }
-            var fetchDataCallsCount = 0
-            var fetchDataCalled: Bool {
+            public var fetchDataCallsCount = 0
+            public var fetchDataCalled: Bool {
                 return fetchDataCallsCount > 0
             }
-            var fetchDataReceivedName: (String, count: Int)?
-            var fetchDataReceivedInvocations: [(String, count: Int)] = []
-            var fetchDataReturnValue: (() -> Void)!
-            var fetchDataClosure: (((String, count: Int)) async -> (() -> Void))?
+            public var fetchDataReceivedName: (String, count: Int)?
+            public var fetchDataReceivedInvocations: [(String, count: Int)] = []
+            public var fetchDataReturnValue: (() -> Void)!
+            public var fetchDataClosure: (((String, count: Int)) async -> (() -> Void))?
+            public
             func fetchData(_ name: (String, count: Int)) async -> (() -> Void) {
                 fetchDataCallsCount += 1
                 fetchDataReceivedName = (name)
@@ -138,53 +145,58 @@ final class UT_SpyableMacro: XCTestCase {
                     return fetchDataReturnValue
                 }
             }
-            var fetchUsernameContextCompletionCallsCount = 0
-            var fetchUsernameContextCompletionCalled: Bool {
+            public var fetchUsernameContextCompletionCallsCount = 0
+            public var fetchUsernameContextCompletionCalled: Bool {
                 return fetchUsernameContextCompletionCallsCount > 0
             }
-            var fetchUsernameContextCompletionReceivedArguments: (context: String, completion: (String) -> Void)?
-            var fetchUsernameContextCompletionReceivedInvocations: [(context: String, completion: (String) -> Void)] = []
-            var fetchUsernameContextCompletionClosure: ((String, @escaping (String) -> Void) -> Void)?
+            public var fetchUsernameContextCompletionReceivedArguments: (context: String, completion: (String) -> Void)?
+            public var fetchUsernameContextCompletionReceivedInvocations: [(context: String, completion: (String) -> Void)] = []
+            public var fetchUsernameContextCompletionClosure: ((String, @escaping (String) -> Void) -> Void)?
+            public
             func fetchUsername(context: String, completion: @escaping (String) -> Void) {
                 fetchUsernameContextCompletionCallsCount += 1
                 fetchUsernameContextCompletionReceivedArguments = (context, completion)
                 fetchUsernameContextCompletionReceivedInvocations.append((context, completion))
                 fetchUsernameContextCompletionClosure?(context, completion)
             }
-            var onTapBackContextActionCallsCount = 0
-            var onTapBackContextActionCalled: Bool {
+            public var onTapBackContextActionCallsCount = 0
+            public var onTapBackContextActionCalled: Bool {
                 return onTapBackContextActionCallsCount > 0
             }
-            var onTapBackContextActionClosure: ((String, () -> Void) -> Void)?
+            public var onTapBackContextActionClosure: ((String, () -> Void) -> Void)?
+            public
             func onTapBack(context: String, action: () -> Void) {
                 onTapBackContextActionCallsCount += 1
                 onTapBackContextActionClosure?(context, action)
             }
-            var onTapNextContextActionCallsCount = 0
-            var onTapNextContextActionCalled: Bool {
+            public var onTapNextContextActionCallsCount = 0
+            public var onTapNextContextActionCalled: Bool {
                 return onTapNextContextActionCallsCount > 0
             }
-            var onTapNextContextActionClosure: ((String, @Sendable () -> Void) -> Void)?
+            public var onTapNextContextActionClosure: ((String, @Sendable () -> Void) -> Void)?
+            public
             func onTapNext(context: String, action: @Sendable () -> Void) {
                 onTapNextContextActionCallsCount += 1
                 onTapNextContextActionClosure?(context, action)
             }
-            var assertCallsCount = 0
-            var assertCalled: Bool {
+            public var assertCallsCount = 0
+            public var assertCalled: Bool {
                 return assertCallsCount > 0
             }
-            var assertClosure: ((@autoclosure () -> String) -> Void)?
+            public var assertClosure: ((@autoclosure () -> String) -> Void)?
+            public
             func assert(_ message: @autoclosure () -> String) {
                 assertCallsCount += 1
                 assertClosure?(message())
             }
-            var useGenericsValues1Values2Values3CallsCount = 0
-            var useGenericsValues1Values2Values3Called: Bool {
+            public var useGenericsValues1Values2Values3CallsCount = 0
+            public var useGenericsValues1Values2Values3Called: Bool {
                 return useGenericsValues1Values2Values3CallsCount > 0
             }
-            var useGenericsValues1Values2Values3ReceivedArguments: (values1: [Any], values2: Array<Any>, values3: (Any, Any, Int))?
-            var useGenericsValues1Values2Values3ReceivedInvocations: [(values1: [Any], values2: Array<Any>, values3: (Any, Any, Int))] = []
-            var useGenericsValues1Values2Values3Closure: (([Any], Array<Any>, (Any, Any, Int)) -> Void)?
+            public var useGenericsValues1Values2Values3ReceivedArguments: (values1: [Any], values2: Array<Any>, values3: (Any, Any, Int))?
+            public var useGenericsValues1Values2Values3ReceivedInvocations: [(values1: [Any], values2: Array<Any>, values3: (Any, Any, Int))] = []
+            public var useGenericsValues1Values2Values3Closure: (([Any], Array<Any>, (Any, Any, Int)) -> Void)?
+            public
             func useGenerics<T, U>(values1: [T], values2: Array<U>, values3: (T, U, Int)) {
                 useGenericsValues1Values2Values3CallsCount += 1
                 useGenericsValues1Values2Values3ReceivedArguments = (values1, values2, values3)
@@ -212,6 +224,8 @@ final class UT_SpyableMacro: XCTestCase {
         \(protocolDeclaration)
 
         class MyProtocolSpy: MyProtocol {
+            init() {
+            }
         }
         """,
       macros: sut
@@ -231,6 +245,8 @@ final class UT_SpyableMacro: XCTestCase {
         \(protocolDeclaration)
 
         class MyProtocolSpy: MyProtocol {
+            init() {
+            }
         }
         """,
       macros: sut
@@ -251,6 +267,8 @@ final class UT_SpyableMacro: XCTestCase {
 
         #if CUSTOM
         class MyProtocolSpy: MyProtocol {
+            init() {
+            }
         }
         #endif
         """,
@@ -276,6 +294,8 @@ final class UT_SpyableMacro: XCTestCase {
 
         #if CUSTOM
         class MyProtocolSpy: MyProtocol {
+            init() {
+            }
         }
         #endif
         """,
@@ -296,6 +316,8 @@ final class UT_SpyableMacro: XCTestCase {
         \(protocolDeclaration)
 
         class MyProtocolSpy: MyProtocol {
+            init() {
+            }
         }
         """,
       diagnostics: [
@@ -332,6 +354,8 @@ final class UT_SpyableMacro: XCTestCase {
         \(protocolDeclaration)
 
         class MyProtocolSpy: MyProtocol {
+            init() {
+            }
         }
         """,
       diagnostics: [
@@ -352,4 +376,58 @@ final class UT_SpyableMacro: XCTestCase {
       macros: sut
     )
   }
+
+  func testSpyClassAccessLevelsMatchProtocolAccessLevels() {
+    let accessLevelMappings = [
+      (protocolAccessLevel: "public", spyClassAccessLevel: "public"),
+      (protocolAccessLevel: "package", spyClassAccessLevel: "package"),
+      (protocolAccessLevel: "internal", spyClassAccessLevel: "internal"),
+      (protocolAccessLevel: "fileprivate", spyClassAccessLevel: "fileprivate"),
+      (protocolAccessLevel: "private", spyClassAccessLevel: "fileprivate"),
+    ]
+
+    for mapping in accessLevelMappings {
+      let protocolDefinition = """
+        \(mapping.protocolAccessLevel) protocol ServiceProtocol {
+            var removed: (() -> Void)? { get set }
+
+            func fetchUsername(context: String, completion: @escaping (String) -> Void)
+        }
+        """
+
+      assertMacroExpansion(
+        """
+        @Spyable
+        \(protocolDefinition)
+        """,
+        expandedSource: """
+
+          \(protocolDefinition)
+
+          \(mapping.spyClassAccessLevel) class ServiceProtocolSpy: ServiceProtocol {
+              \(mapping.spyClassAccessLevel) init() {
+              }
+              \(mapping.spyClassAccessLevel)
+              var removed: (() -> Void)?
+              \(mapping.spyClassAccessLevel) var fetchUsernameContextCompletionCallsCount = 0
+              \(mapping.spyClassAccessLevel) var fetchUsernameContextCompletionCalled: Bool {
+                  return fetchUsernameContextCompletionCallsCount > 0
+              }
+              \(mapping.spyClassAccessLevel) var fetchUsernameContextCompletionReceivedArguments: (context: String, completion: (String) -> Void)?
+              \(mapping.spyClassAccessLevel) var fetchUsernameContextCompletionReceivedInvocations: [(context: String, completion: (String) -> Void)] = []
+              \(mapping.spyClassAccessLevel) var fetchUsernameContextCompletionClosure: ((String, @escaping (String) -> Void) -> Void)?
+              \(mapping.spyClassAccessLevel)
+
+              func fetchUsername(context: String, completion: @escaping (String) -> Void) {
+                  fetchUsernameContextCompletionCallsCount += 1
+                  fetchUsernameContextCompletionReceivedArguments = (context, completion)
+                  fetchUsernameContextCompletionReceivedInvocations.append((context, completion))
+                  fetchUsernameContextCompletionClosure?(context, completion)
+              }
+          }
+          """,
+        macros: sut
+      )
+    }
+  }
 }