From 02610329061b8d0fef989f15e4c59ec8b45bd6eb Mon Sep 17 00:00:00 2001 From: KeitaroKawahara Date: Thu, 10 Jul 2025 14:43:26 +0900 Subject: [PATCH 1/7] Add protocol-level @available annotation inheritance to generated mocks --- Sources/MockoloFramework/Models/ParsedEntity.swift | 5 ++++- Tests/TestActor/FixtureActor.swift | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift index e02486a3..46c20202 100644 --- a/Sources/MockoloFramework/Models/ParsedEntity.swift +++ b/Sources/MockoloFramework/Models/ParsedEntity.swift @@ -65,12 +65,15 @@ struct ResolvedEntity { func model() -> Model { let metadata = entity.metadata + // Combine protocol-level attributes with member-level attributes + let protocolLevelAttributes = entity.entityNode.attributesDescription.isEmpty ? [] : [entity.entityNode.attributesDescription] + let combinedAttributes = protocolLevelAttributes + attributes return NominalModel(name: metadata?.nameOverride ?? (key + "Mock"), namespaces: entity.entityNode.namespaces, acl: entity.entityNode.accessLevel, declKindOfMockAnnotatedBaseType: entity.entityNode.declKind, declKind: inheritsActorProtocol ? .actor : .class, - attributes: attributes, + attributes: combinedAttributes, offset: entity.entityNode.offset, inheritedTypeName: (entity.metadata?.module?.withDot ?? "") + key, genericWhereConstraints: entity.entityNode.genericWhereConstraints, diff --git a/Tests/TestActor/FixtureActor.swift b/Tests/TestActor/FixtureActor.swift index 45373500..b9ab91a2 100644 --- a/Tests/TestActor/FixtureActor.swift +++ b/Tests/TestActor/FixtureActor.swift @@ -81,6 +81,9 @@ init() { } } + @MainActor + /// @mockable + @available(iOS 18.0, *) class P1Mock: P1 { init() { } } From 1ff56ed65e6dbaf78afb8db67b18291cacf5dd64 Mon Sep 17 00:00:00 2001 From: KeitaroKawahara Date: Thu, 26 Mar 2026 23:51:22 +0900 Subject: [PATCH 2/7] Fix unintended formatting changes introduced during conflict resolution --- .../Models/ParsedEntity.swift | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift index 0b39a557..170321cf 100644 --- a/Sources/MockoloFramework/Models/ParsedEntity.swift +++ b/Sources/MockoloFramework/Models/ParsedEntity.swift @@ -27,15 +27,14 @@ struct ResolvedEntity { var declaredInits: [MethodModel] { return uniqueModels.compactMap { (_, model) in guard let model = model as? MethodModel, - model.isInitializer - else { return nil } + model.isInitializer else { return nil } return model } } var initParamCandidates: [VariableModel] { return sortedInitVars( - in: uniqueModels.compactMap { $0.1 as? VariableModel } + in: uniqueModels.compactMap{ $0.1 as? VariableModel } ) } @@ -54,9 +53,9 @@ struct ResolvedEntity { let curVarsSorted = unprocessed.sorted(path: \.offset, fallback: \.name) let curVarNames = curVarsSorted.map(\.name) - let parentVars = processed.filter { !curVarNames.contains($0.name) } + let parentVars = processed.filter {!curVarNames.contains($0.name)} let parentVarsSorted = parentVars.sorted(path: \.offset, fallback: \.name) - let result = [curVarsSorted, parentVarsSorted].flatMap { $0 } + let result = [curVarsSorted, parentVarsSorted].flatMap{$0} return result } @@ -103,10 +102,7 @@ protocol EntityNode { var genericWhereConstraints: [String] { get } var offset: Int64 { get } var hasBlankInit: Bool { get } - func subContainer( - metadata: AnnotationMetadata?, declKind: NominalTypeDeclKind, path: String?, - isProcessed: Bool - ) -> EntityNodeSubContainer + func subContainer(metadata: AnnotationMetadata?, declKind: NominalTypeDeclKind, path: String?, isProcessed: Bool) -> EntityNodeSubContainer } struct EntityNodeSubContainer { @@ -172,30 +168,25 @@ public final class Entity { return metadata != nil } - static func node( - with entityNode: EntityNode, - filepath: String, - isPrivate: Bool, - isFinal: Bool, - metadata: AnnotationMetadata?, - processed: Bool - ) -> Entity? { - - guard !isPrivate, !isFinal else { return nil } - - return Entity( - entityNode: entityNode, - filepath: filepath, - metadata: metadata, - isProcessed: processed) + static func node(with entityNode: EntityNode, + filepath: String, + isPrivate: Bool, + isFinal: Bool, + metadata: AnnotationMetadata?, + processed: Bool) -> Entity? { + + guard !isPrivate, !isFinal else {return nil} + + return Entity(entityNode: entityNode, + filepath: filepath, + metadata: metadata, + isProcessed: processed) } - init( - entityNode: EntityNode, - filepath: String, - metadata: AnnotationMetadata?, - isProcessed: Bool - ) { + init(entityNode: EntityNode, + filepath: String, + metadata: AnnotationMetadata?, + isProcessed: Bool) { self.entityNode = entityNode self.filepath = filepath self.metadata = metadata From d4a41e0c6f9a61742e7f410d40b2f2bfbb0fdc85 Mon Sep 17 00:00:00 2001 From: KeitaroKawahara Date: Fri, 27 Mar 2026 00:20:59 +0900 Subject: [PATCH 3/7] Add tests for protocol-level @available inheritance on Sendable mocks --- Tests/TestSendable/FixtureSendable.swift | 36 ++++++++++++++++++++++++ Tests/TestSendable/SendableTests.swift | 5 ++++ 2 files changed, 41 insertions(+) diff --git a/Tests/TestSendable/FixtureSendable.swift b/Tests/TestSendable/FixtureSendable.swift index 59f128e4..61ba1165 100644 --- a/Tests/TestSendable/FixtureSendable.swift +++ b/Tests/TestSendable/FixtureSendable.swift @@ -208,4 +208,40 @@ } } } + +@Fixture enum availableSendableProtocol { + /// @mockable + @available(iOS 18.0, *) + public protocol Foo: Sendable { + func bar() -> String + } + + @Fixture(includesConcurrencyHelpers: true) + enum expected { + @available(iOS 18.0, *) + public final class FooMock: Foo, @unchecked Sendable { + public init() { } + + + private let barState = MockoloMutex(MockoloHandlerState String>()) + public var barCallCount: Int { + return barState.withLock(\.callCount) + } + public var barHandler: (@Sendable () -> String)? { + get { barState.withLock(\.handler) } + set { barState.withLock { $0.handler = newValue } } + } + public func bar() -> String { + let barHandler = barState.withLock { state in + state.callCount += 1 + return state.handler + } + if let barHandler = barHandler { + return barHandler() + } + return "" + } + } + } +} #endif diff --git a/Tests/TestSendable/SendableTests.swift b/Tests/TestSendable/SendableTests.swift index 4bd7f1a0..3130eadb 100644 --- a/Tests/TestSendable/SendableTests.swift +++ b/Tests/TestSendable/SendableTests.swift @@ -21,5 +21,10 @@ class SendableTests: MockoloTestCase { verify(srcContent: confirmedSendableProtocol._source, dstContent: confirmedSendableProtocol.expected._source) } + + func testAvailableSendableProtocol() { + verify(srcContent: availableSendableProtocol._source, + dstContent: availableSendableProtocol.expected._source) + } } #endif From 2fa6c3d1b0d0e632e34bc4f63337edacdc58ca44 Mon Sep 17 00:00:00 2001 From: KeitaroKawahara Date: Mon, 6 Apr 2026 08:56:36 +0900 Subject: [PATCH 4/7] Add availability-gated Sendable fixture coverage --- Tests/TestSendable/FixtureSendable.swift | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/Tests/TestSendable/FixtureSendable.swift b/Tests/TestSendable/FixtureSendable.swift index 61ba1165..ff9ad166 100644 --- a/Tests/TestSendable/FixtureSendable.swift +++ b/Tests/TestSendable/FixtureSendable.swift @@ -210,28 +210,31 @@ } @Fixture enum availableSendableProtocol { + @available(macOS 99.0, *) + struct Bar {} + /// @mockable - @available(iOS 18.0, *) - public protocol Foo: Sendable { - func bar() -> String + @available(macOS 99.0, *) + protocol Foo: Sendable { + func bar() -> Bar } @Fixture(includesConcurrencyHelpers: true) enum expected { - @available(iOS 18.0, *) - public final class FooMock: Foo, @unchecked Sendable { - public init() { } + @available(macOS 99.0, *) + final class FooMock: Foo, @unchecked Sendable { + init() { } - private let barState = MockoloMutex(MockoloHandlerState String>()) - public var barCallCount: Int { + private let barState = MockoloMutex(MockoloHandlerState Bar>()) + var barCallCount: Int { return barState.withLock(\.callCount) } - public var barHandler: (@Sendable () -> String)? { + var barHandler: (@Sendable () -> Bar)? { get { barState.withLock(\.handler) } set { barState.withLock { $0.handler = newValue } } } - public func bar() -> String { + func bar() -> Bar { let barHandler = barState.withLock { state in state.callCount += 1 return state.handler @@ -239,7 +242,7 @@ if let barHandler = barHandler { return barHandler() } - return "" + fatalError("barHandler returns can't have a default value thus its handler must be set") } } } From 3e5175194d6386e06763de66c249b38003b7966b Mon Sep 17 00:00:00 2001 From: KeitaroKawahara Date: Mon, 6 Apr 2026 08:58:33 +0900 Subject: [PATCH 5/7] Remove redundant ParsedEntity attribute comment --- Sources/MockoloFramework/Models/ParsedEntity.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift index 170321cf..255f225a 100644 --- a/Sources/MockoloFramework/Models/ParsedEntity.swift +++ b/Sources/MockoloFramework/Models/ParsedEntity.swift @@ -65,7 +65,6 @@ struct ResolvedEntity { func model() -> Model { let metadata = entity.metadata - // Combine protocol-level attributes with member-level attributes let protocolLevelAttributes = entity.entityNode.attributesDescription.isEmpty ? [] : [entity.entityNode.attributesDescription] From 7f3a38b86c747398af67344e1a6c047e936ca53e Mon Sep 17 00:00:00 2001 From: KeitaroKawahara Date: Sat, 18 Apr 2026 16:30:19 +0900 Subject: [PATCH 6/7] Fix availability attribute extraction for mock generation --- .../MockoloFramework/Models/ParsedEntity.swift | 7 ++----- .../Parsers/SwiftSyntaxExtensions.swift | 17 +++++++++++++---- Tests/TestActor/FixtureActor.swift | 2 -- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift index 255f225a..020e0ff5 100644 --- a/Sources/MockoloFramework/Models/ParsedEntity.swift +++ b/Sources/MockoloFramework/Models/ParsedEntity.swift @@ -65,10 +65,7 @@ struct ResolvedEntity { func model() -> Model { let metadata = entity.metadata - let protocolLevelAttributes = - entity.entityNode.attributesDescription.isEmpty - ? [] : [entity.entityNode.attributesDescription] - let combinedAttributes = protocolLevelAttributes + attributes + let combinedAttributes = entity.entityNode.attributeDescriptions + attributes return NominalModel(selfType: .init(name: metadata?.nameOverride ?? (key + "Mock")), namespaces: entity.entityNode.namespaces, acl: entity.entityNode.accessLevel, @@ -95,7 +92,7 @@ protocol EntityNode { var nameText: String { get } var mayHaveGlobalActor: Bool { get } var accessLevel: String { get } - var attributesDescription: String { get } + var attributeDescriptions: [String] { get } var declKind: NominalTypeDeclKind { get } var inheritedTypes: [String] { get } var genericWhereConstraints: [String] { get } diff --git a/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift b/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift index e4cf2798..016aff56 100644 --- a/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift +++ b/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift @@ -303,8 +303,8 @@ extension ProtocolDeclSyntax: EntityNode { return genericWhereClause?.requirements.map { $0.with(\.trailingComma, nil).trimmedDescription } ?? [] } - var attributesDescription: String { - self.attributes.trimmedDescription + var attributeDescriptions: [String] { + return attributes.descriptions } func annotationMetadata(with annotation: String) -> AnnotationMetadata? { @@ -354,8 +354,8 @@ extension ClassDeclSyntax: EntityNode { return genericWhereClause?.requirements.map { $0.with(\.trailingComma, nil).trimmedDescription } ?? [] } - var attributesDescription: String { - self.attributes.trimmedDescription + var attributeDescriptions: [String] { + return attributes.descriptions } var isFinal: Bool { @@ -412,6 +412,15 @@ fileprivate func findNamespaces(parent: Syntax?) -> [String] { } extension AttributeListSyntax { + fileprivate var descriptions: [String] { + return compactMap { element in + guard case .attribute(let attribute) = element else { + return nil + } + return attribute.trimmedDescription + } + } + fileprivate var mayHaveGlobalActor: Bool { let wellKnownGlobalActor: Set = [.mainActor] return self.contains { element in diff --git a/Tests/TestActor/FixtureActor.swift b/Tests/TestActor/FixtureActor.swift index b6513476..77aff0f1 100644 --- a/Tests/TestActor/FixtureActor.swift +++ b/Tests/TestActor/FixtureActor.swift @@ -100,8 +100,6 @@ init() { } } - @MainActor - /// @mockable @available(iOS 18.0, *) class P1Mock: P1 { init() { } From 32dffb15d1bf2708b71fb7b38cdccfb8c2bf510d Mon Sep 17 00:00:00 2001 From: KeitaroKawahara Date: Sat, 18 Apr 2026 16:30:46 +0900 Subject: [PATCH 7/7] Add availability test for inherited protocols --- Tests/TestSendable/FixtureSendable.swift | 33 ++++++++++++++++++++++++ Tests/TestSendable/SendableTests.swift | 5 ++++ 2 files changed, 38 insertions(+) diff --git a/Tests/TestSendable/FixtureSendable.swift b/Tests/TestSendable/FixtureSendable.swift index ff9ad166..863a5280 100644 --- a/Tests/TestSendable/FixtureSendable.swift +++ b/Tests/TestSendable/FixtureSendable.swift @@ -247,4 +247,37 @@ } } } + +@Fixture enum availableInheritedProtocol { + @available(macOS 100.0, *) + struct Bar {} + + @available(macOS 90.0, *) + protocol Foo { + } + + /// @mockable + @available(macOS 100.0, *) + protocol Foo2: Foo { + var bar: Bar { get set } + } + + @Fixture enum expected { + @available(macOS 100.0, *) + class Foo2Mock: Foo2 { + init() { } + init(bar: Bar) { + self._bar = bar + } + + + private(set) var barSetCallCount = 0 + private var _bar: Bar! { didSet { barSetCallCount += 1 } } + var bar: Bar { + get { return _bar } + set { _bar = newValue } + } + } + } +} #endif diff --git a/Tests/TestSendable/SendableTests.swift b/Tests/TestSendable/SendableTests.swift index 3130eadb..beef3dfb 100644 --- a/Tests/TestSendable/SendableTests.swift +++ b/Tests/TestSendable/SendableTests.swift @@ -26,5 +26,10 @@ class SendableTests: MockoloTestCase { verify(srcContent: availableSendableProtocol._source, dstContent: availableSendableProtocol.expected._source) } + + func testAvailableInheritedProtocol() { + verify(srcContent: availableInheritedProtocol._source, + dstContent: availableInheritedProtocol.expected._source) + } } #endif