Skip to content

Commit

Permalink
Fix to avoid using global actors as default values (#269)
Browse files Browse the repository at this point in the history
* Fix `any` annotated protocol cannot find default value

* Skip to add typeMap when the type may have globalActor

* A small renaming
  • Loading branch information
sidepelican authored Oct 31, 2024
1 parent 39e406f commit 56d8697
Show file tree
Hide file tree
Showing 10 changed files with 156 additions and 24 deletions.
1 change: 1 addition & 0 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
21 changes: 15 additions & 6 deletions Sources/MockoloFramework/Operations/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
27 changes: 27 additions & 0 deletions Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ extension ProtocolDeclSyntax: EntityNode {
return name.text
}

var mayHaveGlobalActor: Bool {
return attributes.mayHaveGlobalActor
}

var accessLevel: String {
return self.modifiers.acl
}
Expand Down Expand Up @@ -313,6 +317,10 @@ extension ClassDeclSyntax: EntityNode {
return name.text
}

var mayHaveGlobalActor: Bool {
return attributes.mayHaveGlobalActor
}

var accessLevel: String {
return self.modifiers.acl
}
Expand Down Expand Up @@ -381,6 +389,25 @@ fileprivate func findNamespaces(parent: Syntax?) -> [String] {
.reversed()
}

extension AttributeListSyntax {
fileprivate var mayHaveGlobalActor: Bool {
let wellKnownGlobalActor: Set<String> = [.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
Expand Down
15 changes: 7 additions & 8 deletions Sources/MockoloFramework/Templates/ClosureTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 10 additions & 0 deletions Sources/MockoloFramework/Utils/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand All @@ -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) "
}
Expand Down
17 changes: 7 additions & 10 deletions Sources/MockoloFramework/Utils/TypeParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 : "()",
Expand All @@ -656,7 +656,7 @@ public final class SwiftType {
}


private static let defaultTypeValueMap =
private static let defaultValueMap =
["Int": "0",
"Int8": "0",
"Int16": "0",
Expand All @@ -676,6 +676,7 @@ public final class SwiftType {
"TimeInterval": "0.0",
"NSTimeInterval": "0.0",
"PublishSubject": "PublishSubject()",
"Data": "Data()",
"Date": "Date()",
"NSDate": "NSDate()",
"CGRect": ".zero",
Expand All @@ -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()",
Expand Down
5 changes: 5 additions & 0 deletions Tests/TestActor/ActorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@ final class ActorTests: MockoloTestCase {
verify(srcContent: parentProtocolInheritsActor,
dstContent: parentProtocolInheritsActorMock)
}

func testGlobalActorProtocol() {
verify(srcContent: globalActorProtocol,
dstContent: globalActorProtocolMock)
}
}
45 changes: 45 additions & 0 deletions Tests/TestActor/FixtureGlobalActor.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
"""
5 changes: 5 additions & 0 deletions Tests/TestExistentialAny/ExistentialAnyTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,9 @@ class ExistentialAnyTests: MockoloTestCase {
verify(srcContent: existentialAny,
dstContent: existentialAnyMock)
}

func testExistentialAnyDefaultTypeMap() {
verify(srcContent: existentialAnyDefaultTypeMap,
dstContent: existentialAnyDefaultTypeMapMock)
}
}
34 changes: 34 additions & 0 deletions Tests/TestExistentialAny/FixtureExistentialAny.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
}
"""

0 comments on commit 56d8697

Please sign in to comment.