diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift index ba34e746..7cf10edf 100644 --- a/Sources/MockoloFramework/Models/ParsedEntity.swift +++ b/Sources/MockoloFramework/Models/ParsedEntity.swift @@ -87,6 +87,7 @@ struct ResolvedEntityContainer { protocol EntityNode { var namespaces: [String] { get } var nameText: String { get } + var mayHaveGlobalActor: Bool { get } var accessLevel: String { get } var attributesDescription: String { get } var declType: DeclType { get } diff --git a/Sources/MockoloFramework/Operations/Generator.swift b/Sources/MockoloFramework/Operations/Generator.swift index a035d3d9..e020c94c 100644 --- a/Sources/MockoloFramework/Operations/Generator.swift +++ b/Sources/MockoloFramework/Operations/Generator.swift @@ -107,12 +107,21 @@ public func generate(sourceDirs: [String], let t2 = CFAbsoluteTimeGetCurrent() log("Took", t2-t1, level: .verbose) - let typeKeyList = [parentMocks.compactMap {$0.key.components(separatedBy: "Mock").first}, annotatedProtocolMap.map {$0.key}].flatMap{$0} - var typeKeys = [String: String]() - typeKeyList.forEach { (t: String) in - typeKeys[t] = "\(t)Mock()" - } - SwiftType.customTypeMap = typeKeys + let typeKeyList = [ + parentMocks.compactMap { (key, value) -> String? in + if value.entityNode.mayHaveGlobalActor { + return nil + } + return key.components(separatedBy: "Mock").first + }, + annotatedProtocolMap.filter { !$0.value.entityNode.mayHaveGlobalActor }.map(\.key) + ] + .flatMap { $0 } + .map { typeName in + // nameOverride does not work correctly but it giving up. + return (typeName, "\(typeName)Mock()") + } + SwiftType.customDefaultValueMap = [String: String](typeKeyList, uniquingKeysWith: { $1 }) signpost_begin(name: "Generate models") log("Resolve inheritance and generate unique entity models...", level: .info) diff --git a/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift b/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift index dacf2eb4..c3200af7 100644 --- a/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift +++ b/Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift @@ -267,6 +267,10 @@ extension ProtocolDeclSyntax: EntityNode { return name.text } + var mayHaveGlobalActor: Bool { + return attributes.mayHaveGlobalActor + } + var accessLevel: String { return self.modifiers.acl } @@ -313,6 +317,10 @@ extension ClassDeclSyntax: EntityNode { return name.text } + var mayHaveGlobalActor: Bool { + return attributes.mayHaveGlobalActor + } + var accessLevel: String { return self.modifiers.acl } @@ -381,6 +389,25 @@ fileprivate func findNamespaces(parent: Syntax?) -> [String] { .reversed() } +extension AttributeListSyntax { + fileprivate var mayHaveGlobalActor: Bool { + let wellKnownGlobalActor: Set = [.mainActor] + return self.contains { element in + switch element { + case .attribute(let attribute): + return wellKnownGlobalActor.contains(attribute.attributeName.trimmedDescription) + case .ifConfigDecl(let ifConfig): + return ifConfig.clauses.contains { clause in + if case .attributes(let attributes) = clause.elements { + return attributes.mayHaveGlobalActor + } + return false + } + } + } + } +} + extension VariableDeclSyntax { func models(with acl: String, declType: DeclType, metadata: AnnotationMetadata?, processed: Bool) -> [Model] { // Detect whether it's static diff --git a/Sources/MockoloFramework/Templates/ClosureTemplate.swift b/Sources/MockoloFramework/Templates/ClosureTemplate.swift index 75e0ffcd..820e8152 100644 --- a/Sources/MockoloFramework/Templates/ClosureTemplate.swift +++ b/Sources/MockoloFramework/Templates/ClosureTemplate.swift @@ -64,15 +64,14 @@ extension ClosureModel { private func renderReturnDefaultStatement(name: String, type: SwiftType) -> String { guard !type.isUnknown else { return "" } - let result = type.defaultVal() ?? String.fatalError - - if result.isEmpty { - return "" - } - if result.contains(String.fatalError) { - return "\(String.fatalError)(\"\(name) returns can't have a default value thus its handler must be set\")" + if let result = type.defaultVal() { + if result.isEmpty { + return "" + } + return "return \(result)" } - return "return \(result)" + + return "\(String.fatalError)(\"\(name) returns can't have a default value thus its handler must be set\")" } private func renderOptionalGenericClosure( diff --git a/Sources/MockoloFramework/Utils/StringExtensions.swift b/Sources/MockoloFramework/Utils/StringExtensions.swift index 31c2cd65..378ae29f 100644 --- a/Sources/MockoloFramework/Utils/StringExtensions.swift +++ b/Sources/MockoloFramework/Utils/StringExtensions.swift @@ -100,6 +100,7 @@ extension String { static let name = "name" static let sendable = "Sendable" static let uncheckedSendable = "@unchecked Sendable" + static let mainActor = "MainActor" static public let mockAnnotation = "@mockable" static public let mockObservable = "@MockObservable" static public let poundIf = "#if " @@ -120,6 +121,15 @@ extension String { return self } + var removingExistentialAny: String { + var typeName = self + if typeName.hasPrefix(.any) { + typeName.removeFirst(String.any.count) + typeName = typeName.trimmingCharacters(in: .whitespaces) + } + return typeName + } + var withSpace: String { return "\(self) " } diff --git a/Sources/MockoloFramework/Utils/TypeParser.swift b/Sources/MockoloFramework/Utils/TypeParser.swift index 60d102b1..61277529 100644 --- a/Sources/MockoloFramework/Utils/TypeParser.swift +++ b/Sources/MockoloFramework/Utils/TypeParser.swift @@ -343,7 +343,6 @@ public final class SwiftType { /// if "non-nil", type is non-optional /// if "", type is String, with an empty string value func defaultVal(with overrides: [String: String]? = nil, overrideKey: String = "", isInitParam: Bool = false) -> String? { - if let val = cachedDefaultVal { return val } @@ -364,7 +363,8 @@ public final class SwiftType { return cachedDefaultVal } - if let val = SwiftType.customTypeMap?[typeName] { + // There is no "existential any" to the customDefaultValueMap key. + if let val = SwiftType.customDefaultValueMap?[typeName.removingExistentialAny] { cachedDefaultVal = val return val } @@ -466,7 +466,7 @@ public final class SwiftType { return "\(arg.typeName)()" } - if let val = SwiftType.defaultTypeValueMap[arg.typeName] { + if let val = SwiftType.defaultValueMap[arg.typeName] { return val } return nil @@ -640,7 +640,7 @@ public final class SwiftType { } } - public static var customTypeMap: [String: String]? + public static var customDefaultValueMap: [String: String]? private let bracketPrefixTypes = ["Array", "Set", "Dictionary"] private let rxTypes = [String.publishSubject : "()", @@ -656,7 +656,7 @@ public final class SwiftType { } - private static let defaultTypeValueMap = + private static let defaultValueMap = ["Int": "0", "Int8": "0", "Int16": "0", @@ -676,6 +676,7 @@ public final class SwiftType { "TimeInterval": "0.0", "NSTimeInterval": "0.0", "PublishSubject": "PublishSubject()", + "Data": "Data()", "Date": "Date()", "NSDate": "NSDate()", "CGRect": ".zero", @@ -685,14 +686,10 @@ public final class SwiftType { "UIColor": ".black", "UIFont": ".systemFont(ofSize: 12)", "UIImage": "UIImage()", - "UIView": "UIView(frame: .zero)", - "UIViewController": "UIViewController()", - "UICollectionView": "UICollectionView(frame: .zero, collectionViewLayout: UICollectionViewLayout())", - "UICollectionViewLayout": "UICollectionViewLayout()", - "UIScrollView": "UIScrollView()", "UIScrollViewKeyboardDismissMode": ".interactive", "UIAccessibilityTraits": ".none", "Void": "Void", + "()": "()", "URL": "URL(fileURLWithPath: \"\")", "NSURL": "NSURL(fileURLWithPath: \"\")", "UUID": "UUID()", diff --git a/Tests/TestActor/ActorTests.swift b/Tests/TestActor/ActorTests.swift index 7b223b7a..1691649d 100644 --- a/Tests/TestActor/ActorTests.swift +++ b/Tests/TestActor/ActorTests.swift @@ -8,4 +8,9 @@ final class ActorTests: MockoloTestCase { verify(srcContent: parentProtocolInheritsActor, dstContent: parentProtocolInheritsActorMock) } + + func testGlobalActorProtocol() { + verify(srcContent: globalActorProtocol, + dstContent: globalActorProtocolMock) + } } diff --git a/Tests/TestActor/FixtureGlobalActor.swift b/Tests/TestActor/FixtureGlobalActor.swift new file mode 100644 index 00000000..40387ed2 --- /dev/null +++ b/Tests/TestActor/FixtureGlobalActor.swift @@ -0,0 +1,45 @@ +import MockoloFramework + +let globalActorProtocol = """ +/// \(String.mockAnnotation) +@MainActor protocol RootController: AnyObject { + var viewController: UIViewController { get } +} + +/// \(String.mockAnnotation) +protocol RootBuildable { + func build() -> RootController +} +""" + +let globalActorProtocolMock = """ +class RootControllerMock: RootController { + init() { } + init(viewController: UIViewController) { + self._viewController = viewController + } + + + + private var _viewController: UIViewController! + var viewController: UIViewController { + get { return _viewController } + set { _viewController = newValue } + } +} + +class RootBuildableMock: RootBuildable { + init() { } + + + private(set) var buildCallCount = 0 + var buildHandler: (() -> (RootController))? + func build() -> RootController { + buildCallCount += 1 + if let buildHandler = buildHandler { + return buildHandler() + } + fatalError("buildHandler returns can't have a default value thus its handler must be set") + } +} +""" diff --git a/Tests/TestExistentialAny/ExistentialAnyTests.swift b/Tests/TestExistentialAny/ExistentialAnyTests.swift index f0f5bf7e..6e4839c7 100644 --- a/Tests/TestExistentialAny/ExistentialAnyTests.swift +++ b/Tests/TestExistentialAny/ExistentialAnyTests.swift @@ -5,4 +5,9 @@ class ExistentialAnyTests: MockoloTestCase { verify(srcContent: existentialAny, dstContent: existentialAnyMock) } + + func testExistentialAnyDefaultTypeMap() { + verify(srcContent: existentialAnyDefaultTypeMap, + dstContent: existentialAnyDefaultTypeMapMock) + } } diff --git a/Tests/TestExistentialAny/FixtureExistentialAny.swift b/Tests/TestExistentialAny/FixtureExistentialAny.swift index be40c1f5..2bbaeeb4 100644 --- a/Tests/TestExistentialAny/FixtureExistentialAny.swift +++ b/Tests/TestExistentialAny/FixtureExistentialAny.swift @@ -45,3 +45,37 @@ class ExistentialAnyMock: ExistentialAny { } } """ + +let existentialAnyDefaultTypeMap = """ +/// \(String.mockAnnotation) +protocol SomeProtocol { +} + +/// \(String.mockAnnotation) +protocol UseSomeProtocol { + func foo() -> any SomeProtocol +} +""" + +let existentialAnyDefaultTypeMapMock = """ +class SomeProtocolMock: SomeProtocol { + init() { } + + +} + +class UseSomeProtocolMock: UseSomeProtocol { + init() { } + + + private(set) var fooCallCount = 0 + var fooHandler: (() -> (any SomeProtocol))? + func foo() -> any SomeProtocol { + fooCallCount += 1 + if let fooHandler = fooHandler { + return fooHandler() + } + return SomeProtocolMock() + } +} +"""