Skip to content
Merged
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
19 changes: 12 additions & 7 deletions Sources/SafeDICore/Generators/ScopeGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -865,8 +865,10 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
static let configurationStructName = "SafeDIMockConfiguration"

/// The qualified configuration type name for references (e.g., `ChildA.SafeDIMockConfiguration`).
/// Uses the concrete fulfilling type so the struct can be nested in a concrete type
/// extension. Protocol extensions cannot contain nested type declarations.
var configurationTypeName: String {
"\(instantiatedTypeDescription.asSource).\(Self.configurationStructName)"
"\(concreteType.asSource).\(Self.configurationStructName)"
}

/// The builder closure type as a Swift source string (unlabeled parameters).
Expand Down Expand Up @@ -918,7 +920,7 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
let indent = Self.standardIndent
return uniqueTypes.map { node in
(
typeName: node.instantiatedTypeDescription.asSource,
typeName: node.concreteType.asSource,
structCode: Self.generateConfigurationStruct(for: node, indent: indent),
)
}
Expand Down Expand Up @@ -1090,8 +1092,11 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
}

/// Collects all unique types from the `MockParameterNode` tree, deduplicated
/// by `instantiatedTypeDescription`. Returns nodes in depth-first order
/// (children before parents) so that referenced types appear before their referrers.
/// by `concreteType`. Returns nodes in depth-first order (children before
/// parents) so that referenced types appear before their referrers.
/// Uses `concreteType` (not `instantiatedTypeDescription`) because config
/// structs are nested in concrete type extensions — protocol extensions
/// cannot contain nested type declarations.
/// When the same type appears in both sendable and non-sendable contexts,
/// the sendable version is preferred (`@Sendable` closures work in both contexts).
private static func collectUniqueConfigurationTypes(
Expand All @@ -1101,7 +1106,7 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
var result = [MockParameterNode]()

func walk(_ node: MockParameterNode, ancestorTypes: Set<String> = []) {
let key = node.instantiatedTypeDescription.asSource
let key = node.concreteType.asSource
// Skip nodes whose type matches an ancestor — self-referencing cycle.
guard !ancestorTypes.contains(key) else { return }
var childAncestors = ancestorTypes
Expand All @@ -1117,7 +1122,7 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
// replace it — @Sendable closures are compatible in both contexts.
if node.requiresSendable,
let existingIndex = result.firstIndex(where: {
$0.instantiatedTypeDescription.asSource == key && !$0.requiresSendable
$0.concreteType.asSource == key && !$0.requiresSendable
})
{
result[existingIndex] = node
Expand Down Expand Up @@ -1207,7 +1212,7 @@ actor ScopeGenerator: CustomStringConvertible, Sendable {
// Exclude children whose type matches this node — they'd create a recursive
// value type. These are self-referencing Instantiators (lazy cycles).
let nonCycleChildren = node.children.filter {
$0.instantiatedTypeDescription != node.instantiatedTypeDescription
$0.concreteType != node.concreteType
}
let childLabelMap = disambiguatePropertyLabels(for: nonCycleChildren)
for child in nonCycleChildren {
Expand Down
57 changes: 21 additions & 36 deletions Tests/SafeDIToolTests/SafeDIToolMockGenerationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4106,6 +4106,23 @@ struct SafeDIToolMockGenerationTests: ~Copyable {
}
}
#endif

#if DEBUG
extension DefaultAuthService {
struct SafeDIMockConfiguration {
init(
networkService: (() -> DefaultNetworkService)? = nil,
_ safeDIBuilder: ((NetworkService, NetworkService) -> DefaultAuthService)? = nil
) {
self.networkService = networkService
self.safeDIBuilder = safeDIBuilder
}

let networkService: (() -> DefaultNetworkService)?
let safeDIBuilder: ((NetworkService, NetworkService) -> DefaultAuthService)?
}
}
#endif
""", "Unexpected output \(output.mockFiles["DefaultAuthService+SafeDIMock.swift"] ?? "")")

#expect(output.mockFiles["DefaultNetworkService+SafeDIMock.swift"] == """
Expand All @@ -4132,14 +4149,14 @@ struct SafeDIToolMockGenerationTests: ~Copyable {
struct SafeDIParameters {
init(
networkService: (() -> DefaultNetworkService)? = nil,
authService: AuthService.SafeDIMockConfiguration = .init()
authService: DefaultAuthService.SafeDIMockConfiguration = .init()
) {
self.networkService = networkService
self.authService = authService
}

let networkService: (() -> DefaultNetworkService)?
let authService: AuthService.SafeDIMockConfiguration
let authService: DefaultAuthService.SafeDIMockConfiguration
}

static func mock(
Expand All @@ -4162,22 +4179,6 @@ struct SafeDIToolMockGenerationTests: ~Copyable {
// Any modifications made to this file will be overwritten on subsequent builds.
// Please refrain from editing this file directly.

#if DEBUG
extension AuthService {
struct SafeDIMockConfiguration {
init(
networkService: (() -> DefaultNetworkService)? = nil,
_ safeDIBuilder: ((NetworkService, NetworkService) -> DefaultAuthService)? = nil
) {
self.networkService = networkService
self.safeDIBuilder = safeDIBuilder
}

let networkService: (() -> DefaultNetworkService)?
let safeDIBuilder: ((NetworkService, NetworkService) -> DefaultAuthService)?
}
}
#endif

""", "Unexpected output \(output.mockConfigurationFile ?? "")")
}
Expand Down Expand Up @@ -6221,12 +6222,12 @@ struct SafeDIToolMockGenerationTests: ~Copyable {
extension Root {
struct SafeDIParameters {
init(
service: AnyService.SafeDIMockConfiguration = .init()
service: ConcreteService.SafeDIMockConfiguration = .init()
) {
self.service = service
}

let service: AnyService.SafeDIMockConfiguration
let service: ConcreteService.SafeDIMockConfiguration
}

static func mock(
Expand All @@ -6248,22 +6249,6 @@ struct SafeDIToolMockGenerationTests: ~Copyable {
// Any modifications made to this file will be overwritten on subsequent builds.
// Please refrain from editing this file directly.

#if DEBUG
extension AnyService {
struct SafeDIMockConfiguration {
init(
helper: (() -> Helper)? = nil,
_ safeDIBuilder: ((Helper) -> ConcreteService)? = nil
) {
self.helper = helper
self.safeDIBuilder = safeDIBuilder
}

let helper: (() -> Helper)?
let safeDIBuilder: ((Helper) -> ConcreteService)?
}
}
#endif

""", "Unexpected output \(output.mockConfigurationFile ?? "")")
}
Expand Down
Loading