Skip to content

Commit

Permalink
Concurrency safe handler (#276)
Browse files Browse the repository at this point in the history
* Remove adding unchecked sendable

* Add requireSendable context parameter

* sendable support in method model

* a bit refactoring

* Fix unnecessary param name and type splitting

* Stop using implicit arguments and context

* process argument history type

* Add final when Sendable required

* Fix state handler type

* sendable handler call

* Add @sendable to closure type

* generate concurrency helpers

* Resume unchecked Sendable
  • Loading branch information
sidepelican authored Dec 3, 2024
1 parent 954b952 commit 1fb44ed
Show file tree
Hide file tree
Showing 22 changed files with 551 additions and 244 deletions.
32 changes: 22 additions & 10 deletions Sources/MockoloFramework/Models/ArgumentsHistoryModel.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import Foundation
//
// Copyright (c) 2018. Uber Technologies
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

final class ArgumentsHistoryModel: Model {
let name: String
let type: SwiftType
let capturedValueType: SwiftType
let offset: Int64 = .max
let capturableParamNames: [String]
let capturableParamTypes: [SwiftType]
let capturableParams: [(String, SwiftType)]
let isHistoryAnnotated: Bool

var modelType: ModelType {
Expand All @@ -22,11 +35,10 @@ final class ArgumentsHistoryModel: Model {
self.name = name + .argsHistorySuffix
self.isHistoryAnnotated = isHistoryAnnotated

self.capturableParamNames = capturables.map(\.name.safeName)
self.capturableParamTypes = capturables.map(\.type)
self.capturableParams = capturables.map { ($0.name.safeName, $0.type) }

let genericTypeNameList = genericTypeParams.map(\.name)
self.type = SwiftType.toArgumentsHistoryType(with: capturableParamTypes, typeParams: genericTypeNameList)
self.capturedValueType = SwiftType.toArgumentsCaptureType(with: capturableParams.map(\.1), typeParams: genericTypeNameList)
}

func enable(force: Bool) -> Bool {
Expand All @@ -44,11 +56,11 @@ final class ArgumentsHistoryModel: Model {
return nil
}

switch capturableParamNames.count {
switch capturableParams.count {
case 1:
return "\(overloadingResolvedName)\(String.argsHistorySuffix).append(\(capturableParamNames[0]))"
return "\(overloadingResolvedName)\(String.argsHistorySuffix).append(\(capturableParams[0].0))"
case 2...:
let paramNamesStr = capturableParamNames.joined(separator: ", ")
let paramNamesStr = capturableParams.map(\.0).joined(separator: ", ")
return "\(overloadingResolvedName)\(String.argsHistorySuffix).append((\(paramNamesStr)))"
default:
fatalError("paramNames must not be empty.")
Expand Down
22 changes: 10 additions & 12 deletions Sources/MockoloFramework/Models/ClosureModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,49 +22,47 @@ final class ClosureModel: Model {

let funcReturnType: SwiftType
let genericTypeNames: [String]
let paramNames: [String]
let paramTypes: [SwiftType]
let params: [(String, SwiftType)]
let isAsync: Bool
let throwing: ThrowingKind

var modelType: ModelType {
return .closure
}

init(genericTypeParams: [ParamModel], paramNames: [String], paramTypes: [SwiftType], isAsync: Bool, throwing: ThrowingKind, returnType: SwiftType) {
init(genericTypeParams: [ParamModel], params: [(String, SwiftType)], isAsync: Bool, throwing: ThrowingKind, returnType: SwiftType) {
// In the mock's call handler, rethrows is unavailable.
let throwing = throwing.coerceRethrowsToThrows
self.isAsync = isAsync
self.throwing = throwing
self.genericTypeNames = genericTypeParams.map(\.name)
self.paramNames = paramNames
self.paramTypes = paramTypes
self.params = params
self.funcReturnType = returnType
}

func type(enclosingType: SwiftType) -> SwiftType {
func type(enclosingType: SwiftType, requiresSendable: Bool) -> SwiftType {
return SwiftType.toClosureType(
params: paramTypes,
params: params.map(\.1),
typeParams: genericTypeNames,
isAsync: isAsync,
throwing: throwing,
returnType: funcReturnType,
encloser: enclosingType
encloser: enclosingType,
requiresSendable: requiresSendable
)
}

func render(
context: RenderContext,
arguments: GenerationArguments = .default
arguments: GenerationArguments
) -> String? {
guard let overloadingResolvedName = context.overloadingResolvedName,
let enclosingType = context.enclosingType else {
return nil
}
return applyClosureTemplate(type: type(enclosingType: enclosingType),
return applyClosureTemplate(type: type(enclosingType: enclosingType, requiresSendable: context.requiresSendable),
name: overloadingResolvedName + .handlerSuffix,
paramVals: paramNames,
paramTypes: paramTypes,
params: params,
returnDefaultType: funcReturnType)
}
}
Expand Down
3 changes: 1 addition & 2 deletions Sources/MockoloFramework/Models/MethodModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,7 @@ final class MethodModel: Model {
}

return ClosureModel(genericTypeParams: genericTypeParams,
paramNames: params.map(\.name),
paramTypes: params.map(\.type),
params: params.map { ($0.name, $0.type) },
isAsync: isAsync,
throwing: throwing,
returnType: returnType)
Expand Down
1 change: 1 addition & 0 deletions Sources/MockoloFramework/Models/Model.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct RenderContext {
var overloadingResolvedName: String?
var enclosingType: SwiftType?
var annotatedTypeKind: NominalTypeDeclKind?
var requiresSendable: Bool = false
}

/// Represents a model for an entity such as var, func, class, etc.
Expand Down
11 changes: 5 additions & 6 deletions Sources/MockoloFramework/Models/NominalModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ final class NominalModel: Model {
let accessLevel: String
let identifier: String
let declKindOfMockAnnotatedBaseType: NominalTypeDeclKind
let inheritedTypes: [String]
let entities: [(String, Model)]
let initParamCandidates: [VariableModel]
let declaredInits: [MethodModel]
let metadata: AnnotationMetadata?
let declKind: NominalTypeDeclKind
let requiresSendable: Bool

var modelType: ModelType {
return .nominal
Expand All @@ -39,27 +39,27 @@ final class NominalModel: Model {
acl: String,
declKindOfMockAnnotatedBaseType: NominalTypeDeclKind,
declKind: NominalTypeDeclKind,
inheritedTypes: [String],
attributes: [String],
offset: Int64,
metadata: AnnotationMetadata?,
initParamCandidates: [VariableModel],
declaredInits: [MethodModel],
entities: [(String, Model)]) {
self.identifier = identifier
entities: [(String, Model)],
requiresSendable: Bool) {
self.identifier = identifier
self.name = metadata?.nameOverride ?? (identifier + "Mock")
self.type = SwiftType(self.name)
self.namespaces = namespaces
self.declKindOfMockAnnotatedBaseType = declKindOfMockAnnotatedBaseType
self.declKind = declKind
self.inheritedTypes = inheritedTypes
self.entities = entities
self.declaredInits = declaredInits
self.initParamCandidates = initParamCandidates
self.metadata = metadata
self.offset = offset
self.attribute = Set(attributes.filter {$0.contains(String.available)}).joined(separator: " ")
self.accessLevel = acl
self.requiresSendable = requiresSendable
}

func render(
Expand All @@ -71,7 +71,6 @@ final class NominalModel: Model {
identifier: self.identifier,
accessLevel: accessLevel,
attribute: attribute,
inheritedTypes: inheritedTypes,
metadata: metadata,
arguments: arguments,
initParamCandidates: initParamCandidates,
Expand Down
20 changes: 16 additions & 4 deletions Sources/MockoloFramework/Models/ParamModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
// limitations under the License.
//

import Foundation

final class ParamModel: Model {
internal init(label: String, name: String, type: SwiftType, isGeneric: Bool, inInit: Bool, needsVarDecl: Bool, offset: Int64, length: Int64) {
self.label = label
Expand Down Expand Up @@ -84,9 +82,23 @@ final class ParamModel: Model {
}

func render(
context: RenderContext = .init(),
arguments: GenerationArguments = .default
context: RenderContext,
arguments: GenerationArguments
) -> String? {
return applyParamTemplate(name: name, label: label, type: type, inInit: inInit)
}
}

extension [ParamModel] {
func render(
context: RenderContext,
arguments: GenerationArguments
) -> String {
return self.compactMap {
$0.render(
context: context,
arguments: arguments
)
}.joined(separator: ", ")
}
}
13 changes: 10 additions & 3 deletions Sources/MockoloFramework/Models/ParsedEntity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ struct ResolvedEntity {
var uniqueModels: [(String, Model)]
var attributes: [String]
var inheritedTypes: [String]
var inheritsActorProtocol: Bool

var declaredInits: [MethodModel] {
return uniqueModels.compactMap { (_, model) in
Expand All @@ -39,6 +38,10 @@ struct ResolvedEntity {
)
}

var inheritsActorProtocol: Bool {
return inheritedTypes.contains(.actorProtocol)
}

/// Returns models that can be used as parameters to an initializer
/// @param models The models of the current entity including unprocessed (ones to generate) and
/// processed (already mocked by a previous run if any) models.
Expand All @@ -56,19 +59,23 @@ struct ResolvedEntity {
return result
}

var requiresSendable: Bool {
return inheritedTypes.contains(.sendable) || inheritedTypes.contains(.error)
}

func model() -> Model {
return NominalModel(identifier: key,
namespaces: entity.entityNode.namespaces,
acl: entity.entityNode.accessLevel,
declKindOfMockAnnotatedBaseType: entity.entityNode.declKind,
declKind: inheritsActorProtocol ? .actor : .class,
inheritedTypes: inheritedTypes,
attributes: attributes,
offset: entity.entityNode.offset,
metadata: entity.metadata,
initParamCandidates: initParamCandidates,
declaredInits: declaredInits,
entities: uniqueModels)
entities: uniqueModels,
requiresSendable: requiresSendable)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/MockoloFramework/Models/TypeAliasModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ final class TypeAliasModel: Model {

func render(
context: RenderContext,
arguments: GenerationArguments = .default
arguments: GenerationArguments
) -> String? {
let addAcl = context.annotatedTypeKind == .protocol && !processed
if processed || useDescription, let modelDescription = modelDescription?.trimmingCharacters(in: .whitespacesAndNewlines) {
Expand Down
3 changes: 2 additions & 1 deletion Sources/MockoloFramework/Models/VariableModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ final class VariableModel: Model {
allowSetCallCount: arguments.allowSetCallCount,
shouldOverride: shouldOverride,
accessLevel: accessLevel,
context: context)
context: context,
arguments: arguments)
}
}
18 changes: 13 additions & 5 deletions Sources/MockoloFramework/Operations/Generator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,25 @@ public func generate(sourceDirs: [String],
signpost_begin(name: "Write results")
log("Write the mock results and import lines to", outputFilePath, level: .info)

let needsConcurrencyHelpers = resolvedEntities.contains { $0.requiresSendable }

let imports = handleImports(pathToImportsMap: pathToImportsMap,
customImports: customImports,
customImports: customImports + (needsConcurrencyHelpers ? ["Foundation"] : []),
excludeImports: excludeImports,
testableImports: testableImports,
relevantPaths: relevantPaths)

var helpers = [String]()
if needsConcurrencyHelpers {
helpers.append(applyConcurrencyHelpersTemplate())
}

let result = try write(candidates: candidates,
header: header,
macro: macro,
imports: imports,
to: outputFilePath)
header: header,
macro: macro,
imports: imports,
helpers: helpers,
to: outputFilePath)
signpost_end(name: "Write results")
let t5 = CFAbsoluteTimeGetCurrent()
log("Took", t5-t4, level: .verbose)
Expand Down
14 changes: 11 additions & 3 deletions Sources/MockoloFramework/Operations/OutputWriter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func write(candidates: [(String, Int64)],
header: String?,
macro: String?,
imports: String,
helpers: [String],
to outputFilePath: String) throws -> String {

let entities = candidates
Expand All @@ -35,11 +36,18 @@ func write(candidates: [(String, Int64)],
let headerStr = (header ?? "") + .headerDoc
var macroStart = ""
var macroEnd = ""
if let mcr = macro, !mcr.isEmpty {
macroStart = .poundIf + mcr
if let macro, !macro.isEmpty {
macroStart = .poundIf + macro
macroEnd = .poundEndIf
}
let ret = [headerStr, macroStart, imports, entities.joined(separator: "\n"), macroEnd].joined(separator: "\n\n")
let ret = [
headerStr,
macroStart,
imports,
entities.joined(separator: "\n"),
helpers.joined(separator: "\n\n"),
macroEnd,
].joined(separator: "\n\n")
let currentFileContents = try? String(contentsOfFile: outputFilePath, encoding: .utf8)
guard currentFileContents != ret else {
log("Not writing the file as content is unchanged", level: .info)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,12 @@ private func generateUniqueModels(key: String,
let uniqueModels = [mockedUniqueEntities, unmockedUniqueEntities].flatMap {$0}
.sorted(path: \.value.offset, fallback: \.key)

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

let resolvedEntity = ResolvedEntity(
key: key,
entity: entity,
uniqueModels: uniqueModels,
attributes: attributes,
inheritedTypes: mockInheritedTypes,
inheritsActorProtocol: inheritedTypes.contains(.actorProtocol)
inheritedTypes: inheritedTypes.sorted()
)

return ResolvedEntityContainer(entity: resolvedEntity, paths: paths)
Expand Down
Loading

0 comments on commit 1fb44ed

Please sign in to comment.