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(_ 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(_ 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(_ 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(_ 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(_ 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 [ 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: Foo { + init() { + } } """ ) @@ -539,6 +581,8 @@ final class UT_SpyFactory: XCTestCase { """, expectingClassDeclaration: """ class FooSpy: 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, values3: (Any, Any, Int))? - var useGenericsValues1Values2Values3ReceivedInvocations: [(values1: [Any], values2: Array, values3: (Any, Any, Int))] = [] - var useGenericsValues1Values2Values3Closure: (([Any], Array, (Any, Any, Int)) -> Void)? + public var useGenericsValues1Values2Values3ReceivedArguments: (values1: [Any], values2: Array, values3: (Any, Any, Int))? + public var useGenericsValues1Values2Values3ReceivedInvocations: [(values1: [Any], values2: Array, values3: (Any, Any, Int))] = [] + public var useGenericsValues1Values2Values3Closure: (([Any], Array, (Any, Any, Int)) -> Void)? + public func useGenerics(values1: [T], values2: Array, 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 + ) + } + } }