From 71342c53aaff755d0f0d983b87be2b0fab563bec Mon Sep 17 00:00:00 2001 From: elsh Date: Sun, 1 Mar 2020 15:34:06 -0800 Subject: [PATCH 1/3] Add module name override support --- .../MockoloFramework/Models/ClassModel.swift | 13 ++- .../Models/ParsedEntity.swift | 33 ++++---- .../Operations/UniqueModelGenerator.swift | 3 +- .../Parsers/SourceKitExtensions.swift | 67 ++++++---------- .../SwiftSyntaxExtensions.swift | 79 +++++++++---------- .../Templates/ClassTemplate.swift | 41 ++++++++-- .../Templates/VariableTemplate.swift | 2 +- .../Utils/DataExtensions.swift | 23 +++++- .../Utils/InheritanceResolver.swift | 27 +------ .../Utils/StringExtensions.swift | 39 ++++++--- .../MockoloFramework/Utils/TypeParser.swift | 6 +- .../FixtureModuleOverrides.swift | 36 +++++++++ Tests/TestModuleNames/ModuleNameTests.swift | 9 +++ .../NonSimpleCaseTests.swift | 26 ------ .../FixtureRxVars.swift | 0 Tests/TestRx/RxVarTests.swift | 30 +++++++ .../FixtureTestableImportStatements.swift | 0 .../TestableImportStatementsTests.swift | 0 18 files changed, 250 insertions(+), 184 deletions(-) create mode 100644 Tests/TestModuleNames/FixtureModuleOverrides.swift create mode 100644 Tests/TestModuleNames/ModuleNameTests.swift rename Tests/{TestNonSimpleVars => TestRx}/FixtureRxVars.swift (100%) create mode 100644 Tests/TestRx/RxVarTests.swift rename Tests/{TestTestableImportStatements => TestTestableImports}/FixtureTestableImportStatements.swift (100%) rename Tests/{TestTestableImportStatements => TestTestableImports}/TestableImportStatementsTests.swift (100%) diff --git a/Sources/MockoloFramework/Models/ClassModel.swift b/Sources/MockoloFramework/Models/ClassModel.swift index 7125e85c..e4939c3d 100644 --- a/Sources/MockoloFramework/Models/ClassModel.swift +++ b/Sources/MockoloFramework/Models/ClassModel.swift @@ -25,11 +25,10 @@ final class ClassModel: Model { let identifier: String let declType: DeclType let entities: [(String, Model)] - let typealiasWhitelist: [String: [String]]? let initParamCandidates: [Model] let declaredInits: [MethodModel] - let overrides: [String: String]? - + let metadata: AnnotationMetadata? + var modelType: ModelType { return .class } @@ -39,8 +38,7 @@ final class ClassModel: Model { declType: DeclType, attributes: [String], offset: Int64, - overrides: [String: String]?, - typealiasWhitelist: [String: [String]]?, + metadata: AnnotationMetadata?, initParamCandidates: [Model], declaredInits: [MethodModel], entities: [(String, Model)]) { @@ -51,14 +49,13 @@ final class ClassModel: Model { self.entities = entities self.declaredInits = declaredInits self.initParamCandidates = initParamCandidates - self.overrides = overrides + self.metadata = metadata self.offset = offset self.attribute = Set(attributes.filter {$0.contains(String.available)}).joined(separator: " ") self.accessControlLevelDescription = acl.isEmpty ? "" : acl + " " - self.typealiasWhitelist = typealiasWhitelist } func render(with identifier: String, typeKeys: [String: String]? = nil) -> String? { - return applyClassTemplate(name: name, identifier: self.identifier, typeKeys: typeKeys, accessControlLevelDescription: accessControlLevelDescription, attribute: attribute, declType: declType, overrides: overrides, typealiasWhitelist: typealiasWhitelist, initParamCandidates: initParamCandidates, declaredInits: declaredInits, entities: entities) + return applyClassTemplate(name: name, identifier: self.identifier, typeKeys: typeKeys, accessControlLevelDescription: accessControlLevelDescription, attribute: attribute, declType: declType, metadata: metadata, initParamCandidates: initParamCandidates, declaredInits: declaredInits, entities: entities) } } diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift index c69b70aa..080eaf72 100644 --- a/Sources/MockoloFramework/Models/ParsedEntity.swift +++ b/Sources/MockoloFramework/Models/ParsedEntity.swift @@ -23,7 +23,6 @@ struct ResolvedEntity { let entity: Entity let uniqueModels: [(String, Model)] let attributes: [String] - let typealiasWhitelist: [String: [String]]? var declaredInits: [MethodModel] { return uniqueModels.filter {$0.1.isInitializer}.compactMap{ $0.1 as? MethodModel } @@ -66,8 +65,7 @@ struct ResolvedEntity { declType: entity.entityNode.declType, attributes: attributes, offset: entity.entityNode.offset, - overrides: entity.overrides, - typealiasWhitelist: typealiasWhitelist, + metadata: entity.metadata, initParamCandidates: initParamCandidates, declaredInits: declaredInits, entities: uniqueModels) @@ -88,7 +86,7 @@ protocol EntityNode { var inheritedTypes: [String] { get } var offset: Int64 { get } var hasBlankInit: Bool { get } - func subContainer(overrides: [String: String]?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer + func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer } final class EntityNodeSubContainer { @@ -102,10 +100,12 @@ final class EntityNodeSubContainer { } } -// Contains arguments to annotation -// Ex. @mockable(typealias: T = Any; U = String; ...) +/// Contains arguments to annotation +/// e.g. @mockable(module: name = Foo; typealias: T = Any; U = String; rx: barStream = PublishSubject) struct AnnotationMetadata { - var overrides: [String: String]? + var module: String? + var typeAliases: [String: String]? + var varTypes: [String: String]? } @@ -113,12 +113,14 @@ struct AnnotationMetadata { public final class Entity { var filepath: String = "" var data: Data? = nil - - let isAnnotated: Bool - let overrides: [String: String]? let entityNode: EntityNode let isProcessed: Bool - + let metadata: AnnotationMetadata? + + var isAnnotated: Bool { + return metadata != nil + } + static func node(with entityNode: EntityNode, filepath: String = "", data: Data? = nil, @@ -132,8 +134,7 @@ public final class Entity { let node = Entity(entityNode: entityNode, filepath: filepath, data: data, - isAnnotated: metadata != nil, - overrides: metadata?.overrides, + metadata: metadata, isProcessed: processed) return node @@ -142,14 +143,12 @@ public final class Entity { init(entityNode: EntityNode, filepath: String = "", data: Data? = nil, - isAnnotated: Bool, - overrides: [String: String]?, + metadata: AnnotationMetadata?, isProcessed: Bool) { self.entityNode = entityNode self.filepath = filepath self.data = data - self.isAnnotated = isAnnotated - self.overrides = overrides + self.metadata = metadata self.isProcessed = isProcessed } } diff --git a/Sources/MockoloFramework/Operations/UniqueModelGenerator.swift b/Sources/MockoloFramework/Operations/UniqueModelGenerator.swift index 69a9d58b..6e136902 100644 --- a/Sources/MockoloFramework/Operations/UniqueModelGenerator.swift +++ b/Sources/MockoloFramework/Operations/UniqueModelGenerator.swift @@ -79,8 +79,7 @@ func generateUniqueModels(key: String, let uniqueModels = [mockedUniqueEntities, unmockedUniqueEntities].flatMap {$0} - let whitelist = typealiasWhitelist(in: uniqueModels) - let resolvedEntity = ResolvedEntity(key: key, entity: entity, uniqueModels: uniqueModels, attributes: attributes, typealiasWhitelist: whitelist) + let resolvedEntity = ResolvedEntity(key: key, entity: entity, uniqueModels: uniqueModels, attributes: attributes) return ResolvedEntityContainer(entity: resolvedEntity, paths: paths, imports: pathToContentList) } diff --git a/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift b/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift index 67d7dc8e..9273fc73 100644 --- a/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift +++ b/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift @@ -33,46 +33,29 @@ extension Structure: EntityNode { guard let _ = extracted.range(of: annotation) else { return nil } var ret = AnnotationMetadata() - // Look up the typealias argument if any - var args = [String: String]() - if let arg = Data.typealias, let argMap = parseAnnotationArguments(key: arg, in: extracted) { - args = argMap - } - if let arg = Data.rx, let argMap = parseAnnotationArguments(key: arg, in: extracted) { - for (k, v) in argMap { - args[k] = v + // Look up override arguments if any + if let argsMap = extracted.parseAnnotationArguments(for: String.typealiasColon, String.moduleColon, String.rxColon, String.varColon) { + if let val = argsMap[.typealiasColon] { + ret.typeAliases = val } - } - ret.overrides = args - - return ret - } - - private func parseAnnotationArguments(key: Data, in extracted: Data) -> [String: String]? { - if let keyRange = extracted.range(of: key) { - let args = extracted[keyRange.endIndex...] - let argsStr = String(data: args, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) - - if var patValStr = argsStr { - if patValStr.hasSuffix(")") { - patValStr.removeLast() - } - let aliases = patValStr.components(separatedBy: String.annotationArgDelimiter).filter { !$0.isEmpty } - var aliasMap = [String: String]() - - aliases.forEach { (item: String) in - let keyVal = item.components(separatedBy: "=").map{$0.trimmingCharacters(in: .whitespaces)} - if let key = keyVal.first, let val = keyVal.last { - aliasMap[key] = val - } + if let val = argsMap[.rxColon] { + ret.varTypes = val + } + if let val = argsMap[.varColon] { + if ret.varTypes == nil { + ret.varTypes = val + } else { + ret.varTypes?.merge(val, uniquingKeysWith: {$1}) } - - return aliasMap + } + if let val = argsMap[.moduleColon] { + ret.module = val[.name] } } - return nil + return ret } + func extractAttributes(_ data: Data, filterOn: String? = nil) -> [String] { guard let attributeDict = attributes else { return [] @@ -115,16 +98,16 @@ extension Structure: EntityNode { return result } - func subContainer(overrides: [String: String]?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer { - let memberList = members(with: path, encloserType: declType, data: data, overrides: overrides, processed: isProcessed) + func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer { + let memberList = members(with: path, encloserType: declType, data: data, metadata: metadata, processed: isProcessed) let subAttributes = memberAttributes(with: data) return EntityNodeSubContainer(attributes: subAttributes, members: memberList, hasInit: hasInitMember) } - func members(with path: String?, encloserType: DeclType, data: Data?, overrides: [String: String]?, processed: Bool) -> [Model] { + func members(with path: String?, encloserType: DeclType, data: Data?, metadata: AnnotationMetadata?, processed: Bool) -> [Model] { guard let path = path, let data = data else { return [] } return self.substructures.compactMap { (child: Structure) -> Model? in - return model(for: child, encloserType: encloserType, filepath: path, data: data, overrides: overrides, processed: processed) + return model(for: child, encloserType: encloserType, filepath: path, data: data, metadata: metadata, processed: processed) } } @@ -160,10 +143,10 @@ extension Structure: EntityNode { return true } - func model(for element: Structure, encloserType: DeclType, filepath: String, data: Data, overrides: [String: String]?, processed: Bool = false) -> Model? { + func model(for element: Structure, encloserType: DeclType, filepath: String, data: Data, metadata: AnnotationMetadata?, processed: Bool = false) -> Model? { if element.isVariable { if validateMember(element, declType, processed: processed) { - return VariableModel(element, encloserType: encloserType, filepath: filepath, data: data, overrideTypes: overrides, processed: processed) + return VariableModel(element, encloserType: encloserType, filepath: filepath, data: data, overrideTypes: metadata?.varTypes, processed: processed) } } else if element.isMethod || element.isSubscript { // initializer is considered a method by sourcekit var validated = false @@ -179,7 +162,7 @@ extension Structure: EntityNode { return nil } else if element.isAssociatedType || element.isTypealias { - return TypeAliasModel(element, filepath: filepath, data: data, overrideTypes: overrides, processed: processed) + return TypeAliasModel(element, filepath: filepath, data: data, overrideTypes: metadata?.typeAliases, processed: processed) } return nil @@ -241,7 +224,7 @@ extension Structure: EntityNode { } var isInitializer: Bool { - return name.hasPrefix(.initializerPrefix) && isInstanceMethod + return name.hasPrefix(.initializerLeftParen) && isInstanceMethod } var hasBlankInit: Bool { diff --git a/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift b/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift index 2c568b45..5301e780 100644 --- a/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift +++ b/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift @@ -145,11 +145,11 @@ extension MemberDeclListItemSyntax { return modifiers?.acl ?? "" } - func transformToModel(with encloserAcl: String, declType: DeclType, overrides: [String: String]?, processed: Bool) -> (Model, String?, Bool)? { + func transformToModel(with encloserAcl: String, declType: DeclType, metadata: AnnotationMetadata?, processed: Bool) -> (Model, String?, Bool)? { if let varMember = self.decl as? VariableDeclSyntax { if validateMember(varMember.modifiers, declType, processed: processed) { let acl = memberAcl(varMember.modifiers, encloserAcl, declType) - if let item = varMember.models(with: acl, declType: declType, overrides: overrides, processed: processed).first { + if let item = varMember.models(with: acl, declType: declType, overrides: metadata?.varTypes, processed: processed).first { return (item, varMember.attributes?.trimmedDescription, false) } } @@ -173,14 +173,14 @@ extension MemberDeclListItemSyntax { } } else if let patMember = self.decl as? AssociatedtypeDeclSyntax { let acl = memberAcl(patMember.modifiers, encloserAcl, declType) - let item = patMember.model(with: acl, declType: declType, overrides: overrides, processed: processed) + let item = patMember.model(with: acl, declType: declType, overrides: metadata?.typeAliases, processed: processed) return (item, patMember.attributes?.trimmedDescription, false) } else if let taMember = self.decl as? TypealiasDeclSyntax { let acl = memberAcl(taMember.modifiers, encloserAcl, declType) - let item = taMember.model(with: acl, declType: declType, overrides: overrides, processed: processed) + let item = taMember.model(with: acl, declType: declType, overrides: metadata?.typeAliases, processed: processed) return (item, taMember.attributes?.trimmedDescription, false) } else if let ifMacroMember = self.decl as? IfConfigDeclSyntax { - let (item, attr, initFlag) = ifMacroMember.model(with: encloserAcl, declType: declType, overrides: overrides, processed: processed) + let (item, attr, initFlag) = ifMacroMember.model(with: encloserAcl, declType: declType, metadata: metadata, processed: processed) return (item, attr, initFlag) } @@ -204,13 +204,13 @@ extension MemberDeclListSyntax { return false } - func memberData(with encloserAcl: String, declType: DeclType, overrides: [String: String]?, processed: Bool) -> EntityNodeSubContainer { + func memberData(with encloserAcl: String, declType: DeclType, metadata: AnnotationMetadata?, processed: Bool) -> EntityNodeSubContainer { var attributeList = [String]() var memberList = [Model]() var hasInit = false for m in self { - if let (item, attr, initFlag) = m.transformToModel(with: encloserAcl, declType: declType, overrides: overrides, processed: processed) { + if let (item, attr, initFlag) = m.transformToModel(with: encloserAcl, declType: declType, metadata: metadata, processed: processed) { memberList.append(item) if let attrDesc = attr { attributeList.append(attrDesc) @@ -223,7 +223,7 @@ extension MemberDeclListSyntax { } extension IfConfigDeclSyntax { - func model(with encloserAcl: String, declType: DeclType, overrides: [String: String]?, processed: Bool) -> (Model, String?, Bool) { + func model(with encloserAcl: String, declType: DeclType, metadata: AnnotationMetadata?, processed: Bool) -> (Model, String?, Bool) { var subModels = [Model]() var attrDesc: String? var hasInit = false @@ -234,7 +234,7 @@ extension IfConfigDeclSyntax { name = desc for element in list { - if let (item, attr, initFlag) = element.transformToModel(with: encloserAcl, declType: declType, overrides: overrides, processed: processed) { + if let (item, attr, initFlag) = element.transformToModel(with: encloserAcl, declType: declType, metadata: metadata, processed: processed) { subModels.append(item) if let attr = attr, attr.contains(String.available) { attrDesc = attr @@ -287,8 +287,8 @@ extension ProtocolDeclSyntax: EntityNode { return false } - func subContainer(overrides: [String: String]?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer { - return self.members.members.memberData(with: acl, declType: declType, overrides: overrides, processed: isProcessed) + func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer { + return self.members.members.memberData(with: acl, declType: declType, metadata: metadata, processed: isProcessed) } } @@ -338,8 +338,8 @@ extension ClassDeclSyntax: EntityNode { return leadingTrivia?.annotationMetadata(with: annotation) } - func subContainer(overrides: [String: String]?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer { - return self.members.members.memberData(with: acl, declType: declType, overrides: nil, processed: isProcessed) + func subContainer(metadata: AnnotationMetadata?, declType: DeclType, path: String?, data: Data?, isProcessed: Bool) -> EntityNodeSubContainer { + return self.members.members.memberData(with: acl, declType: declType, metadata: nil, processed: isProcessed) } } @@ -613,45 +613,40 @@ extension Trivia { // of typealias decls for T and U. private func metadata(with annotation: String, in val: String) -> AnnotationMetadata? { if val.contains(annotation) { - var argsMap: [String: String]? let comps = val.components(separatedBy: annotation) - if var last = comps.last, !last.isEmpty { - if last.hasPrefix("(") { - last.removeFirst() + var ret = AnnotationMetadata() + if var argsStr = comps.last, !argsStr.isEmpty { + if argsStr.hasPrefix("(") { + argsStr.removeFirst() } - if last.hasSuffix(")") { - last.removeLast() + if argsStr.hasSuffix(")") { + argsStr.removeLast() } - if let args = last.components(separatedBy: String.typealiasColon).last, !args.isEmpty { - let ret = parseAnnotationArguments(in: args) - argsMap = ret + if argsStr.contains(String.typealiasColon), let subStr = argsStr.components(separatedBy: String.typealiasColon).last, !subStr.isEmpty { + ret.typeAliases = subStr.arguments(with: .annotationArgDelimiter) } - - if let args = last.components(separatedBy: String.rxColon).last, !args.isEmpty { - let ret = parseAnnotationArguments(in: args) - for (k, v) in ret { - argsMap?[k] = v + if argsStr.contains(String.moduleColon), let subStr = argsStr.components(separatedBy: String.moduleColon).last, !subStr.isEmpty { + let val = subStr.arguments(with: .annotationArgDelimiter) + ret.module = val?[.name] + } + if argsStr.contains(String.rxColon), let subStr = argsStr.components(separatedBy: String.rxColon).last, !subStr.isEmpty { + ret.varTypes = subStr.arguments(with: .annotationArgDelimiter) + } + if argsStr.contains(String.varColon), let subStr = argsStr.components(separatedBy: String.varColon).last, !subStr.isEmpty { + if let val = subStr.arguments(with: .annotationArgDelimiter) { + if ret.varTypes == nil { + ret.varTypes = val + } else { + ret.varTypes?.merge(val, uniquingKeysWith: {$1}) + } } } } - return AnnotationMetadata(overrides: argsMap) + return ret } return nil } - private func parseAnnotationArguments(in argstr: String) -> [String: String] { - let args = argstr.components(separatedBy: String.annotationArgDelimiter) - var argsMap = [String: String]() - args.forEach { (item: String) in - let keyVal = item.components(separatedBy: "=").map{$0.trimmingCharacters(in: .whitespaces)} - if let k = keyVal.first, let v = keyVal.last { - argsMap[k] = v - } - } - return argsMap - } - - // Looks up an annotation (e.g. /// @mockable) and its arguments if any. // See metadata(with:, in:) for more info on the annotation arguments. func annotationMetadata(with annotation: String) -> AnnotationMetadata? { @@ -677,3 +672,5 @@ extension Trivia { return nil } } + + diff --git a/Sources/MockoloFramework/Templates/ClassTemplate.swift b/Sources/MockoloFramework/Templates/ClassTemplate.swift index 49f77d59..df72acf7 100644 --- a/Sources/MockoloFramework/Templates/ClassTemplate.swift +++ b/Sources/MockoloFramework/Templates/ClassTemplate.swift @@ -22,17 +22,16 @@ func applyClassTemplate(name: String, accessControlLevelDescription: String, attribute: String, declType: DeclType, - overrides: [String: String]?, - typealiasWhitelist: [String: [String]]?, + metadata: AnnotationMetadata?, initParamCandidates: [Model], declaredInits: [MethodModel], entities: [(String, Model)]) -> String { - let extraInits = extraInitsIfNeeded(initParamCandidates: initParamCandidates, declaredInits: declaredInits, accessControlLevelDescription: accessControlLevelDescription, declType: declType, overrides: overrides, typeKeys: typeKeys) - + let extraInits = extraInitsIfNeeded(initParamCandidates: initParamCandidates, declaredInits: declaredInits, accessControlLevelDescription: accessControlLevelDescription, declType: declType, overrides: metadata?.varTypes, typeKeys: typeKeys) + let typealiases = typealiasWhitelist(in: entities) let renderedEntities = entities .compactMap { (uniqueId: String, model: Model) -> (String, Int64)? in - if model.modelType == .typeAlias, let _ = typealiasWhitelist?[model.name] { + if model.modelType == .typeAlias, let _ = typealiases?[model.name] { // this case will be handlded by typealiasWhitelist look up later return nil } @@ -54,16 +53,21 @@ func applyClassTemplate(name: String, .joined(separator: "\n") var typealiasTemplate = "" - if let typealiasWhitelist = typealiasWhitelist { + if let typealiasWhitelist = typealiases { typealiasTemplate = typealiasWhitelist.map { (arg: (key: String, value: [String])) -> String in let joinedType = arg.value.sorted().joined(separator: " & ") return "\(String.typealias) \(arg.key) = \(joinedType)" }.joined(separator: "\n") } - + + var moduleDot = "" + if let moduleName = metadata?.module, !moduleName.isEmpty { + moduleDot = moduleName + "." + } + let template = """ \(attribute) - \(accessControlLevelDescription)class \(name): \(identifier) { + \(accessControlLevelDescription)class \(moduleDot)\(name): \(moduleDot)\(identifier) { \(1.tab)\(typealiasTemplate) \(extraInits) \(renderedEntities) @@ -166,3 +170,24 @@ private func extraInitsIfNeeded(initParamCandidates: [Model], return template } + +/// Returns a map of typealiases with conflicting types to be whitelisted +/// @param models Potentially contains typealias models +/// @returns A map of typealiases with multiple possible types +func typealiasWhitelist(`in` models: [(String, Model)]) -> [String: [String]]? { + let typealiasModels = models.filter{$0.1.modelType == .typeAlias} + var aliasMap = [String: [String]]() + typealiasModels.forEach { (arg: (key: String, value: Model)) in + + let alias = arg.value + if aliasMap[alias.name] == nil { + aliasMap[alias.name] = [alias.type.typeName] + } else { + if let val = aliasMap[alias.name], !val.contains(alias.type.typeName) { + aliasMap[alias.name]?.append(alias.type.typeName) + } + } + } + let aliasDupes = aliasMap.filter {$0.value.count > 1} + return aliasDupes.isEmpty ? nil : aliasDupes +} diff --git a/Sources/MockoloFramework/Templates/VariableTemplate.swift b/Sources/MockoloFramework/Templates/VariableTemplate.swift index e5ba3767..429acaba 100644 --- a/Sources/MockoloFramework/Templates/VariableTemplate.swift +++ b/Sources/MockoloFramework/Templates/VariableTemplate.swift @@ -117,7 +117,7 @@ func applyRxVariableTemplate(name: String, } let typeName = type.typeName - if let range = typeName.range(of: String.observableVarPrefix), let lastIdx = typeName.lastIndex(of: ">") { + if let range = typeName.range(of: String.observableLeftAngleBracket), let lastIdx = typeName.lastIndex(of: ">") { let typeParamStr = typeName[range.upperBound.. Data? { guard offset >= 0, length > 0 else { return nil } @@ -33,6 +33,25 @@ extension Data { guard let subdata = sliced(offset: offset, length: length) else { return "" } return String(data: subdata, encoding: .utf8) ?? "" } + + func parseAnnotationArguments(for keys: String...) -> [String: [String: String]]? { + let extracted = self + var ret = [String: [String: String]]() + for k in keys { + if let key = k.data(using: .utf8), let keyRange = extracted.range(of: key) { + let argsData = extracted[keyRange.endIndex...] + let argsStr = String(data: argsData, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + if var mutableStr = argsStr { + if mutableStr.hasSuffix(")") { + mutableStr.removeLast() + } + let overrideArgs = mutableStr.arguments(with: String.annotationArgDelimiter) + ret[k] = overrideArgs + } + } + } + return ret + } } diff --git a/Sources/MockoloFramework/Utils/InheritanceResolver.swift b/Sources/MockoloFramework/Utils/InheritanceResolver.swift index 8f037805..b0905e3c 100644 --- a/Sources/MockoloFramework/Utils/InheritanceResolver.swift +++ b/Sources/MockoloFramework/Utils/InheritanceResolver.swift @@ -42,7 +42,7 @@ func lookupEntities(key: String, // Look up the mock entities of a protocol specified by the name. if let current = protocolMap[key] { - let sub = current.entityNode.subContainer(overrides: current.overrides, declType: declType, path: current.filepath, data: current.data, isProcessed: current.isProcessed) + let sub = current.entityNode.subContainer(metadata: current.metadata, declType: declType, path: current.filepath, data: current.data, isProcessed: current.isProcessed) models.append(contentsOf: sub.members) if !current.isProcessed { attributes.append(contentsOf: sub.attributes) @@ -68,7 +68,7 @@ func lookupEntities(key: String, } } else if let parentMock = inheritanceMap["\(key)Mock"], declType == .protocolType { // If the parent protocol is not in the protocol map, look it up in the input parent mocks map. - let sub = parentMock.entityNode.subContainer(overrides: parentMock.overrides, declType: declType, path: parentMock.filepath, data: parentMock.data, isProcessed: parentMock.isProcessed) + let sub = parentMock.entityNode.subContainer(metadata: parentMock.metadata, declType: declType, path: parentMock.filepath, data: parentMock.data, isProcessed: parentMock.isProcessed) processedModels.append(contentsOf: sub.members) if !parentMock.isProcessed { attributes.append(contentsOf: sub.attributes) @@ -152,27 +152,6 @@ func uniqueEntities(`in` models: [Model], exclude: [String: Model], fullnames: [ } -/// Returns a map of typealiases with conflicting types to be whitelisted -/// @param models Potentially contains typealias models -/// @returns A map of typealiases with multiple possible types -func typealiasWhitelist(`in` models: [(String, Model)]) -> [String: [String]]? { - let typealiasModels = models.filter{$0.1.modelType == .typeAlias} - var aliasMap = [String: [String]]() - typealiasModels.forEach { (arg: (key: String, value: Model)) in - - let alias = arg.value - if aliasMap[alias.name] == nil { - aliasMap[alias.name] = [alias.type.typeName] - } else { - if let val = aliasMap[alias.name], !val.contains(alias.type.typeName) { - aliasMap[alias.name]?.append(alias.type.typeName) - } - } - } - let aliasDupes = aliasMap.filter {$0.value.count > 1} - return aliasDupes.isEmpty ? nil : aliasDupes -} - /// Returns import lines of a file /// @param content The source file content @@ -182,7 +161,7 @@ func findImportLines(data: Data, offset: Int64?) -> [String] { if let offset = offset, offset > 0 { let part = data.toString(offset: 0, length: offset) let lines = part.components(separatedBy: "\n") - let importlines = lines.filter {$0.trimmingCharacters(in: .whitespaces).hasPrefix(String.import)} + let importlines = lines.filter {$0.trimmingCharacters(in: .whitespaces).hasPrefix(String.importSpace)} return importlines } diff --git a/Sources/MockoloFramework/Utils/StringExtensions.swift b/Sources/MockoloFramework/Utils/StringExtensions.swift index 1d531fdf..480f20d0 100644 --- a/Sources/MockoloFramework/Utils/StringExtensions.swift +++ b/Sources/MockoloFramework/Utils/StringExtensions.swift @@ -39,12 +39,13 @@ extension String { static let doneInit = "_doneInit" static let hasBlankInit = "_hasBlankInit" static let `static` = "static" - static let `import` = "import " + static let importSpace = "import " static public let `class` = "class" static public let `final` = "final" static let override = "override" static let mockType = "protocol" static let unknownVal = "Unknown" + static let name = "name" static let any = "Any" static let anyObject = "AnyObject" static let fatalError = "fatalError" @@ -52,29 +53,27 @@ extension String { static let `open` = "open" static let initializer = "init" static let handlerSuffix = "Handler" - static let observableVarPrefix = "Observable<" - static let rxObservableVarPrefix = "RxSwift.Observable<" - static let rxPublishSubject = "RxSwift.PublishSubject" + static let observableLeftAngleBracket = "Observable<" + static let rxObservableLeftAngleBracket = "RxSwift.Observable<" static let publishSubject = "PublishSubject" static let behaviorSubject = "BehaviorSubject" static let replaySubject = "ReplaySubject" - static let rx = "Rx" static let observableEmpty = "Observable.empty()" static let rxObservableEmpty = "RxSwift.Observable.empty()" static let `required` = "required" static let `convenience` = "convenience" static let closureArrow = "->" + static let moduleColon = "module:" static let typealiasColon = "typealias:" static let rxColon = "rx:" + static let varColon = "var:" static let `typealias` = "typealias" static let annotationArgDelimiter = ";" - static let rxReplaySubject = "RxSwift.ReplaySubject" - static let replaySubjectAllocSuffix = ".create(bufferSize: 1)" static let subjectSuffix = "Subject" static let underlyingVarPrefix = "underlying" static let setCallCountSuffix = "SetCallCount" static let callCountSuffix = "CallCount" - static let initializerPrefix = "init(" + static let initializerLeftParen = "init(" static let `escaping` = "@escaping" static let autoclosure = "@autoclosure" static public let mockAnnotation = "@mockable" @@ -110,6 +109,26 @@ extension String { self.hasSuffix("SubjectKind") || (self.hasSuffix(.handlerSuffix) && type.isOptional) } + + func arguments(with delimiter: String) -> [String: String]? { + let argstr = self + let args = argstr.components(separatedBy: delimiter) + var argsMap = [String: String]() + for item in args { + let keyVal = item.components(separatedBy: "=").map{$0.trimmingCharacters(in: .whitespaces)} + + if let k = keyVal.first { + if k.contains(":") { + break + } + + if let v = keyVal.last { + argsMap[k] = v + } + } + } + return !argsMap.isEmpty ? argsMap : nil + } } let separatorsForDisplay = CharacterSet(charactersIn: "<>[] :,()_-.&@#!{}@+\"\'") @@ -158,7 +177,7 @@ extension StringProtocol { } var moduleName: String? { - guard self.hasPrefix(String.import) else { return nil } - return self.dropFirst(String.import.count).trimmingCharacters(in: .whitespaces) + guard self.hasPrefix(String.importSpace) else { return nil } + return self.dropFirst(String.importSpace.count).trimmingCharacters(in: .whitespaces) } } diff --git a/Sources/MockoloFramework/Utils/TypeParser.swift b/Sources/MockoloFramework/Utils/TypeParser.swift index 167eaff2..deaf66ef 100644 --- a/Sources/MockoloFramework/Utils/TypeParser.swift +++ b/Sources/MockoloFramework/Utils/TypeParser.swift @@ -306,7 +306,7 @@ public struct Type { func defaultVal(with typeKeys: [String: String]? = nil, overrides: [String: String]? = nil, overrideKey: String = "", isInitParam: Bool = false) -> String? { let (subjectType, subjectVal) = parseRxVar(overrides: overrides, overrideKey: overrideKey, isInitParam: isInitParam) if subjectType != nil { - return isInitParam ? subjectVal : (typeName.hasSuffix(String.rxObservableVarPrefix) ? String.rxObservableEmpty : String.observableEmpty) + return isInitParam ? subjectVal : (typeName.hasSuffix(String.rxObservableLeftAngleBracket) ? String.rxObservableEmpty : String.observableEmpty) } if let val = parseDefaultVal(isInitParam: isInitParam, overrides: overrides, overrideKey: overrideKey) { @@ -320,8 +320,8 @@ public struct Type { } func parseRxVar(overrides: [String: String]?, overrideKey: String, isInitParam: Bool) -> (String?, String?) { - if typeName.hasPrefix(String.observableVarPrefix) || typeName.hasPrefix(String.rxObservableVarPrefix), - let range = typeName.range(of: String.observableVarPrefix), let lastIdx = typeName.lastIndex(of: ">") { + if typeName.hasPrefix(String.observableLeftAngleBracket) || typeName.hasPrefix(String.rxObservableLeftAngleBracket), + let range = typeName.range(of: String.observableLeftAngleBracket), let lastIdx = typeName.lastIndex(of: ">") { let typeParamStr = typeName[range.upperBound.. Double +} + +""" + +let moduleOverrideMock = """ + +class Foo.TaskRoutingMock: Foo.TaskRouting { + + private var _doneInit = false + + init() { _doneInit = true } + init(bar: String = "") { + self.bar = bar + _doneInit = true + } + var barSetCallCount = 0 + var bar: String = "" { didSet { barSetCallCount += 1 } } + var bazCallCount = 0 + var bazHandler: (() -> (Double))? + func baz() -> Double { + bazCallCount += 1 + if let bazHandler = bazHandler { + return bazHandler() + } + return 0.0 + } +} +""" diff --git a/Tests/TestModuleNames/ModuleNameTests.swift b/Tests/TestModuleNames/ModuleNameTests.swift new file mode 100644 index 00000000..0fa9f832 --- /dev/null +++ b/Tests/TestModuleNames/ModuleNameTests.swift @@ -0,0 +1,9 @@ +import Foundation + +class ModuleNameTests: MockoloTestCase { + + func testModuleOverride() { + verify(srcContent: moduleOverride, + dstContent: moduleOverrideMock) + } +} diff --git a/Tests/TestNonSimpleVars/NonSimpleCaseTests.swift b/Tests/TestNonSimpleVars/NonSimpleCaseTests.swift index aebb0e65..748f46a8 100644 --- a/Tests/TestNonSimpleVars/NonSimpleCaseTests.swift +++ b/Tests/TestNonSimpleVars/NonSimpleCaseTests.swift @@ -37,30 +37,4 @@ class NonSimpleVarTests: MockoloTestCase { dstContent: forArgClosureFuncMock, concurrencyLimit: nil) } - - func testRx() { - verify(srcContent: rx, - dstContent: rxMock, - concurrencyLimit: nil) - } - - func testRxMultiParents() { - verify(srcContent: rxMultiParents, - dstContent: rxMultiParentsMock, - concurrencyLimit: nil) - } - - func testRxVarInherited() { - verify(srcContent: rxVarInherited, - dstContent: rxVarInheritedMock, - concurrencyLimit: nil) - } - - func testRxVarSubjects() { - verify(srcContent: rxVarSubjects, - dstContent: rxVarSubjectsMock, - concurrencyLimit: nil) - } - - } diff --git a/Tests/TestNonSimpleVars/FixtureRxVars.swift b/Tests/TestRx/FixtureRxVars.swift similarity index 100% rename from Tests/TestNonSimpleVars/FixtureRxVars.swift rename to Tests/TestRx/FixtureRxVars.swift diff --git a/Tests/TestRx/RxVarTests.swift b/Tests/TestRx/RxVarTests.swift new file mode 100644 index 00000000..39d465ab --- /dev/null +++ b/Tests/TestRx/RxVarTests.swift @@ -0,0 +1,30 @@ +import Foundation + +class RxVarTests: MockoloTestCase { + + func testRx() { + verify(srcContent: rx, + dstContent: rxMock, + concurrencyLimit: nil) + } + + func testRxMultiParents() { + verify(srcContent: rxMultiParents, + dstContent: rxMultiParentsMock, + concurrencyLimit: nil) + } + + func testRxVarInherited() { + verify(srcContent: rxVarInherited, + dstContent: rxVarInheritedMock, + concurrencyLimit: nil) + } + + func testRxVarSubjects() { + verify(srcContent: rxVarSubjects, + dstContent: rxVarSubjectsMock, + concurrencyLimit: nil) + } + + +} diff --git a/Tests/TestTestableImportStatements/FixtureTestableImportStatements.swift b/Tests/TestTestableImports/FixtureTestableImportStatements.swift similarity index 100% rename from Tests/TestTestableImportStatements/FixtureTestableImportStatements.swift rename to Tests/TestTestableImports/FixtureTestableImportStatements.swift diff --git a/Tests/TestTestableImportStatements/TestableImportStatementsTests.swift b/Tests/TestTestableImports/TestableImportStatementsTests.swift similarity index 100% rename from Tests/TestTestableImportStatements/TestableImportStatementsTests.swift rename to Tests/TestTestableImports/TestableImportStatementsTests.swift From 75061b1dcd35523beaea2e5daf6a054bc16b8e24 Mon Sep 17 00:00:00 2001 From: Ellie Shin Date: Mon, 2 Mar 2020 16:18:31 -0800 Subject: [PATCH 2/3] updates --- Sources/MockoloFramework/Templates/ClassTemplate.swift | 2 +- Tests/TestModuleNames/FixtureModuleOverrides.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/MockoloFramework/Templates/ClassTemplate.swift b/Sources/MockoloFramework/Templates/ClassTemplate.swift index df72acf7..036c094d 100644 --- a/Sources/MockoloFramework/Templates/ClassTemplate.swift +++ b/Sources/MockoloFramework/Templates/ClassTemplate.swift @@ -67,7 +67,7 @@ func applyClassTemplate(name: String, let template = """ \(attribute) - \(accessControlLevelDescription)class \(moduleDot)\(name): \(moduleDot)\(identifier) { + \(accessControlLevelDescription)class \(name): \(moduleDot)\(identifier) { \(1.tab)\(typealiasTemplate) \(extraInits) \(renderedEntities) diff --git a/Tests/TestModuleNames/FixtureModuleOverrides.swift b/Tests/TestModuleNames/FixtureModuleOverrides.swift index bf7e840c..68c81475 100644 --- a/Tests/TestModuleNames/FixtureModuleOverrides.swift +++ b/Tests/TestModuleNames/FixtureModuleOverrides.swift @@ -12,7 +12,7 @@ protocol TaskRouting: BaseRouting { let moduleOverrideMock = """ -class Foo.TaskRoutingMock: Foo.TaskRouting { +class TaskRoutingMock: Foo.TaskRouting { private var _doneInit = false From 9389db5371a3de01b499d03072b7a43c9d281cec Mon Sep 17 00:00:00 2001 From: Ellie Shin Date: Mon, 2 Mar 2020 18:02:39 -0800 Subject: [PATCH 3/3] Rename key to prefix for module name and update readme --- README.md | 44 ++++++++++++++++++- .../Models/ParsedEntity.swift | 2 +- .../Parsers/SourceKitExtensions.swift | 2 +- .../SwiftSyntaxExtensions.swift | 2 +- .../Utils/StringExtensions.swift | 2 +- .../FixtureModuleOverrides.swift | 2 +- 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 5986cb5f..43d9650a 100644 --- a/README.md +++ b/README.md @@ -196,7 +196,26 @@ func testMock() { } ``` -An override argument, e.g. for a typealias, can be passed into the annotation (default or custom) delimited by a semi-colon. +A list of override arguments can be passed in (delimited by a semicolon) to the annotation to set var types, typealiases, module names, etc. + +For a module name: + +```swift +/// @mockable(module: prefix = Bar) +public protocol Foo { + ... +} +``` + +This will generate: + +```swift +public class FooMock: Bar.Foo { + ... +} +``` + +For a typealias: ```swift /// @mockable(typealias: T = AnyObject; U = StringProtocol) @@ -220,6 +239,29 @@ public class FooMock: Foo { ``` +For a var type such as an RxSwift observable: + +```swift +/// @mockable(rx: intStream = ReplaySubject; doubleStream = BehaviorSubject) +public protocol Foo { + var intStream: Observable { get } + var doubleStream: Observable { get } +} +``` + +This will generate: + +```swift +public class FooMock: Foo { + var intStreamSubject = ReplaySubject.create(bufferSize: 1) + var intStream: Observable { /* use intStreamSubject */ } + var doubleStreamSubject = BehaviorSubject(value: 0) + var doubleStream: Observable { /* use doubleStreamSubject */ } +} +``` + + + ## Used libraries [SwiftSyntax](https://github.com/apple/swift-syntax) | diff --git a/Sources/MockoloFramework/Models/ParsedEntity.swift b/Sources/MockoloFramework/Models/ParsedEntity.swift index 080eaf72..0a6e7776 100644 --- a/Sources/MockoloFramework/Models/ParsedEntity.swift +++ b/Sources/MockoloFramework/Models/ParsedEntity.swift @@ -101,7 +101,7 @@ final class EntityNodeSubContainer { } /// Contains arguments to annotation -/// e.g. @mockable(module: name = Foo; typealias: T = Any; U = String; rx: barStream = PublishSubject) +/// e.g. @mockable(module: prefix = Foo; typealias: T = Any; U = String; rx: barStream = PublishSubject) struct AnnotationMetadata { var module: String? var typeAliases: [String: String]? diff --git a/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift b/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift index 9273fc73..c73b9b9b 100644 --- a/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift +++ b/Sources/MockoloFramework/Parsers/SourceKitExtensions.swift @@ -49,7 +49,7 @@ extension Structure: EntityNode { } } if let val = argsMap[.moduleColon] { - ret.module = val[.name] + ret.module = val[.prefix] } } return ret diff --git a/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift b/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift index 5301e780..b18bcc03 100644 --- a/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift +++ b/Sources/MockoloFramework/Parsers/ViaSwiftSyntax/SwiftSyntaxExtensions.swift @@ -627,7 +627,7 @@ extension Trivia { } if argsStr.contains(String.moduleColon), let subStr = argsStr.components(separatedBy: String.moduleColon).last, !subStr.isEmpty { let val = subStr.arguments(with: .annotationArgDelimiter) - ret.module = val?[.name] + ret.module = val?[.prefix] } if argsStr.contains(String.rxColon), let subStr = argsStr.components(separatedBy: String.rxColon).last, !subStr.isEmpty { ret.varTypes = subStr.arguments(with: .annotationArgDelimiter) diff --git a/Sources/MockoloFramework/Utils/StringExtensions.swift b/Sources/MockoloFramework/Utils/StringExtensions.swift index 480f20d0..29e2462c 100644 --- a/Sources/MockoloFramework/Utils/StringExtensions.swift +++ b/Sources/MockoloFramework/Utils/StringExtensions.swift @@ -45,7 +45,7 @@ extension String { static let override = "override" static let mockType = "protocol" static let unknownVal = "Unknown" - static let name = "name" + static let prefix = "prefix" static let any = "Any" static let anyObject = "AnyObject" static let fatalError = "fatalError" diff --git a/Tests/TestModuleNames/FixtureModuleOverrides.swift b/Tests/TestModuleNames/FixtureModuleOverrides.swift index 68c81475..c926017d 100644 --- a/Tests/TestModuleNames/FixtureModuleOverrides.swift +++ b/Tests/TestModuleNames/FixtureModuleOverrides.swift @@ -2,7 +2,7 @@ import MockoloFramework let moduleOverride = """ -/// \(String.mockAnnotation)(module: name = Foo) +/// \(String.mockAnnotation)(module: prefix = Foo) protocol TaskRouting: BaseRouting { var bar: String { get } func baz() -> Double