From d2d06830c8b9be33c3ad4cfc0f8dc1a09f8caaf7 Mon Sep 17 00:00:00 2001 From: Peter Krassay Date: Sun, 7 Jun 2026 10:03:03 -0600 Subject: [PATCH 1/2] Fix IUO type rendering so `T!` no longer generates `T?!` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mockolo parses an implicitly-unwrapped optional (`Int!`) as `Optional` with an `isIUO` flag, and `SwiftType.description` rendered its single optionality twice — the optional-sugar case appended `?` and the `isIUO` block appended `!` — producing `Int?!` (≈ `Int??`), which does not satisfy the `Int!` (≡ `Int?`) protocol requirement, so the generated mock failed to conform. This was pre-existing; no fixture exercised an IUO declaration (compiled `@Fixture` mocks surfaced it). - Properties: suppress the duplicate `?` for IUO in the optional-sugar case; the `isIUO` block still appends `!`, so the type renders `Int!`. Inert for everything else — plain `Int?` is unchanged, and the intentional `SomeProtocol!` force-unwrap backings go through the default nominal branch. - Functions / subscripts / generic returns: `!` is illegal inside a closure type, so the synthesized handler closure (`((Int!) -> Int!)?`) was non-compiling. Clear `isIUO` on the handler's argument and return types in `toClosureType` so they render as plain optionals (`((Int?) -> Int?)?`, legal), while the declared signature keeps `Int!` and still conforms. Clearing `isIUO` is a no-op for non-IUO types, so no existing handler changes. Adds compiled fixtures proving conformance: `iuoVars` (get-only + get-set) in TestVars and `iuoFuncs` (func param/return + subscript) in TestFuncs. No existing fixture output changes. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../MockoloFramework/Utils/SwiftType.swift | 14 ++++++-- .../FixtureNonSimpleFuncs.swift | 36 +++++++++++++++++++ .../TestBasicFuncs/FuncBasicTests.swift | 5 +++ Tests/TestVars/FixtureNonSimpleVars.swift | 22 ++++++++++++ Tests/TestVars/VarTests.swift | 5 +++ 5 files changed, 80 insertions(+), 2 deletions(-) diff --git a/Sources/MockoloFramework/Utils/SwiftType.swift b/Sources/MockoloFramework/Utils/SwiftType.swift index d1165cb2..cd4ad2c2 100644 --- a/Sources/MockoloFramework/Utils/SwiftType.swift +++ b/Sources/MockoloFramework/Utils/SwiftType.swift @@ -87,7 +87,9 @@ struct SwiftType: Equatable, CustomStringConvertible { switch nominal.name { case .optionalTypeSugarName where nominal.genericParameterTypes.count == 1: - repr += "\(nominal.genericParameterTypes[0])?" + // For an IUO (`T!`, stored as Optional + isIUO) the `!` is appended by the isIUO + // block below; emitting `?` here too would wrongly render `T?!`. + repr += "\(nominal.genericParameterTypes[0])\(isIUO ? "" : "?")" case .arrayTypeSugarName where nominal.genericParameterTypes.count == 1: repr += "[\(nominal.genericParameterTypes[0])]" case .dictionaryTypeSugarName where nominal.genericParameterTypes.count == 2: @@ -476,11 +478,19 @@ struct SwiftType: Equatable, CustomStringConvertible { returnTypeCast = " as! " + .`Self` } + // `!` (IUO) is illegal inside a closure type, so an IUO param/return must render as a plain + // optional here (it still witnesses the IUO requirement). Clearing `isIUO` is a no-op for + // every non-IUO type, so existing handlers are unaffected. + displayableReturnType.isIUO = false var resultType = SwiftType( kind: .closure(.init( isAsync: isAsync, throwing: throwing, - arguments: params.map { .init(type: $0.processTypeParams(with: typeParams)) }, + arguments: params.map { + var argType = $0.processTypeParams(with: typeParams) + argType.isIUO = false + return .init(type: argType) + }, returning: displayableReturnType )) ) diff --git a/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift b/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift index 31c14c37..be1e6c23 100644 --- a/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift +++ b/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift @@ -35,6 +35,42 @@ import MockoloFramework } } +// IUO func/subscript params and returns: the synthesized handler closure renders `Int?` (IUO is +// illegal inside a closure type), while the signature keeps `Int!` and conforms. +@Fixture enum iuoFuncs { + /// @mockable + public protocol IUOFunc { + func f(x: Int!) -> Int! + subscript(i: Int) -> Int! { get } + } + + @Fixture enum expected { + public class IUOFuncMock: IUOFunc { + public init() { } + public private(set) var fCallCount = 0 + public var fHandler: ((Int?) -> Int?)? + public func f(x: Int!) -> Int! { + fCallCount += 1 + if let fHandler = fHandler { + return fHandler(x) + } + return nil + } + public private(set) var subscriptCallCount = 0 + public var subscriptHandler: ((Int) -> Int?)? + public subscript(i: Int) -> Int! { + get { + subscriptCallCount += 1 + if let subscriptHandler = subscriptHandler { + return subscriptHandler(i) + } + return nil + } + } + } + } +} + @Fixture enum subscripts { /// @mockable protocol SubscriptProtocol { diff --git a/Tests/TestFuncs/TestBasicFuncs/FuncBasicTests.swift b/Tests/TestFuncs/TestBasicFuncs/FuncBasicTests.swift index ea5f0804..1af22d77 100644 --- a/Tests/TestFuncs/TestBasicFuncs/FuncBasicTests.swift +++ b/Tests/TestFuncs/TestBasicFuncs/FuncBasicTests.swift @@ -9,6 +9,11 @@ class BasicFuncTests: MockoloTestCase { dstContent: subscripts.expected._source) } + func testIUOFuncs() { + verify(srcContent: iuoFuncs._source, + dstContent: iuoFuncs.expected._source) + } + func testGetOnlySubscripts() { verify(srcContent: getOnlySubscripts._source, dstContent: getOnlySubscripts.expected._source) diff --git a/Tests/TestVars/FixtureNonSimpleVars.swift b/Tests/TestVars/FixtureNonSimpleVars.swift index 05330f31..720d76d2 100644 --- a/Tests/TestVars/FixtureNonSimpleVars.swift +++ b/Tests/TestVars/FixtureNonSimpleVars.swift @@ -47,3 +47,25 @@ import MockoloFramework } } } + +// An implicitly-unwrapped optional property renders as `Int!` (not `Int?!`) and conforms. +@Fixture enum iuoVars { + /// @mockable + public protocol IUOVar { + var a: Int! { get } + var b: Int! { get set } + } + + @Fixture enum expected { + public class IUOVarMock: IUOVar { + public init() { } + public init(a: Int! = nil, b: Int! = nil) { + self.a = a + self.b = b + } + public var a: Int! = nil + public private(set) var bSetCallCount = 0 + public var b: Int! = nil { didSet { bSetCallCount += 1 } } + } + } +} diff --git a/Tests/TestVars/VarTests.swift b/Tests/TestVars/VarTests.swift index bb510f91..28a6b8bb 100644 --- a/Tests/TestVars/VarTests.swift +++ b/Tests/TestVars/VarTests.swift @@ -21,6 +21,11 @@ class VarTests: MockoloTestCase { allowSetCallCount: true) } + func testIUOVars() { + verify(srcContent: iuoVars._source, + dstContent: iuoVars.expected._source) + } + #if compiler(>=6.0) func testAsyncThrows() { verify(srcContent: asyncThrowsVars._source, From 66263b7d0623a29e587dff212d3ccac473215ce9 Mon Sep 17 00:00:00 2001 From: Iceman Date: Wed, 10 Jun 2026 15:17:12 +0900 Subject: [PATCH 2/2] Treat IUO as a kind of Optional --- .../Utils/StringExtensions.swift | 1 + .../MockoloFramework/Utils/SwiftType.swift | 58 +++++++++++-------- .../FixtureNonSimpleFuncs.swift | 10 ++-- Tests/TestVars/FixtureNonSimpleVars.swift | 1 - 4 files changed, 38 insertions(+), 32 deletions(-) diff --git a/Sources/MockoloFramework/Utils/StringExtensions.swift b/Sources/MockoloFramework/Utils/StringExtensions.swift index 3e24050f..5a0025b6 100644 --- a/Sources/MockoloFramework/Utils/StringExtensions.swift +++ b/Sources/MockoloFramework/Utils/StringExtensions.swift @@ -119,6 +119,7 @@ extension String { static let arrayTypeSugarName = "[]" static let dictionaryTypeSugarName = "[:]" static let optionalTypeSugarName = "?" + static let optionalAutounwrappedTypeSugarName = "!" var safeName: String { var text = self diff --git a/Sources/MockoloFramework/Utils/SwiftType.swift b/Sources/MockoloFramework/Utils/SwiftType.swift index cd4ad2c2..3de21ced 100644 --- a/Sources/MockoloFramework/Utils/SwiftType.swift +++ b/Sources/MockoloFramework/Utils/SwiftType.swift @@ -57,7 +57,6 @@ struct SwiftType: Equatable, CustomStringConvertible { var kind: Kind var attributes: [String] = [] var someOrAny: SomeOrAny? - var isIUO: Bool = false var hasEllipsis: Bool = false var typeName: String { @@ -87,9 +86,9 @@ struct SwiftType: Equatable, CustomStringConvertible { switch nominal.name { case .optionalTypeSugarName where nominal.genericParameterTypes.count == 1: - // For an IUO (`T!`, stored as Optional + isIUO) the `!` is appended by the isIUO - // block below; emitting `?` here too would wrongly render `T?!`. - repr += "\(nominal.genericParameterTypes[0])\(isIUO ? "" : "?")" + repr += "\(nominal.genericParameterTypes[0])?" + case .optionalAutounwrappedTypeSugarName where nominal.genericParameterTypes.count == 1: + repr += "\(nominal.genericParameterTypes[0])!" case .arrayTypeSugarName where nominal.genericParameterTypes.count == 1: repr += "[\(nominal.genericParameterTypes[0])]" case .dictionaryTypeSugarName where nominal.genericParameterTypes.count == 2: @@ -123,14 +122,6 @@ struct SwiftType: Equatable, CustomStringConvertible { case .composition(let composition): repr += composition.elements.map(\.description).joined(separator: " & ") } - if isIUO { - switch kind { - case .tuple, .nominal: - repr += "!" - case .closure, .composition: - repr = "(\(repr))!" - } - } if hasEllipsis { repr += "..." } @@ -193,7 +184,11 @@ struct SwiftType: Equatable, CustomStringConvertible { } var isOptional: Bool { - isNominal(named: .optional) || isNominal(named: .optionalTypeSugarName) + isNominal(named: .optional) || isNominal(named: .optionalTypeSugarName) || isNominal(named: .optionalAutounwrappedTypeSugarName) + } + + var isIUO: Bool { + isNominal(named: .optionalAutounwrappedTypeSugarName) } var isSelf: Bool { @@ -288,9 +283,8 @@ struct SwiftType: Equatable, CustomStringConvertible { ret = unwrapped } } - ret.isIUO = true - return ret.description + return ret.iuoWrapped().description } func defaultVal(with overrides: [String: String]? = nil, overrideKey: String = "", isInitParam: Bool = false) -> String? { @@ -345,7 +339,7 @@ struct SwiftType: Equatable, CustomStringConvertible { var processedType = subtype.processTypeParams(with: typeParams) processedType.attributes.removeAll(where: { $0 == .inout }) processedType.attributes.removeAll(where: { $0 == .escaping }) - processedType.isIUO = false + processedType.unwrapIUO() return processedType } @@ -458,10 +452,9 @@ struct SwiftType: Equatable, CustomStringConvertible { returnAsType = unwrapped asSuffix = "?" } else if returnType.isIUO { - displayableReturnType = .Any - displayableReturnType.isIUO = true + displayableReturnType = .Any.iuoWrapped() var asType = returnType - asType.isIUO = false + asType.unwrapIUO() returnAsType = asType } else if returnType.isSelf { returnAsType = .Self @@ -479,16 +472,15 @@ struct SwiftType: Equatable, CustomStringConvertible { } // `!` (IUO) is illegal inside a closure type, so an IUO param/return must render as a plain - // optional here (it still witnesses the IUO requirement). Clearing `isIUO` is a no-op for - // every non-IUO type, so existing handlers are unaffected. - displayableReturnType.isIUO = false + // optional here (it still witnesses the IUO requirement). + displayableReturnType.unwrapIUO() var resultType = SwiftType( kind: .closure(.init( isAsync: isAsync, throwing: throwing, arguments: params.map { var argType = $0.processTypeParams(with: typeParams) - argType.isIUO = false + argType.unwrapIUO() return .init(type: argType) }, returning: displayableReturnType @@ -640,8 +632,7 @@ extension SwiftType { case .implicitlyUnwrappedOptionalType(let syntax): // T! let base = SwiftType(typeSyntax: syntax.wrappedType) - self = base.optionalWrapped() - self.isIUO = true + self = base.iuoWrapped() case .identifierType(let syntax): // T @@ -730,6 +721,12 @@ extension SwiftType { ) } + func iuoWrapped() -> SwiftType { + return copy( + kind: .nominal(.init(name: .optionalAutounwrappedTypeSugarName, genericParameterTypes: [.init(kind: kind)])) + ) + } + func optionalUnwrapped() -> SwiftType? { guard isOptional else { return nil @@ -740,6 +737,17 @@ extension SwiftType { return nominal.genericParameterTypes.first } + mutating func unwrapIUO() { + guard isNominal(named: .optionalAutounwrappedTypeSugarName) else { + return + } + guard case .nominal(let nominal) = kind, + let wrapped = nominal.genericParameterTypes.first else { + return + } + self = wrapped + } + func copy(kind: Kind) -> Self { var copy = self copy.kind = kind diff --git a/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift b/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift index be1e6c23..b1afa7e9 100644 --- a/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift +++ b/Tests/TestFuncs/TestBasicFuncs/FixtureNonSimpleFuncs.swift @@ -35,20 +35,18 @@ import MockoloFramework } } -// IUO func/subscript params and returns: the synthesized handler closure renders `Int?` (IUO is -// illegal inside a closure type), while the signature keeps `Int!` and conforms. @Fixture enum iuoFuncs { /// @mockable public protocol IUOFunc { func f(x: Int!) -> Int! - subscript(i: Int) -> Int! { get } + subscript(i: Int!) -> Int! { get } } @Fixture enum expected { public class IUOFuncMock: IUOFunc { public init() { } public private(set) var fCallCount = 0 - public var fHandler: ((Int?) -> Int?)? + public var fHandler: ((Int) -> Int)? public func f(x: Int!) -> Int! { fCallCount += 1 if let fHandler = fHandler { @@ -57,8 +55,8 @@ import MockoloFramework return nil } public private(set) var subscriptCallCount = 0 - public var subscriptHandler: ((Int) -> Int?)? - public subscript(i: Int) -> Int! { + public var subscriptHandler: ((Int) -> Int)? + public subscript(i: Int!) -> Int! { get { subscriptCallCount += 1 if let subscriptHandler = subscriptHandler { diff --git a/Tests/TestVars/FixtureNonSimpleVars.swift b/Tests/TestVars/FixtureNonSimpleVars.swift index 720d76d2..821bfeb2 100644 --- a/Tests/TestVars/FixtureNonSimpleVars.swift +++ b/Tests/TestVars/FixtureNonSimpleVars.swift @@ -48,7 +48,6 @@ import MockoloFramework } } -// An implicitly-unwrapped optional property renders as `Int!` (not `Int?!`) and conforms. @Fixture enum iuoVars { /// @mockable public protocol IUOVar {