diff --git a/Sources/MockingMacros/Macros/MockedMacro/MockedMacro.swift b/Sources/MockingMacros/Macros/MockedMacro/MockedMacro.swift index 4f466d8..2ffb7e3 100644 --- a/Sources/MockingMacros/Macros/MockedMacro/MockedMacro.swift +++ b/Sources/MockingMacros/Macros/MockedMacro/MockedMacro.swift @@ -23,6 +23,17 @@ public struct MockedMacro: PeerMacro { } let macroArguments = MacroArguments(node: node) + + if let conflictingConditionalClauses = self.conflictingConditionalClauses( + from: protocolDeclaration.memberBlock.members + ) { + return try self.expansionWithConditionalConformance( + protocolDeclaration: protocolDeclaration, + macroArguments: macroArguments, + clauses: conflictingConditionalClauses + ) + } + let mockDeclaration = DeclSyntax( ClassDeclSyntax( attributes: AttributeListSyntax { @@ -121,7 +132,8 @@ extension MockedMacro { /// generated from the associated types defined by the provided protocol. /// /// The clause supports associated types with comma-separated constraints, - /// composition (`&`), or a combination of both. + /// composition (`&`), or a combination of both. Associated types inside + /// `#if` blocks are also extracted. /// /// ```swift /// @Mocked @@ -132,27 +144,44 @@ extension MockedMacro { /// final class DependencyMock: Dependency {} /// ``` /// - /// - Parameter protocolDeclaration: The protocol to which the mock must - /// conform. - /// - Returns: The generic parameter clause to apply to the mock - /// declaration. + /// - Parameters: + /// - protocolDeclaration: The protocol to which the mock must conform. + /// - excludingConstraintsFor: Names of associated types whose constraints + /// should be excluded (used for conditional conformance). + /// - Returns: The generic parameter clause to apply to the mock declaration. private static func mockGenericParameterClause( - from protocolDeclaration: ProtocolDeclSyntax + from protocolDeclaration: ProtocolDeclSyntax, + excludingConstraintsFor excludedNames: Set = [] ) -> GenericParameterClauseSyntax? { let memberBlock = protocolDeclaration.memberBlock - let associatedTypeDeclarations = memberBlock.memberDeclarations( - ofType: AssociatedTypeDeclSyntax.self + let associatedTypeDeclarations = self.associatedTypeDeclarations( + from: memberBlock.members ) guard !associatedTypeDeclarations.isEmpty else { return nil } + // Deduplicate by name (same associated type may appear in multiple #if branches) + var seenNames: Set = [] + let uniqueAssociatedTypes = associatedTypeDeclarations.filter { decl in + let name = decl.name.text + if seenNames.contains(name) { + return false + } + seenNames.insert(name) + return true + } + return GenericParameterClauseSyntax { - for associatedTypeDeclaration in associatedTypeDeclarations { + for associatedTypeDeclaration in uniqueAssociatedTypes { let genericParameterName = associatedTypeDeclaration.name.trimmed + let shouldExcludeConstraints = excludedNames + .contains(associatedTypeDeclaration.name.text) - if let inheritanceClause = associatedTypeDeclaration.inheritanceClause { + if !shouldExcludeConstraints, + let inheritanceClause = associatedTypeDeclaration.inheritanceClause + { let commaSeparatedInheritedTypes = inheritanceClause .inheritedTypes(ofType: IdentifierTypeSyntax.self) .compactMap { CompositionTypeElementSyntax(type: $0) } @@ -183,6 +212,45 @@ extension MockedMacro { } } + /// Returns the associated type declarations from the provided `members`, + /// including those inside `#if` declarations. + /// + /// - Parameter members: The members to search. + /// - Returns: The associated type declarations from the provided `members`. + private static func associatedTypeDeclarations( + from members: MemberBlockItemListSyntax + ) -> [AssociatedTypeDeclSyntax] { + members.flatMap { member -> [AssociatedTypeDeclSyntax] in + if let associatedTypeDeclaration = member.decl.as(AssociatedTypeDeclSyntax.self) { + return [associatedTypeDeclaration] + } else if let ifConfigDeclaration = member.decl.as(IfConfigDeclSyntax.self) { + return self.associatedTypeDeclarations(from: ifConfigDeclaration) + } else { + return [] + } + } + } + + /// Returns the associated type declarations from the provided + /// `ifConfigDeclaration`. + /// + /// This method recursively searches nested `#if` declarations. + /// + /// - Parameter ifConfigDeclaration: The `#if` declaration to search. + /// - Returns: The associated type declarations from the provided + /// `ifConfigDeclaration`. + private static func associatedTypeDeclarations( + from ifConfigDeclaration: IfConfigDeclSyntax + ) -> [AssociatedTypeDeclSyntax] { + ifConfigDeclaration.clauses.flatMap { clause -> [AssociatedTypeDeclSyntax] in + guard case let .decls(members) = clause.elements else { + return [] + } + + return self.associatedTypeDeclarations(from: members) + } + } + // MARK: Inheritance Clause /// Returns the inheritance clause to apply to the mock declaration, which @@ -228,69 +296,91 @@ extension MockedMacro { private static func mockGenericWhereClause( from protocolDeclaration: ProtocolDeclSyntax ) -> GenericWhereClauseSyntax? { - let genericWhereClauses = protocolDeclaration.genericWhereClauses - - guard !genericWhereClauses.isEmpty else { + guard !protocolDeclaration.genericWhereClauses.isEmpty else { return nil } return GenericWhereClauseSyntax { - for genericWhereClause in genericWhereClauses { - for requirement in genericWhereClause.requirements { - requirement.trimmed - } - } + protocolDeclaration.genericWhereClauses + .flatMap(\.requirements) + .map(\.trimmed) } } // MARK: Members /// Returns the member block to apply to the mock, generated from the - /// properties and methods of the provided protocol. + /// members from the provided `protocolDeclaration`. /// - /// - Parameter protocolDeclaration: The protocol to which the mock must - /// conform. + /// - Parameter protocolDeclaration: The protocol being mocked. /// - Returns: The member block to apply to the mock. private static func mockMemberBlock( from protocolDeclaration: ProtocolDeclSyntax ) throws -> MemberBlockSyntax { let accessLevel = protocolDeclaration.minimumConformingAccessLevel - let memberBlock = protocolDeclaration.memberBlock - let initializerDeclarations = memberBlock.memberDeclarations( - ofType: InitializerDeclSyntax.self - ) - let propertyDeclarations = memberBlock.memberDeclarations( - ofType: VariableDeclSyntax.self - ) - let methodDeclarations = memberBlock.memberDeclarations( - ofType: FunctionDeclSyntax.self + let members = try self.mockMembers( + from: protocolDeclaration.memberBlock.members, + with: accessLevel, + in: protocolDeclaration ) - return try MemberBlockSyntax { - for initializerDeclaration in initializerDeclarations { - try self.mockInitializerConformanceDeclaration( - with: accessLevel, - from: initializerDeclaration - ) - } + return MemberBlockSyntax(members: members) + } - for propertyDeclaration in propertyDeclarations { - for binding in propertyDeclaration.bindings { - try self.mockPropertyConformanceDeclaration( - with: accessLevel, - for: binding, - from: propertyDeclaration + /// Returns the members to apply to the mock, generated from the provided + /// `members` from the provided `protocolDeclaration` and marked with the + /// provided `accessLevel`. + /// + /// Associated types are skipped since they become generic parameters for + /// the mock class rather than member declarations. + /// + /// - Parameters: + /// - members: The members from the protocol being mocked. + /// - accessLevel: The access level to apply to the mock's members. + /// - protocolDeclaration: The protocol being mocked. + /// - Returns: The members to apply to the mock. + private static func mockMembers( + from members: MemberBlockItemListSyntax, + with accessLevel: AccessLevelSyntax, + in protocolDeclaration: ProtocolDeclSyntax + ) throws -> MemberBlockItemListSyntax { + try MemberBlockItemListSyntax { + for member in members { + if let initializerDeclaration = member.decl.as(InitializerDeclSyntax.self) { + MemberBlockItemSyntax( + decl: try self.mockInitializerConformanceDeclaration( + with: accessLevel, + from: initializerDeclaration + ) + ) + } else if let propertyDeclaration = member.decl.as(VariableDeclSyntax.self) { + for binding in propertyDeclaration.bindings { + MemberBlockItemSyntax( + decl: try self.mockPropertyConformanceDeclaration( + with: accessLevel, + for: binding, + from: propertyDeclaration + ) + ) + } + } else if let methodDeclaration = member.decl.as(FunctionDeclSyntax.self) { + MemberBlockItemSyntax( + decl: try self.mockMethodConformanceDeclaration( + with: accessLevel, + for: methodDeclaration, + in: protocolDeclaration + ) ) + } else if let ifConfigDeclaration = member.decl.as(IfConfigDeclSyntax.self) { + if let mockIfConfigDeclaration = try self.mockIfConfigDeclaration( + from: ifConfigDeclaration, + with: accessLevel, + in: protocolDeclaration + ) { + MemberBlockItemSyntax(decl: mockIfConfigDeclaration) + } } } - - for methodDeclaration in methodDeclarations { - try self.mockMethodConformanceDeclaration( - with: accessLevel, - for: methodDeclaration, - in: protocolDeclaration - ) - } } } @@ -468,6 +558,65 @@ extension MockedMacro { } } + // MARK: If Configs + + /// Returns an `IfConfigDeclSyntax` containing mock member declarations, + /// generated from the provided `ifConfigDeclaration` from the provided + /// `protocolDeclaration` and marked with the provided `accessLevel`. + /// + /// This method preserves the conditional compilation structure from the + /// provided `ifConfigDeclaration` in the returned `IfConfigDeclSyntax`. + /// + /// - Parameters: + /// - ifConfigDeclaration: The `IfConfigDeclSyntax` from the protocol. + /// - accessLevel: The access level to apply to the mock declarations. + /// - protocolDeclaration: The protocol being mocked. + /// - Returns: An `IfConfigDeclSyntax` containing mock member declarations, + /// or `nil` if none of the clauses contain member declarations (e.g., are + /// empty or contain only associated type declarations). + private static func mockIfConfigDeclaration( + from ifConfigDeclaration: IfConfigDeclSyntax, + with accessLevel: AccessLevelSyntax, + in protocolDeclaration: ProtocolDeclSyntax + ) throws -> IfConfigDeclSyntax? { + let clauses = try IfConfigClauseListSyntax( + ifConfigDeclaration.clauses.map { clause in + guard case let .decls(members) = clause.elements else { + return clause + } + + let mockMembers = try self.mockMembers( + from: members, + with: accessLevel, + in: protocolDeclaration + ) + + return IfConfigClauseSyntax( + poundKeyword: clause.poundKeyword.trimmed, + condition: clause.condition?.trimmed, + elements: .decls(mockMembers) + ) + } + ) + + let allClausesEmpty = clauses.allSatisfy { clause in + guard case let .decls(members) = clause.elements else { + return true + } + + return members.isEmpty + } + + guard !allClausesEmpty else { + return nil + } + + return IfConfigDeclSyntax( + clauses: clauses, + poundEndif: ifConfigDeclaration.poundEndif.trimmed + ) + } + // MARK: Modifiers /// Returns modifiers to apply to override declarations, generated using the @@ -530,3 +679,239 @@ extension MockedMacro { } } } + +// MARK: - Conditional Associated Type Conformance + +extension MockedMacro { + + /// A tuple representing one clause of an `#if` block containing associated + /// types with their conditional compilation condition. + /// + /// - `condition`: The conditional compilation expression (e.g., `DEBUG`), + /// or `nil` for `#else` clauses. + /// - `associatedTypes`: The associated type declarations within this clause. + private typealias ConditionalClause = ( + condition: ExprSyntax?, + associatedTypes: [AssociatedTypeDeclSyntax] + ) + + /// Returns conditional clauses containing associated types with conflicting + /// constraints across different `#if` branches. + /// + /// This method detects when the same associated type has different constraints + /// in different conditional compilation branches, requiring conditional conformance. + /// + /// - Parameter members: The protocol members to search for conflicting + /// conditional associated types. + /// - Returns: An array of conditional clauses if conflicts are found, or + /// `nil` if there are no conflicts. + private static func conflictingConditionalClauses( + from members: MemberBlockItemListSyntax + ) -> [ConditionalClause]? { + members + .compactMap { member in member.decl.as(IfConfigDeclSyntax.self) } + .compactMap { ifConfigDeclaration -> [ConditionalClause]? in + let clauses = ifConfigDeclaration.clauses + .compactMap { clause -> ConditionalClause? in + guard case let .decls(declarations) = clause.elements else { + return nil + } + + let associatedTypes = declarations.compactMap { declaration in + declaration.decl.as(AssociatedTypeDeclSyntax.self) + } + + guard !associatedTypes.isEmpty else { + return nil + } + + return (clause.condition, associatedTypes) + } + + guard !clauses.isEmpty else { + return nil + } + + let constraintSetsByTypeName = Dictionary( + grouping: clauses.flatMap(\.associatedTypes), + by: \.name.text + ).mapValues { types in + Set(types.map { type in + type.inheritanceClause?.description + .trimmingCharacters(in: .whitespaces) ?? "" + }) + } + + guard constraintSetsByTypeName.values.contains(where: { $0.count > 1 }) else { + return nil + } + + return clauses + } + .first + } + + /// Returns the macro expansion with conditional conformance extensions + /// for protocols with conflicting associated type constraints in `#if` blocks. + /// + /// This method generates a mock class without associated type constraints, + /// along with conditional extensions that conform to the protocol under + /// specific compilation conditions with the appropriate constraints applied. + /// + /// - Parameters: + /// - protocolDeclaration: The protocol to which the mock must conform. + /// - macroArguments: The macro arguments provided to the `@Mocked` attribute. + /// - clauses: The conditional clauses containing conflicting associated + /// type constraints. + /// - Returns: The declarations to generate, including the mock class and + /// conditional conformance extensions. + private static func expansionWithConditionalConformance( + protocolDeclaration: ProtocolDeclSyntax, + macroArguments: MacroArguments, + clauses: [ConditionalClause] + ) throws -> [DeclSyntax] { + let mockName = self.mockName(from: protocolDeclaration) + let associatedTypeNamesToExclude = Set(clauses.flatMap { clause in + clause.associatedTypes.map(\.name.text) + }) + + let mockDeclaration = ClassDeclSyntax( + attributes: AttributeListSyntax { + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: "MockedMembers"), + trailingTrivia: .newline + ) + }, + modifiers: self.mockModifiers(from: protocolDeclaration), + classKeyword: .keyword(protocolDeclaration.isActorConstrained ? .actor : .class), + name: mockName, + genericParameterClause: self.mockGenericParameterClause( + from: protocolDeclaration, + excludingConstraintsFor: associatedTypeNamesToExclude + ), + inheritanceClause: macroArguments.sendableConformance == .unchecked + ? InheritanceClauseSyntax { InheritedTypeSyntax.uncheckedSendable } + : nil, + genericWhereClause: self.mockGenericWhereClause(from: protocolDeclaration), + memberBlock: try self.mockMemberBlock(from: protocolDeclaration) + ) + + let ifConfigDeclaration = IfConfigDeclSyntax( + clauses: IfConfigClauseListSyntax( + clauses.enumerated().map { index, clause in + IfConfigClauseSyntax( + poundKeyword: index == 0 + ? .poundIfToken() + : clause.condition != nil ? .poundElseifToken() : .poundElseToken(), + condition: clause.condition, + elements: .decls(MemberBlockItemListSyntax { + MemberBlockItemSyntax(decl: ExtensionDeclSyntax( + extendedType: IdentifierTypeSyntax(name: mockName), + inheritanceClause: InheritanceClauseSyntax { + InheritedTypeSyntax(type: protocolDeclaration.type) + }, + genericWhereClause: self.whereClause(from: clause.associatedTypes), + memberBlock: MemberBlockSyntax(members: []) + )) + }) + ) + } + ) + ) + + let declarations: [DeclSyntax] = [ + DeclSyntax(mockDeclaration), + DeclSyntax(ifConfigDeclaration), + ] + + guard let condition = macroArguments.compilationCondition.rawValue else { + return declarations + } + + return [DeclSyntax(IfConfigDeclSyntax(clauses: IfConfigClauseListSyntax { + IfConfigClauseSyntax( + poundKeyword: .poundIfToken(), + condition: DeclReferenceExprSyntax(baseName: .identifier(condition)), + elements: .statements(CodeBlockItemListSyntax( + declarations.map { declaration in + CodeBlockItemSyntax(item: .decl(declaration)) + } + )) + ) + }))] + } + + /// Builds a generic `where` clause from associated type constraints. + /// + /// This method extracts all constraints from the provided associated types + /// and combines them into a single `where` clause. It handles both simple + /// constraints (e.g., `Item: Equatable`) and composition constraints + /// (e.g., `Item: Equatable & Sendable`). + /// + /// For example, given these associated types: + /// + /// ```swift + /// associatedtype Item: Equatable + /// associatedtype Value: Codable & Sendable + /// ``` + /// + /// This method generates: + /// + /// ```swift + /// where Item: Equatable, Value: Codable, Value: Sendable + /// ``` + /// + /// This is used when creating conditional conformance extensions for mocks + /// where different `#if` branches define conflicting constraints for the + /// same associated type. + /// + /// - Parameter associatedTypes: The associated type declarations from which + /// to extract constraints. + /// - Returns: A generic `where` clause containing all the constraints, or + /// `nil` if no constraints are found. + private static func whereClause( + from associatedTypes: [AssociatedTypeDeclSyntax] + ) -> GenericWhereClauseSyntax? { + let allConstraints = associatedTypes.flatMap { associatedType -> [( + name: TokenSyntax, + type: TypeSyntax + )] in + guard let inheritanceClause = associatedType.inheritanceClause else { + return [] + } + + let identifierTypes = inheritanceClause + .inheritedTypes(ofType: IdentifierTypeSyntax.self) + .map(TypeSyntax.init) + + let compositionTypes = inheritanceClause + .inheritedTypes(ofType: CompositionTypeSyntax.self) + .flatMap(\.elements) + .map { compositionType in + TypeSyntax(compositionType.type) + } + + return (identifierTypes + compositionTypes).map { type in ( + associatedType.name.trimmed, + type + ) } + } + + guard !allConstraints.isEmpty else { + return nil + } + + return GenericWhereClauseSyntax { + allConstraints.map { constraint in + GenericRequirementSyntax(requirement: .conformanceRequirement( + ConformanceRequirementSyntax( + leftType: IdentifierTypeSyntax(name: constraint.name), + colon: .colonToken(), + rightType: constraint.type.trimmed + ) + )) + } + } + } +} diff --git a/Tests/MockingMacrosTests/Macros/MockedMacro/Mocked_IfConfigDeclarationTests.swift b/Tests/MockingMacrosTests/Macros/MockedMacro/Mocked_IfConfigDeclarationTests.swift new file mode 100644 index 0000000..0533799 --- /dev/null +++ b/Tests/MockingMacrosTests/Macros/MockedMacro/Mocked_IfConfigDeclarationTests.swift @@ -0,0 +1,468 @@ +// +// Mocked_IfConfigDeclarationTests.swift +// +// Copyright © 2026 Fetch. +// + +#if canImport(MockingMacros) +import Testing +@testable import MockingMacros + +struct Mocked_IfConfigDeclarationTests { + + // MARK: Initializer in #if Block Tests + + @Test(arguments: mockedTestConfigurations) + func initializerInIfBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + init() + #if DEBUG + init(debugParameter: String) + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + \(mock.memberModifiers)init() { + } + #if DEBUG + \(mock.memberModifiers)init(debugParameter: String) { + } + #endif + } + #endif + """ + ) + } + + // MARK: Initializer in #if/#else Block Tests + + @Test(arguments: mockedTestConfigurations) + func initializerInIfElseBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + #if os(iOS) + init(iOSParameter: String) + #else + init(otherPlatformParameter: String) + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + #if os(iOS) + \(mock.memberModifiers)init(iOSParameter: String) { + } + #else + \(mock.memberModifiers)init(otherPlatformParameter: String) { + } + #endif + } + #endif + """ + ) + } + + // MARK: Property in #if Block Tests + + @Test(arguments: mockedTestConfigurations) + func propertyInIfBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + var commonProperty: String { get } + #if DEBUG + var debugProperty: Int { get set } + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + @MockableProperty(.readOnly) + \(mock.memberModifiers)var commonProperty: String + #if DEBUG + @MockableProperty(.readWrite) + \(mock.memberModifiers)var debugProperty: Int + #endif + } + #endif + """ + ) + } + + // MARK: Property in #if/#else Block Tests + + @Test(arguments: mockedTestConfigurations) + func propertyInIfElseBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + #if os(iOS) + var iOSProperty: String { get } + #else + var otherPlatformProperty: Int { get set } + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + #if os(iOS) + @MockableProperty(.readOnly) + \(mock.memberModifiers)var iOSProperty: String + #else + @MockableProperty(.readWrite) + \(mock.memberModifiers)var otherPlatformProperty: Int + #endif + } + #endif + """ + ) + } + + // MARK: Method in #if Block Tests + + @Test(arguments: mockedTestConfigurations) + func methodInIfBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + func commonMethod() + #if DEBUG + func debugOnlyMethod() + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + \(mock.memberModifiers)func commonMethod() + #if DEBUG + \(mock.memberModifiers)func debugOnlyMethod() + #endif + } + #endif + """ + ) + } + + // MARK: Method in #if/#else Block Tests + + @Test(arguments: mockedTestConfigurations) + func methodInIfElseBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + #if os(iOS) + func iOSMethod() + #else + func otherPlatformMethod() + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + #if os(iOS) + \(mock.memberModifiers)func iOSMethod() + #else + \(mock.memberModifiers)func otherPlatformMethod() + #endif + } + #endif + """ + ) + } + + // MARK: Multiple Members in #if Block Tests + + @Test(arguments: mockedTestConfigurations) + func multipleMembersInIfBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + #if DEBUG + var debugProperty: Int { get } + func debugMethod() + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + #if DEBUG + @MockableProperty(.readOnly) + \(mock.memberModifiers)var debugProperty: Int + \(mock.memberModifiers)func debugMethod() + #endif + } + #endif + """ + ) + } + + // MARK: #if/#elseif/#else Chain Tests + + @Test(arguments: mockedTestConfigurations) + func ifElseifElseChain( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + #if os(iOS) + func iOSMethod() + #elseif os(macOS) + func macOSMethod() + #else + func otherMethod() + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + #if os(iOS) + \(mock.memberModifiers)func iOSMethod() + #elseif os(macOS) + \(mock.memberModifiers)func macOSMethod() + #else + \(mock.memberModifiers)func otherMethod() + #endif + } + #endif + """ + ) + } + + // MARK: Associated Type with Same Declaration in #if/#else Block Tests + + @Test(arguments: mockedTestConfigurations) + func associatedTypeWithSameDeclarationInIfElseBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + #if os(iOS) + associatedtype PlatformView + #else + associatedtype PlatformView + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + } + #endif + """ + ) + } + + // MARK: Associated Type with Different Constraints in #if/#else Tests + + @Test(arguments: mockedTestConfigurations) + func associatedTypeWithDifferentConstraintsInIfElseBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol SomeCollection { + #if DEBUG + associatedtype Item: Equatable + #else + associatedtype Item: Hashable + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class SomeCollectionMock { + } + #if DEBUG + extension SomeCollectionMock: SomeCollection where Item: Equatable { + } + #else + extension SomeCollectionMock: SomeCollection where Item: Hashable { + } + #endif + #endif + """ + ) + } + + @Test(arguments: mockedTestConfigurations) + func associatedTypeWithDifferentConstraintsInIfElseifElseChain( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol PlatformCollection { + #if os(iOS) + associatedtype Element: Equatable + #elseif os(macOS) + associatedtype Element: Hashable + #else + associatedtype Element: Codable + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class PlatformCollectionMock { + } + #if os(iOS) + extension PlatformCollectionMock: PlatformCollection where Element: Equatable { + } + #elseif os(macOS) + extension PlatformCollectionMock: PlatformCollection where Element: Hashable { + } + #else + extension PlatformCollectionMock: PlatformCollection where Element: Codable { + } + #endif + #endif + """ + ) + } + + @Test(arguments: mockedTestConfigurations) + func associatedTypeWithMultipleConstraintsInIfElseBlock( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol MultiConstraintCollection { + #if DEBUG + associatedtype Item: Equatable & Sendable + #else + associatedtype Item: Hashable, Codable + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class MultiConstraintCollectionMock { + } + #if DEBUG + extension MultiConstraintCollectionMock: MultiConstraintCollection where Item: Equatable, Item: Sendable { + } + #else + extension MultiConstraintCollectionMock: MultiConstraintCollection where Item: Hashable, Item: Codable { + } + #endif + #endif + """ + ) + } + + // MARK: OR Condition Tests + + @Test(arguments: mockedTestConfigurations) + func orCondition( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + func commonMethod() + #if DEBUG || TESTFLIGHT + func debugOrTestFlightMethod() + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + \(mock.memberModifiers)func commonMethod() + #if DEBUG || TESTFLIGHT + \(mock.memberModifiers)func debugOrTestFlightMethod() + #endif + } + #endif + """ + ) + } + + // MARK: Nested #if Conditional Tests + + @Test(arguments: mockedTestConfigurations) + func nestedIfConditionals( + interface: InterfaceConfiguration, + mock: MockConfiguration + ) { + assertMocked( + """ + \(interface.accessLevel) protocol Dependency { + #if DEBUG + func debugMethod() + #if os(iOS) + func debugiOSOnlyMethod() + #endif + #endif + } + """, + generates: """ + #if SWIFT_MOCKING_ENABLED + @MockedMembers + \(mock.modifiers)class DependencyMock: Dependency { + #if DEBUG + \(mock.memberModifiers)func debugMethod() + #if os(iOS) + \(mock.memberModifiers)func debugiOSOnlyMethod() + #endif + #endif + } + #endif + """ + ) + } +} +#endif