Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Sendable compliant protocols and classes #254

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Sources/MockoloFramework/Models/ClassModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,20 @@ final class ClassModel: Model {
let accessLevel: String
let identifier: String
let declType: DeclType
let inheritedTypes: [String]
let entities: [(String, Model)]
let initParamCandidates: [VariableModel]
let declaredInits: [MethodModel]
let metadata: AnnotationMetadata?

var modelType: ModelType {
return .class
}

init(identifier: String,
acl: String,
declType: DeclType,
inheritedTypes: [String],
attributes: [String],
offset: Int64,
metadata: AnnotationMetadata?,
Expand All @@ -46,6 +48,7 @@ final class ClassModel: Model {
self.name = metadata?.nameOverride ?? (identifier + "Mock")
self.type = Type(.class)
self.declType = declType
self.inheritedTypes = inheritedTypes
self.entities = entities
self.declaredInits = declaredInits
self.initParamCandidates = initParamCandidates
Expand All @@ -71,6 +74,7 @@ final class ClassModel: Model {
accessLevel: accessLevel,
attribute: attribute,
declType: declType,
inheritedTypes: inheritedTypes,
metadata: metadata,
useTemplateFunc: useTemplateFunc,
useMockObservable: useMockObservable,
Expand Down
2 changes: 2 additions & 0 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct ResolvedEntity {
let entity: Entity
let uniqueModels: [(String, Model)]
let attributes: [String]
let inheritedTypes: [String]

var declaredInits: [MethodModel] {
return uniqueModels.filter {$0.1.isInitializer}.compactMap{ $0.1 as? MethodModel }
Expand Down Expand Up @@ -64,6 +65,7 @@ struct ResolvedEntity {
return ClassModel(identifier: key,
acl: entity.entityNode.accessLevel,
declType: entity.entityNode.declType,
inheritedTypes: inheritedTypes,
attributes: attributes,
offset: entity.entityNode.offset,
metadata: entity.metadata,
Expand Down
15 changes: 10 additions & 5 deletions Sources/MockoloFramework/Operations/UniqueModelGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ private func generateUniqueModels(key: String,
protocolMap: [String: Entity],
inheritanceMap: [String: Entity]) -> ResolvedEntityContainer {

let (models, processedModels, attributes, paths, pathToContentList) = lookupEntities(key: key, declType: entity.entityNode.declType, protocolMap: protocolMap, inheritanceMap: inheritanceMap)
let (models, processedModels, attributes, inheritedTypes, paths, pathToContentList) = lookupEntities(key: key, declType: entity.entityNode.declType, protocolMap: protocolMap, inheritanceMap: inheritanceMap)

let processedFullNames = processedModels.compactMap {$0.fullName}

let processedElements = processedModels.compactMap { (element: Model) -> (String, Model)? in
Expand Down Expand Up @@ -65,8 +65,13 @@ private func generateUniqueModels(key: String,
let mockedUniqueEntities = Dictionary(uniqueKeysWithValues: processedElementsMap)

let uniqueModels = [mockedUniqueEntities, unmockedUniqueEntities].flatMap {$0}

let resolvedEntity = ResolvedEntity(key: key, entity: entity, uniqueModels: uniqueModels, attributes: attributes)


var mockInheritedTypes = [String]()
if inheritedTypes.contains(.sendable) {
mockInheritedTypes.append(.uncheckedSendable)
}

let resolvedEntity = ResolvedEntity(key: key, entity: entity, uniqueModels: uniqueModels, attributes: attributes, inheritedTypes: mockInheritedTypes)

return ResolvedEntityContainer(entity: resolvedEntity, paths: paths, imports: pathToContentList)
}
5 changes: 5 additions & 0 deletions Sources/MockoloFramework/Parsers/SwiftSyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ extension InheritanceClauseSyntax {
} else if let compositionType = type.as(CompositionTypeSyntax.self) {
// example: `protocol A: B & C {}`
return compositionType.elements.map(\.type).map(parseElementType(type:)).flatMap { $0 }
} else if let attributedType = type.as(AttributedTypeSyntax.self) {
// example: `protocol A: @unchecked B {}`
if let baseType = attributedType.baseType.as(IdentifierTypeSyntax.self) {
return [baseType.name.text]
}
}
return []
}
Expand Down
8 changes: 7 additions & 1 deletion Sources/MockoloFramework/Templates/ClassTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ extension ClassModel {
accessLevel: String,
attribute: String,
declType: DeclType,
inheritedTypes: [String],
metadata: AnnotationMetadata?,
useTemplateFunc: Bool,
useMockObservable: Bool,
Expand Down Expand Up @@ -79,6 +80,11 @@ extension ClassModel {

let extraInits = extraInitsIfNeeded(initParamCandidates: initParamCandidates, declaredInits: declaredInits, acl: acl, declType: declType, overrides: metadata?.varTypes)

var inheritedTypesStr = ""
for inheritedType in inheritedTypes {
inheritedTypesStr += ", " + inheritedType
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this can be more beautify like this:

var inheritedTypes = inheritedTypes
inheritedTypes.insert("\(moduleDot)\(identifier)", at: 0)
\(acl)\(finalStr)class \(name): \(inheritedTypes.joined(", ")) {

var body = ""
if !typealiasTemplate.isEmpty {
body += "\(typealiasTemplate)\n"
Expand All @@ -93,7 +99,7 @@ extension ClassModel {
let finalStr = mockFinal ? "\(String.final) " : ""
let template = """
\(attribute)
\(acl)\(finalStr)class \(name): \(moduleDot)\(identifier) {
\(acl)\(finalStr)class \(name): \(moduleDot)\(identifier)\(inheritedTypesStr) {
\(body)
}
"""
Expand Down
14 changes: 9 additions & 5 deletions Sources/MockoloFramework/Utils/InheritanceResolver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,20 @@ import Foundation
/// @param protocolMap Used to look up the current entity and its inheritance types
/// @param inheritanceMap Used to look up inherited types if not contained in protocolMap
/// @returns a list of models representing sub-entities of the current entity, a list of models processed in dependent mock files if exists,
/// cumulated attributes, and a map of filepaths and file contents (used for import lines lookup later).
/// cumulated attributes, cumulated inherited types, and a map of filepaths and file contents (used for import lines lookup later).
func lookupEntities(key: String,
declType: DeclType,
protocolMap: [String: Entity],
inheritanceMap: [String: Entity]) -> ([Model], [Model], [String], [String], [(String, Data, Int64)]) {
inheritanceMap: [String: Entity]) -> ([Model], [Model], [String], Set<String>, [String], [(String, Data, Int64)]) {

// Used to keep track of types to be mocked
var models = [Model]()
// Used to keep track of types that were already mocked
var processedModels = [Model]()
// Gather attributes declared in current or parent protocols
var attributes = [String]()
// Gather inherited types declared in current or parent protocols
var inheritedTypes = Set<String>()
// Gather filepaths and contents used for imports
var pathToContents = [(String, Data, Int64)]()
// Gather filepaths used for imports
Expand All @@ -47,6 +49,7 @@ func lookupEntities(key: String,
if !current.isProcessed {
attributes.append(contentsOf: sub.attributes)
}
inheritedTypes = inheritedTypes.union(current.entityNode.inheritedTypes)
if let data = current.data {
pathToContents.append((current.filepath, data, current.entityNode.offset))
}
Expand All @@ -57,10 +60,11 @@ func lookupEntities(key: String,
// If the protocol inherits other protocols, look up their entities as well.
for parent in current.entityNode.inheritedTypes {
if parent != .class, parent != .anyType, parent != .anyObject {
let (parentModels, parentProcessedModels, parentAttributes, parentPaths, parentPathToContents) = lookupEntities(key: parent, declType: declType, protocolMap: protocolMap, inheritanceMap: inheritanceMap)
let (parentModels, parentProcessedModels, parentAttributes, parentInheritedTypes, parentPaths, parentPathToContents) = lookupEntities(key: parent, declType: declType, protocolMap: protocolMap, inheritanceMap: inheritanceMap)
models.append(contentsOf: parentModels)
processedModels.append(contentsOf: parentProcessedModels)
attributes.append(contentsOf: parentAttributes)
inheritedTypes = inheritedTypes.union(parentInheritedTypes)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please use formUnion to avoid CoW overhead

Copy link
Contributor Author

@nhiroyasu nhiroyasu Apr 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review!
I was not aware of the issue with CoW overhead. I will fix this along with the other review points.

paths.append(contentsOf: parentPaths)
pathToContents.append(contentsOf:parentPathToContents)
}
Expand All @@ -79,7 +83,7 @@ func lookupEntities(key: String,
paths.append(parentMock.filepath)
}

return (models, processedModels, attributes, paths, pathToContents)
return (models, processedModels, attributes, inheritedTypes, paths, pathToContents)
}


Expand Down
2 changes: 2 additions & 0 deletions Sources/MockoloFramework/Utils/StringExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ extension String {
static let `escaping` = "@escaping"
static let autoclosure = "@autoclosure"
static let name = "name"
static let sendable = "Sendable"
static let uncheckedSendable = "@unchecked Sendable"
static public let mockAnnotation = "@mockable"
static public let mockObservable = "@MockObservable"
static public let poundIf = "#if "
Expand Down
94 changes: 94 additions & 0 deletions Tests/TestSendable/FixtureSendable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import MockoloFramework

let sendableProtocol = """
import Foundation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused import.


/// \(String.mockAnnotation)
public protocol SendableProtocol: Sendable {
func update(arg: Int) -> String
}
"""

let sendableProtocolMock = """

import Foundation

public class SendableProtocolMock: SendableProtocol, @unchecked Sendable {
public init() { }


public private(set) var updateCallCount = 0
public var updateHandler: ((Int) -> (String))?
public func update(arg: Int) -> String {
updateCallCount += 1
if let updateHandler = updateHandler {
return updateHandler(arg)
}
return ""
}
}

"""

let uncheckedSendableClass = """
import Foundation

/// \(String.mockAnnotation)
public class UncheckedSendableClass: @unchecked Sendable {
func update(arg: Int) -> String
}
"""

let uncheckedSendableClassMock = """

import Foundation

public class UncheckedSendableClassMock: UncheckedSendableClass, @unchecked Sendable {
public init() { }


private(set) var updateCallCount = 0
var updateHandler: ((Int) -> (String))?
override func update(arg: Int) -> String {
updateCallCount += 1
if let updateHandler = updateHandler {
return updateHandler(arg)
}
return ""
}
}

"""

let confirmedSendableProtocol = """
import Foundation

public protocol SendableSendable: Sendable {
func update(arg: Int) -> String
}

/// \(String.mockAnnotation)
public protocol ConfirmedSendableProtocol: SendableSendable {
}
"""

let confirmedSendableProtocolMock = """

import Foundation

public class ConfirmedSendableProtocolMock: ConfirmedSendableProtocol, @unchecked Sendable {
public init() { }


public private(set) var updateCallCount = 0
public var updateHandler: ((Int) -> (String))?
public func update(arg: Int) -> String {
updateCallCount += 1
if let updateHandler = updateHandler {
return updateHandler(arg)
}
return ""
}
}

"""
20 changes: 20 additions & 0 deletions Tests/TestSendable/SendableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unused import.



class SendableTests: MockoloTestCase {
func testSendableProtocol() {
verify(srcContent: sendableProtocol,
dstContent: sendableProtocolMock)
}

func testUncheckedSendableClass() {
verify(srcContent: uncheckedSendableClass,
dstContent: uncheckedSendableClassMock,
declType: .classType)
}

func testConfirmingSendableProtocol() {
verify(srcContent: confirmedSendableProtocol,
dstContent: confirmedSendableProtocolMock)
}
}