Sources/MockoloFramework/Operations/Generator.swift (152 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 CoreFoundation
import Foundation
enum InputError: Error {
case annotationError
case sourceFilesError
}
/// Performs end to end mock generation flow
@discardableResult
public func generate(sourceDirs: [String],
sourceFiles: [String],
parser: SourceParser,
exclusionSuffixes: [String],
mockFilePaths: [String]?,
annotation: String,
header: String?,
macro: String?,
declType: FindTargetDeclType,
useTemplateFunc: Bool,
allowSetCallCount: Bool,
enableFuncArgsHistory: Bool,
disableCombineDefaultValues: Bool,
mockFinal: Bool,
testableImports: [String],
customImports: [String],
excludeImports: [String],
to outputFilePath: String,
loggingLevel: Int,
concurrencyLimit: Int?) throws -> String {
guard sourceDirs.count > 0 || sourceFiles.count > 0 else {
log("Source files or directories do not exist", level: .error)
throw InputError.sourceFilesError
}
scanConcurrencyLimit = concurrencyLimit
minLogLevel = loggingLevel
var candidates = [(String, Int64)]()
var resolvedEntities = [ResolvedEntity]()
var parentMocks = [String: Entity]()
var protocolMap = [String: Entity]()
var annotatedProtocolMap = [String: Entity]()
var pathToImportsMap = ImportMap()
var relevantPaths = [String]()
signpost_begin(name: "Process input")
let t0 = CFAbsoluteTimeGetCurrent()
log("Process input mock files...", level: .info)
if let mockFilePaths = mockFilePaths, !mockFilePaths.isEmpty {
parser.parseProcessedDecls(mockFilePaths, fileMacro: macro) { (elements, imports) in
elements.forEach { element in
parentMocks[element.entityNode.nameText] = element
}
if let imports = imports {
for (path, importMap) in imports {
pathToImportsMap[path] = importMap
}
}
}
}
signpost_end(name: "Process input")
let t1 = CFAbsoluteTimeGetCurrent()
log("Took", t1-t0, level: .verbose)
signpost_begin(name: "Generate protocol map")
log("Process source files and generate an annotated/protocol map...", level: .info)
let paths = !sourceDirs.isEmpty ? sourceDirs : sourceFiles
let isDirs = !sourceDirs.isEmpty
parser.parseDecls(paths,
isDirs: isDirs,
exclusionSuffixes: exclusionSuffixes,
annotation: annotation,
fileMacro: macro,
declType: declType) { (elements, imports) in
elements.forEach { element in
protocolMap[element.entityNode.nameText] = element
if element.isAnnotated {
annotatedProtocolMap[element.entityNode.nameText] = element
}
}
if let imports = imports {
for (path, importMap) in imports {
pathToImportsMap[path] = importMap
}
}
}
signpost_end(name: "Generate protocol map")
let t2 = CFAbsoluteTimeGetCurrent()
log("Took", t2-t1, level: .verbose)
let typeKeyList = [
parentMocks.compactMap { (key, value) -> String? in
if value.entityNode.mayHaveGlobalActor {
return nil
}
return key.components(separatedBy: "Mock").first
},
annotatedProtocolMap.filter { !$0.value.entityNode.mayHaveGlobalActor }.map(\.key)
]
.flatMap { $0 }
.map { typeName in
// nameOverride does not work correctly but it giving up.
return (typeName, "\(typeName)Mock()")
}
SwiftType.customDefaultValueMap = [String: String](typeKeyList, uniquingKeysWith: { $1 })
signpost_begin(name: "Generate models")
log("Resolve inheritance and generate unique entity models...", level: .info)
generateUniqueModels(protocolMap: protocolMap,
annotatedProtocolMap: annotatedProtocolMap,
inheritanceMap: parentMocks,
completion: { container in
resolvedEntities.append(container.entity)
relevantPaths.append(contentsOf: container.paths)
})
signpost_end(name: "Generate models")
let t3 = CFAbsoluteTimeGetCurrent()
log("Took", t3-t2, level: .verbose)
signpost_begin(name: "Render models")
log("Render models with templates...", level: .info)
renderTemplates(
entities: resolvedEntities,
arguments: .init(
useTemplateFunc: useTemplateFunc,
allowSetCallCount: allowSetCallCount,
mockFinal: mockFinal,
enableFuncArgsHistory: enableFuncArgsHistory,
disableCombineDefaultValues: disableCombineDefaultValues
)
) { (mockString: String, offset: Int64) in
candidates.append((mockString, offset))
}
signpost_end(name: "Render models")
let t4 = CFAbsoluteTimeGetCurrent()
log("Took", t4-t3, level: .verbose)
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 + (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,
helpers: helpers,
to: outputFilePath)
signpost_end(name: "Write results")
let t5 = CFAbsoluteTimeGetCurrent()
log("Took", t5-t4, level: .verbose)
let count = result.components(separatedBy: "\n").count
log("TOTAL", t5-t0, level: .verbose)
log("#Protocols = \(protocolMap.count), #Annotated protocols = \(annotatedProtocolMap.count), #Parent mock classes = \(parentMocks.count), #Final mock classes = \(candidates.count), File LoC = \(count)", level: .verbose)
return result
}