Sources/MockoloFramework/Utils/InheritanceResolver.swift (79 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 Algorithms
/// 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, cumulated inherited types, and a map of filepaths and file contents (used for import lines lookup later).
func lookupEntities(key: String,
declKind: NominalTypeDeclKind,
protocolMap: [String: Entity],
inheritanceMap: [String: Entity]) -> ([Model], [Model], [String], Set<String>, [String]) {
// 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 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, declKind: declKind, path: current.filepath, isProcessed: current.isProcessed)
models.append(contentsOf: sub.members)
if !current.isProcessed {
attributes.append(contentsOf: sub.attributes)
}
inheritedTypes.formUnion(current.entityNode.inheritedTypes)
paths.append(current.filepath)
if declKind == .protocol { // 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, parentInheritedTypes, parentPaths) = lookupEntities(key: parent, declKind: declKind, protocolMap: protocolMap, inheritanceMap: inheritanceMap)
models.append(contentsOf: parentModels)
processedModels.append(contentsOf: parentProcessedModels)
attributes.append(contentsOf: parentAttributes)
inheritedTypes.formUnion(parentInheritedTypes)
paths.append(contentsOf: parentPaths)
}
}
}
} else if let parentMock = inheritanceMap["\(key)Mock"], declKind == .protocol {
// 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, declKind: declKind, path: parentMock.filepath, isProcessed: parentMock.isProcessed)
processedModels.append(contentsOf: sub.members)
if !parentMock.isProcessed {
attributes.append(contentsOf: sub.attributes)
}
paths.append(parentMock.filepath)
}
return (models, processedModels, attributes, inheritedTypes, paths)
}
/// 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]()
for (key, models) in group {
if let 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: { $1 })
} else {
// Check if full name has been looked up
let (unvisited, visited) = models.partitioned(by: { fullNameVisited.contains($0.fullName) })
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: { $1 })
}
}
}
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)
}