diff --git a/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift b/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift index e4cf2798..9845d002 100644 --- a/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift +++ b/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift @@ -786,10 +786,14 @@ final class EntityVisitor: SyntaxVisitor { return .visitChildren } - // Parse conditional import block recursively - let block = parseIfConfigDecl(node) - imports.append(.conditional(block)) - return .skipChildren + if containsOnlyImports(node) { + // Parse conditional import block recursively + let block = parseIfConfigDecl(node) + imports.append(.conditional(block)) + return .skipChildren + } else { + return .visitChildren + } } /// Recursively parses an IfConfigDeclSyntax into a ConditionalImportBlock @@ -826,6 +830,28 @@ final class EntityVisitor: SyntaxVisitor { return ConditionalImportBlock(clauses: clauseList, offset: node.offset) } + /// Returns `true` when every element inside the `#if` block is either + /// an `import` statement or a nested `#if` that itself contains only imports. + private func containsOnlyImports(_ node: IfConfigDeclSyntax) -> Bool { + for clause in node.clauses { + guard let list = clause.elements?.as(CodeBlockItemListSyntax.self) else { + continue + } + for element in list { + if element.item.is(ImportDeclSyntax.self) { + continue + } else if let nested = element.item.as(IfConfigDeclSyntax.self) { + if !containsOnlyImports(nested) { + return false + } + } else { + return false + } + } + } + return true + } + override func visit(_ node: InitializerDeclSyntax) -> SyntaxVisitorContinueKind { return .skipChildren } diff --git a/Tests/TestConditionalImportBlocks/ConditionalImportBlocksTests.swift b/Tests/TestConditionalImportBlocks/ConditionalImportBlocksTests.swift new file mode 100644 index 00000000..66d757d6 --- /dev/null +++ b/Tests/TestConditionalImportBlocks/ConditionalImportBlocksTests.swift @@ -0,0 +1,24 @@ +import XCTest +@testable import MockoloFramework + +final class ConditionalImportBlocksTests: MockoloTestCase { + func testProtocolInsideIfBlockWithNonImportDeclaration() { + verify(srcContent: FixtureConditionalImportBlocks.protocolInIfBlock, + dstContent: FixtureConditionalImportBlocks.protocolInIfBlockMock) + } + func testConditionalImportBlockPreserved() { + verify(srcContent: FixtureConditionalImportBlocks.conditionalImportBlock, + dstContent: FixtureConditionalImportBlocks.conditionalImportBlockMock) + } + func testNestedIfBlocksWithMultipleProtocols() { + verify(srcContent: FixtureConditionalImportBlocks.nestedIfBlocks, + dstContent: FixtureConditionalImportBlocks.nestedIfBlocksMock) + } + func testIfBlockWithImportsAndProtocol() { + verify(srcContent: FixtureConditionalImportBlocks.ifBlockWithImportsAndProtocol, + dstContent: FixtureConditionalImportBlocks.ifBlockWithImportsAndProtocolMock) + } + func testMixedNestedBlocks() { + verify(srcContent: FixtureConditionalImportBlocks.mixedNestedBlocks, + dstContent: FixtureConditionalImportBlocks.mixedNestedBlocksMock) + } diff --git a/Tests/TestConditionalImportBlocks/FixtureConditionalImportBlocks.swift b/Tests/TestConditionalImportBlocks/FixtureConditionalImportBlocks.swift new file mode 100644 index 00000000..26ec9282 --- /dev/null +++ b/Tests/TestConditionalImportBlocks/FixtureConditionalImportBlocks.swift @@ -0,0 +1,161 @@ +enum FixtureConditionalImportBlocks { + + /// Protocol inside a #if block that contains non-import declarations + static let protocolInIfBlock = + """ + #if os(iOS) + /// @mockable + public protocol PlatformProtocol { + func platformFunction() + } + #endif + """ + + /// Expected mock for protocol inside #if block + static let protocolInIfBlockMock = + """ + public class PlatformProtocolMock: PlatformProtocol { + public init() { } + + + public private(set) var platformFunctionCallCount = 0 + public var platformFunctionHandler: (() -> ())? + public func platformFunction() { + platformFunctionCallCount += 1 + if let platformFunctionHandler = platformFunctionHandler { + platformFunctionHandler() + } + } + } + """ + + /// Protocol inside a #if block containing only imports (should be treated as conditional import) + static let conditionalImportBlock = + """ + #if canImport(Foundation) + import Foundation + #endif + + /// @mockable + public protocol ServiceProtocol { + func execute() + } + """ + + /// Expected output with conditional import preserved and protocol mocked + static let conditionalImportBlockMock = + """ + #if canImport(Foundation) + import Foundation + #endif + + + public class ServiceProtocolMock: ServiceProtocol { + public init() { } + + + public private(set) var executeCallCount = 0 + public var executeHandler: (() -> ())? + public func execute() { + executeCallCount += 1 + if let executeHandler = executeHandler { + executeHandler() + } + } + } + """ + + /// Multiple protocols in nested #if blocks with mixed content + static let nestedIfBlocks = + """ + #if os(iOS) + /// @mockable + public protocol iOSProtocol { + func iosMethod() + } + #elseif os(macOS) + /// @mockable + public protocol macOSProtocol { + func macosMethod() + } + #endif + """ + + /// Expected mocks for both iOS and macOS protocols + static let nestedIfBlocksMock = + """ + #if os(iOS) + /// @mockable + public protocol iOSProtocol { + func iosMethod() + } + #elseif os(macOS) + /// @mockable + public protocol macOSProtocol { + func macosMethod() + } + #endif + """ + + /// #if block with imports and a protocol (should visit children and discover protocol) + static let ifBlockWithImportsAndProtocol = + """ + #if DEBUG + import XCTest + /// @mockable + public protocol DebugProtocol { + func debugFunction() + } + #endif + """ + + /// Protocol should be discovered and mocked + static let ifBlockWithImportsAndProtocolMock = + """ + public class DebugProtocolMock: DebugProtocol { + public init() { } + + + public private(set) var debugFunctionCallCount = 0 + public var debugFunctionHandler: (() -> ())? + public func debugFunction() { + debugFunctionCallCount += 1 + if let debugFunctionHandler = debugFunctionHandler { + debugFunctionHandler() + } + } + } + """ + + /// Nested #if blocks where inner only contains imports + static let mixedNestedBlocks = + """ + #if os(iOS) + #if DEBUG + import XCTest + #endif + /// @mockable + public protocol MixedProtocol { + func mixedMethod() + } + #endif + """ + + /// Protocol should be discovered in mixed nested scenario + static let mixedNestedBlocksMock = + """ + public class MixedProtocolMock: MixedProtocol { + public init() { } + + + public private(set) var mixedMethodCallCount = 0 + public var mixedMethodHandler: (() -> ())? + public func mixedMethod() { + mixedMethodCallCount += 1 + if let mixedMethodHandler = mixedMethodHandler { + mixedMethodHandler() + } + } + } + """ +}