Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
161 changes: 161 additions & 0 deletions Tests/TestConditionalImportBlocks/FixtureConditionalImportBlocks.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
"""
}