diff --git a/Sources/SafeDICore/Generators/ScopeGenerator.swift b/Sources/SafeDICore/Generators/ScopeGenerator.swift index e7aa1c4b..a081aaa4 100644 --- a/Sources/SafeDICore/Generators/ScopeGenerator.swift +++ b/Sources/SafeDICore/Generators/ScopeGenerator.swift @@ -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). @@ -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), ) } @@ -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( @@ -1101,7 +1106,7 @@ actor ScopeGenerator: CustomStringConvertible, Sendable { var result = [MockParameterNode]() func walk(_ node: MockParameterNode, ancestorTypes: Set = []) { - 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 @@ -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 @@ -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 { diff --git a/Tests/SafeDIToolTests/SafeDIToolMockGenerationTests.swift b/Tests/SafeDIToolTests/SafeDIToolMockGenerationTests.swift index ef8cf2cc..bcf6f364 100644 --- a/Tests/SafeDIToolTests/SafeDIToolMockGenerationTests.swift +++ b/Tests/SafeDIToolTests/SafeDIToolMockGenerationTests.swift @@ -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"] == """ @@ -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( @@ -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 ?? "")") } @@ -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( @@ -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 ?? "")") }