Sources/MockoloFramework/Utils/InheritanceResolver.swift (89 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
/// Used to resolve inheritance, uniquify duplicate entities, and compute potential init params.
/// Resolves inheritance by looking up the given protocol map and inheritance map
/// @param key The entity name to look up
/// @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).
func lookupEntities(key: String,
declType: DeclType,
protocolMap: [String: Entity],
inheritanceMap: [String: Entity]) -> ([Model], [Model], [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 filepaths and contents used for imports
var pathToContents = [(String, Data, Int64)]()
// Gather filepaths used for imports
var paths = [String]()
// Look up the mock entities of a protocol specified by the name.
if let current = protocolMap[key] {
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)
}
if let data = current.data {
pathToContents.append((current.filepath, data, current.entityNode.offset))
}
paths.append(current.filepath)
if declType == .protocolType { // TODO: remove this once parent protocol (current decl = classtype) handling is resolved.
// 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)
models.append(contentsOf: parentModels)
processedModels.append(contentsOf: parentProcessedModels)
attributes.append(contentsOf: parentAttributes)
paths.append(contentsOf: parentPaths)
pathToContents.append(contentsOf:parentPathToContents)
}
}
}
} 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(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)
}
if let data = parentMock.data {
pathToContents.append((parentMock.filepath, data, parentMock.entityNode.offset))
}
paths.append(parentMock.filepath)
}
return (models, processedModels, attributes, paths, pathToContents)
}
/// Uniquify multiple entities with the same name, e.g. func signature, using the verbosity level
/// @param group The dictionary containing entity name and corresponding models
/// @param level The verbosiy level used for uniquing entity names
/// @param nameByLevelVisited Used to look up whether an entity name has already been used and thus needs
/// to be differentiated
/// @param fullNameVisited Used to look up an entity full name to detect true duplicates (e.g.
/// overloaded functions in multiple parent protocols)
/// @returns a dictionary with unique entity names and corresponding models
private func uniquifyDuplicates(group: [String: [Model]],
level: Int,
nameByLevelVisited: [String: Model]?,
fullNameVisited: [String]) -> [String: Model] {
var bufferNameByLevelVisited = [String: Model]()
var bufferFullNameVisited = [String]()
group.forEach { (key: String, models: [Model]) in
if let nameByLevelVisited = nameByLevelVisited, nameByLevelVisited[key] != nil {
// An entity with the given key already exists, so look up a more verbose name for these entities
let subgroup = Dictionary(grouping: models, by: { (modelElement: Model) -> String in
return modelElement.name(by: level + 1)
})
if !fullNameVisited.isEmpty {
bufferFullNameVisited.append(contentsOf: fullNameVisited)
}
let subresult = uniquifyDuplicates(group: subgroup, level: level+1, nameByLevelVisited: bufferNameByLevelVisited, fullNameVisited: bufferFullNameVisited)
bufferNameByLevelVisited.merge(subresult, uniquingKeysWith: { (bufferElement: Model, subresultElement: Model) -> Model in
return subresultElement
})
} else {
// Check if full name has been looked up
let visited = models.filter {fullNameVisited.contains($0.fullName)}.compactMap{$0}
let unvisited = models.filter {!fullNameVisited.contains($0.fullName)}.compactMap{$0}
if let first = unvisited.first {
// If not, add it to the fullname map to keep track of duplicates
if !visited.isEmpty {
bufferFullNameVisited.append(contentsOf: visited.map{$0.fullName})
}
bufferFullNameVisited.append(first.fullName)
// There could be multiple entities with the same name key; add the first one to
// a buffer and use a more verbose name key for the rest to differentiate them
bufferNameByLevelVisited[key] = first
let nextModels = unvisited[1...]
let subgroup = Dictionary(grouping: nextModels, by: { (modelElement: Model) -> String in
let distinctName = modelElement.name(by: level + 1)
return distinctName
})
let subresult = uniquifyDuplicates(group: subgroup, level: level+1, nameByLevelVisited: bufferNameByLevelVisited, fullNameVisited: bufferFullNameVisited)
bufferNameByLevelVisited.merge(subresult, uniquingKeysWith: { (bufferElement: Model, addedElement: Model) -> Model in
return addedElement
})
}
}
}
return bufferNameByLevelVisited
}
/// Uniquify multiple entities with the same name
/// @param models The entity models that possibly contain duplciates
/// @param exclude The models that are used for lookup only
/// @param fullnames Used to look up full identifiers
/// @returns A map of unique models
func uniqueEntities(`in` models: [Model], exclude: [String: Model], fullnames: [String]) -> [String: Model] {
return uniquifyDuplicates(group: Dictionary(grouping: models) { $0.name(by: 0) }, level: 0, nameByLevelVisited: exclude, fullNameVisited: fullnames)
}