Sources/MockoloFramework/Templates/NominalTemplate.swift (317 lines of code) (raw):

// // 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. // import Foundation extension NominalModel { func applyNominalTemplate(name: String, accessLevel: String, attribute: String, arguments: GenerationArguments, initParamCandidates: [VariableModel], declaredInits: [MethodModel], entities: [(String, Model)]) -> String { processCombineAliases(entities: entities) let acl = accessLevel.isEmpty ? "" : accessLevel + " " let (aliasItems, typeparameters, whereClauses, renderedModelNames) = processAssociatedTypes(in: entities, acl: acl) let renderedEntities = entities .compactMap { (uniqueId: String, model: Model) -> String? in if model is TypealiasRenderableModel && renderedModelNames.contains(model.name) { // this case will be handlded by typealiasWhitelist look up later return nil } if model.modelType == .variable, model.name == String.hasBlankInit { return nil } if model.modelType == .method, let model = model as? MethodModel, model.isInitializer, !model.processed { return nil } return model.render( context: .init( overloadingResolvedName: uniqueId, enclosingType: type, annotatedTypeKind: declKindOfMockAnnotatedBaseType, requiresSendable: requiresSendable ), arguments: arguments ) } .joined(separator: "\n") let extraInits = extraInitsIfNeeded( initParamCandidates: initParamCandidates, declaredInits: declaredInits, acl: acl, declKindOfMockAnnotatedBaseType: declKindOfMockAnnotatedBaseType, context: .init( enclosingType: type, annotatedTypeKind: declKindOfMockAnnotatedBaseType, requiresSendable: requiresSendable ), arguments: arguments ) var body = "" if !extraInits.isEmpty { body += "\(extraInits)\n" } if !aliasItems.isEmpty { body += "\(aliasItems)\n" } if !renderedEntities.isEmpty { body += "\(renderedEntities)" } var uncheckedSendableStr = "" if requiresSendable { uncheckedSendableStr = ", @unchecked Sendable" } let finalStr = arguments.mockFinal || requiresSendable ? String.final.withSpace : "" let template = """ \(attribute) \(acl)\(finalStr)\(declKind.rawValue) \(name)\(typeparameters): \(inheritedTypeName)\(uncheckedSendableStr) \(whereClauses){ \(body) } """ if namespaces.isEmpty { return template } else { return """ extension \(namespaces.joined(separator: ".")) { \(template.addingIndent(1)) } """ } } private func extraInitsIfNeeded( initParamCandidates: [VariableModel], declaredInits: [MethodModel], acl: String, declKindOfMockAnnotatedBaseType: NominalTypeDeclKind, context: RenderContext, arguments: GenerationArguments ) -> String { let declaredInitParamsPerInit = declaredInits.map { $0.params } var needParamedInit = false var needBlankInit = false if declaredInits.isEmpty, initParamCandidates.isEmpty { needBlankInit = true needParamedInit = false } else { if declKindOfMockAnnotatedBaseType == .protocol { needParamedInit = !initParamCandidates.isEmpty needBlankInit = true let buffer = initParamCandidates.sorted(path: \.fullName, fallback: \.name) for paramList in declaredInitParamsPerInit { if paramList.isEmpty { needBlankInit = false } else { let list = paramList.sorted(path: \.fullName, fallback: \.name) if list.count > 0, list.count == buffer.count { let hasDuplicates = zip(list, buffer).contains(where: { $0.fullName == $1.fullName }) if hasDuplicates { needParamedInit = false } } } } } } var initTemplate = "" if needParamedInit { var paramsAssign = "" let params = initParamCandidates .compactMap { (element: VariableModel) -> String? in if let val = element.type?.defaultVal(with: element.rxTypes, overrideKey: element.name, isInitParam: true) { return "\(element.name): \(element.type!.typeName) = \(val)" } if let elementType = element.type { var prefix = "" if elementType.hasClosure == true { if !elementType.isOptional { prefix = String.escaping + " " } } return "\(element.name): \(prefix)\(elementType.typeName)" } else { return nil } } .joined(separator: ", ") paramsAssign = initParamCandidates.map { (element: VariableModel) in switch element.storageKind { case .stored: return "\(2.tab)self.\(element.underlyingName) = \(element.name.safeName)" case .computed: return "\(2.tab)self.\(element.name)\(String.handlerSuffix) = { \(element.name.safeName) }" } }.joined(separator: "\n") initTemplate = """ \(1.tab)\(acl)init(\(params)) { \(paramsAssign) \(1.tab)} """ } let extraInitParamNames = initParamCandidates.map{$0.name} let extraVarsToDecl = Dictionary( grouping: declaredInitParamsPerInit.flatMap { $0.filter { !extraInitParamNames.contains($0.name) } }, by: \.name ) .compactMap { (name: String, params: [ParamModel]) in let shouldErase = params.contains { params[0].type.typeName != $0.type.typeName } return params[0].asInitVarDecl(eraseType: shouldErase) } .sorted() .joined(separator: "\n") let declaredInitStr = declaredInits.compactMap { (m: MethodModel) -> String? in if case let .initKind(required, override) = m.kind, !m.processed { let modifier = required ? "\(String.required) " : (override ? "\(String.override) " : "") let mAcl = m.accessLevel.isEmpty ? "" : "\(m.accessLevel) " let genericTypeDeclsStr = m.genericTypeParams.render(context: context, arguments: arguments) let genericTypesStr = genericTypeDeclsStr.isEmpty ? "" : "<\(genericTypeDeclsStr)>" let paramDeclsStr = m.params.render(context: context, arguments: arguments) let suffixStr = applyFunctionSuffixTemplate( isAsync: m.isAsync, throwing: m.throwing ) if override { let paramsList = m.params.map { param in return "\(param.name): \(param.name.safeName)" }.joined(separator: ", ") return """ \(1.tab)\(modifier)\(mAcl)init\(genericTypesStr)(\(paramDeclsStr)) \(suffixStr){ \(2.tab)super.init(\(paramsList)) \(1.tab)} """ } else { let paramsAssign = m.params.map { param in let underVars = initParamCandidates.compactMap { return $0.name.safeName == param.name.safeName ? $0.underlyingName : nil} if let underVar = underVars.first { return "\(2.tab)self.\(underVar) = \(param.name.safeName)" } else { return "\(2.tab)self.\(param.underlyingName) = \(param.name.safeName)" } }.joined(separator: "\n") return """ \(1.tab)\(modifier)\(mAcl)init\(genericTypesStr)(\(paramDeclsStr)) \(suffixStr){ \(paramsAssign) \(1.tab)} """ } } return nil }.sorted().joined(separator: "\n") var template = "" if !extraVarsToDecl.isEmpty { template += "\(extraVarsToDecl)\n" } if needBlankInit { let blankInit: String if context.annotatedTypeKind == .class { blankInit = "\(acl)override init() { }" } else { // In case of protocol mocking, we want to provide a blank init (if not present already) for convenience, // where instance vars do not have to be set in init since they all have get/set (see VariableTemplate). blankInit = "\(acl)init() { }" } template += "\(1.tab)\(blankInit)\n" } if !initTemplate.isEmpty { template += "\(initTemplate)\n" } if !declaredInitStr.isEmpty { template += "\(declaredInitStr)\n" } return template } func processAssociatedTypes(`in` models: [(String, Model)], acl: String) -> ( aliasItems: String, typeparameters: String, whereClauses: String, renderedModelNames: Set<String> ) { let addAcl = declKindOfMockAnnotatedBaseType == .protocol ? acl : "" let allWhereConstraints = genericWhereConstraints + models.flatMap { ($1 as? AssociatedTypeModel)?.whereConstraints ?? [] } let hasWhereConstraints = !allWhereConstraints.isEmpty let aliasModels = [String: [TypealiasRenderableModel]]( grouping: models.compactMap { $1 as? TypealiasRenderableModel }, by: \.name ).sorted(path: \.key) // If there is a where, do not output typealias as it may not satisfy the conditions if hasWhereConstraints { let aliasItems = aliasModels.compactMap { (name, candidates) in if let defaultType = candidates.firstNonNil(\.defaultType) { return """ \(1.tab)// Unavailable due to the presence of generic constraints \(1.tab)// \(addAcl)\(String.typealias) \(name) = \(defaultType.displayName) """ } return nil }.joined(separator: "\n") let typeparameters = aliasModels.map { (name, candidates) in mergeAssociatedTypes( name: name, models: candidates.compactMap { $0 as? AssociatedTypeModel } ) } return ( aliasItems: aliasItems, typeparameters: typeparameters.isEmpty ? "" : "<\(typeparameters.joined(separator: ", "))>", whereClauses: allWhereConstraints.isEmpty ? "" : "where \(allWhereConstraints.joined(separator: ", ")) ", renderedModelNames: Set(aliasModels.map(\.key)) ) } var aliasItems: String = "" var typeparameters: [String] = [] var renderedModelNames: Set<String> = [] for (name, candidates) in aliasModels { // If only one default type exists and the others have no constraints, use it directly. let havingDefaults = candidates.filter { $0.defaultType != nil } if havingDefaults.count == 1 && !candidates .filter({ $0 !== havingDefaults[0] }) .contains(where: \.hasGenericConstraints) { continue } // If there are no constraints on all candidates, use Any automatically. if candidates.allSatisfy({ !$0.hasGenericConstraints }) { aliasItems.append("\(1.tab)\(addAcl)\(String.typealias) \(name) = Any\n") } else { // The other cases, gather all constraints let typeparameter = mergeAssociatedTypes( name: name, models: candidates.compactMap { $0 as? AssociatedTypeModel } ) typeparameters.append(typeparameter) } renderedModelNames.insert(name) } return ( aliasItems: aliasItems, typeparameters: typeparameters.isEmpty ? "" : "<\(typeparameters.joined(separator: ", "))>", whereClauses: allWhereConstraints.isEmpty ? "" : "where \(allWhereConstraints.joined(separator: ", ")) ", renderedModelNames: renderedModelNames ) func mergeAssociatedTypes(name: String, models: [AssociatedTypeModel]) -> String { let inheritances = models.flatMap(\.inheritances) let typeparameter = if inheritances.isEmpty { name } else { "\(name): \(inheritances.joined(separator: " & "))" } return typeparameter } } // Finds all combine properties that are attempting to use a property wrapper alias // and locates the matching property within the class, if one exists. // private func processCombineAliases(entities: [(String, Model)]) { var variableModels = [VariableModel]() var nameToVariableModels = [String: VariableModel]() for entity in entities { guard let variableModel = entity.1 as? VariableModel else { continue } variableModels.append(variableModel) nameToVariableModels[variableModel.name] = variableModel } for variableModel in variableModels { guard case .property(let wrapper, let name) = variableModel.combineType else { continue } // If a variable member in this entity already exists, link the two together. // Otherwise, the user's setup is incorrect and we will fallback to using a PassthroughSubject. // if let matchingAliasModel = nameToVariableModels[name] { variableModel.wrapperAliasModel = matchingAliasModel matchingAliasModel.propertyWrapper = wrapper } else { variableModel.combineType = .passthroughSubject } } } }