Skip to content

Commit

Permalink
Support Sendable compliant protocols and classes (#254)
Browse files Browse the repository at this point in the history
* Implemented inheritance of @unchecked Sendable for mock classes conforming to the Sendable protocol

* Corrected syntax for inheritedTypes in template processing

* Fixed CoW overhead issues

* Removed unnecessary imports
  • Loading branch information
nhiroyasu committed Apr 15, 2024
1 parent bfcdee5 commit b57ef4a
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 12 deletions.
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
6 changes: 5 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,9 @@ extension ClassModel {

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

var inheritedTypes = inheritedTypes
inheritedTypes.insert("\(moduleDot)\(identifier)", at: 0)

var body = ""
if !typealiasTemplate.isEmpty {
body += "\(typealiasTemplate)\n"
Expand All @@ -93,7 +97,7 @@ extension ClassModel {
let finalStr = mockFinal ? "\(String.final) " : ""
let template = """
\(attribute)
\(acl)\(finalStr)class \(name): \(moduleDot)\(identifier) {
\(acl)\(finalStr)class \(name): \(inheritedTypes.joined(separator: ", ")) {
\(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.formUnion(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.formUnion(parentInheritedTypes)
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
88 changes: 88 additions & 0 deletions Tests/TestSendable/FixtureSendable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import MockoloFramework

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

let sendableProtocolMock = """
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 = """
/// \(String.mockAnnotation)
public class UncheckedSendableClass: @unchecked Sendable {
func update(arg: Int) -> String
}
"""

let uncheckedSendableClassMock = """
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 = """
public protocol SendableSendable: Sendable {
func update(arg: Int) -> String
}
/// \(String.mockAnnotation)
public protocol ConfirmedSendableProtocol: SendableSendable {
}
"""

let confirmedSendableProtocolMock = """
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 ""
}
}
"""
17 changes: 17 additions & 0 deletions Tests/TestSendable/SendableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
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)
}
}

0 comments on commit b57ef4a

Please sign in to comment.