Cuckoo/Generator/Source/CuckooGeneratorFramework/Generator.swift

362 lines
15 KiB
Swift

//
// Generator.swift
// CuckooGenerator
//
// Created by Tadeas Kriz on 13/01/16.
// Copyright © 2016 Brightify. All rights reserved.
//
public struct Generator {
private let declarations: [Token]
private let code = CodeBuilder()
public init(file: FileRepresentation) {
declarations = file.declarations
}
public func generate() -> String {
code.clear()
declarations.forEach { generate(for: $0) }
return code.code
}
private func generate(for token: Token) {
switch token {
case let containerToken as ContainerToken:
generateClass(for: containerToken)
generateNoImplStubClass(for: containerToken)
case let property as InstanceVariable:
generateProperty(for: property)
case let method as Method:
generateMethod(for: method)
default:
break
}
}
private func generateClass(for token: ContainerToken) {
guard token.accessibility != .Private else { return }
code += ""
code += "\(token.accessibility.sourceName)class \(mockClassName(of: token.name)): \(token.name), Cuckoo.Mock {"
code.nest {
code += "\(token.accessibility.sourceName)typealias MocksType = \(token.name)"
code += "\(token.accessibility.sourceName)typealias Stubbing = \(stubbingProxyName(of: token.name))"
code += "\(token.accessibility.sourceName)typealias Verification = \(verificationProxyName(of: token.name))"
code += "\(token.accessibility.sourceName)let manager = Cuckoo.MockManager()"
code += ""
code += "private var observed: \(token.name)?"
code += ""
code += "\(token.accessibility.sourceName)func spy(on victim: \(token.name)) -> Self {"
code.nest {
code += "observed = victim"
code += "return self"
}
code += "}"
token.children.forEach { generate(for: $0) }
code += ""
generateStubbing(for: token)
code += ""
generateVerification(for: token)
}
code += "}"
}
private func generateProperty(for token: InstanceVariable) {
guard token.accessibility != .Private else { return }
code += ""
code += "\(token.accessibility.sourceName)\(token.overriding ? "override " : "")var \(token.name): \(token.type) {"
code.nest {
code += "get {"
code.nest("return manager.getter(\"\(token.name)\", original: observed.map { o in return { () -> \(token.type) in o.\(token.name) } })")
code += "}"
if token.readOnly == false {
code += "set {"
code.nest("manager.setter(\"\(token.name)\", value: newValue, original: observed != nil ? { self.observed?.\(token.name) = $0 } : nil)")
code += "}"
}
}
code += "}"
}
private func generateMethod(for token: Method) {
guard token.accessibility != .Private else { return }
guard !token.isInit else { return }
let override = token is ClassMethod ? "override " : ""
let parametersSignature = token.parameters.enumerated().map { "\($1.labelAndName): \($1.type)" }.joined(separator: ", ")
let parametersSignatureWithoutNames = token.parameters.map { "\($0.name): \($0.type)" }.joined(separator: ", ")
var managerCall: String
let tryIfThrowing: String
if token.isThrowing {
managerCall = "try manager.callThrows(\"\(token.fullyQualifiedName)\""
tryIfThrowing = "try "
} else {
managerCall = "manager.call(\"\(token.fullyQualifiedName)\""
tryIfThrowing = ""
}
managerCall += ", parameters: (\(token.parameters.map { $0.name }.joined(separator: ", ")))"
let methodCall = token.parameters.enumerated().map {
if let label = $1.label {
return "\(label): \($1.name)"
} else {
return $1.name
}
}.joined(separator: ", ")
managerCall += ", original: observed.map { o in return { (\(parametersSignatureWithoutNames))\(token.returnSignature) in \(tryIfThrowing)o.\(token.rawName)(\(methodCall)) } })"
code += ""
code += "\(token.accessibility.sourceName)\(override)\(token.isInit ? "" : "func " )\(token.rawName)(\(parametersSignature))\(token.returnSignature) {"
code.nest("return \(managerCall)")
code += "}"
}
private func generateStubbing(for token: Token) {
switch token {
case let containerToken as ContainerToken:
generateStubbingClass(for: containerToken)
case let property as InstanceVariable:
generateStubbingProperty(for: property)
case let method as Method:
generateStubbingMethod(for: method)
default:
break
}
}
private func generateStubbingClass(for token: ContainerToken) {
guard token.accessibility != .Private else { return }
code += "\(token.accessibility.sourceName)struct \(stubbingProxyName(of: token.name)): Cuckoo.StubbingProxy {"
code.nest {
code += "private let manager: Cuckoo.MockManager"
code += ""
code += "\(token.accessibility.sourceName)init(manager: Cuckoo.MockManager) {"
code.nest("self.manager = manager")
code += "}"
token.children.forEach { generateStubbing(for: $0) }
}
code += "}"
}
private func generateStubbingProperty(for token: InstanceVariable) {
guard token.accessibility != .Private else { return }
let propertyType = token.readOnly ? "Cuckoo.ToBeStubbedReadOnlyProperty" : "Cuckoo.ToBeStubbedProperty"
code += ""
code += "var \(token.name): \(propertyType)<\(genericSafeType(from: token.type))> {"
code.nest("return \(propertyType)(manager: manager, name: \"\(token.name)\")")
code += "}"
}
private func generateStubbingMethod(for token: Method) {
guard token.accessibility != .Private else { return }
guard !token.isInit else { return }
let stubFunction: String
if token.isThrowing {
if token.returnType == "Void" {
stubFunction = "Cuckoo.StubNoReturnThrowingFunction"
} else {
stubFunction = "Cuckoo.StubThrowingFunction"
}
} else {
if token.returnType == "Void" {
stubFunction = "Cuckoo.StubNoReturnFunction"
} else {
stubFunction = "Cuckoo.StubFunction"
}
}
let inputTypes = token.parameters.map { $0.typeWithoutAttributes }.joined(separator: ", ")
var returnType = "\(stubFunction)<(\(genericSafeType(from: inputTypes)))"
if token.returnType != "Void" {
returnType += ", "
returnType += genericSafeType(from: token.returnType)
}
returnType += ">"
code += ""
code += ("\(token.accessibility.sourceName)func \(token.rawName)\(matchableGenerics(with: token.parameters))" +
"(\(matchableParameterSignature(with: token.parameters))) -> \(returnType)\(matchableGenerics(where: token.parameters)) {")
let matchers: String
if token.parameters.isEmpty {
matchers = "[]"
} else {
code.nest(parameterMatchers(for: token.parameters))
matchers = "matchers"
}
code.nest("return \(stubFunction)(stub: manager.createStub(\"\(token.fullyQualifiedName)\", parameterMatchers: \(matchers)))")
code += "}"
}
private func generateVerification(for token: Token) {
switch token {
case let containerToken as ContainerToken:
generateVerificationClass(for: containerToken)
case let property as InstanceVariable:
generateVerificationProperty(for: property)
case let method as Method:
generateVerificationMethod(for: method)
default:
break
}
}
private func generateVerificationClass(for token: ContainerToken) {
guard token.accessibility != .Private else { return }
code += "\(token.accessibility.sourceName)struct \(verificationProxyName(of: token.name)): Cuckoo.VerificationProxy {"
code.nest {
code += "private let manager: Cuckoo.MockManager"
code += "private let callMatcher: Cuckoo.CallMatcher"
code += "private let sourceLocation: Cuckoo.SourceLocation"
code += ""
code += "\(token.accessibility.sourceName)init(manager: Cuckoo.MockManager, callMatcher: Cuckoo.CallMatcher, sourceLocation: Cuckoo.SourceLocation) {"
code.nest {
code += "self.manager = manager"
code += "self.callMatcher = callMatcher"
code += "self.sourceLocation = sourceLocation"
}
code += "}"
token.children.forEach { generateVerification(for: $0) }
}
code += "}"
}
private func generateVerificationProperty(for token: InstanceVariable) {
guard token.accessibility != .Private else { return }
let propertyType = token.readOnly ? "Cuckoo.VerifyReadOnlyProperty" : "Cuckoo.VerifyProperty"
code += ""
code += "var \(token.name): \(propertyType)<\(genericSafeType(from: token.type))> {"
code.nest("return \(propertyType)(manager: manager, name: \"\(token.name)\", callMatcher: callMatcher, sourceLocation: sourceLocation)")
code += "}"
}
private func generateVerificationMethod(for token: Method) {
guard token.accessibility != .Private else { return }
guard !token.isInit else { return }
code += ""
code += "@discardableResult"
code += ("\(token.accessibility.sourceName)func \(token.rawName)\(matchableGenerics(with: token.parameters))" +
"(\(matchableParameterSignature(with: token.parameters))) -> Cuckoo.__DoNotUse<\(genericSafeType(from: token.returnType))>\(matchableGenerics(where: token.parameters)) {")
let matchers: String
if token.parameters.isEmpty {
matchers = "[] as [Cuckoo.ParameterMatcher<Void>]"
} else {
code.nest(parameterMatchers(for: token.parameters))
matchers = "matchers"
}
code.nest("return manager.verify(\"\(token.fullyQualifiedName)\", callMatcher: callMatcher, parameterMatchers: \(matchers), sourceLocation: sourceLocation)")
code += "}"
}
private func generateNoImplStubClass(for token: ContainerToken) {
guard token.accessibility != .Private else { return }
code += ""
code += "\(token.accessibility.sourceName)class \(stubClassName(of: token.name)): \(token.name) {"
code.nest {
token.children.forEach { generateNoImplStub(for: $0) }
}
code += "}"
}
private func generateNoImplStub(for token: Token) {
switch token {
case let property as InstanceVariable:
generateNoImplStubProperty(for: property)
case let method as Method:
generateNoImplStubMethod(for: method)
default:
break
}
}
private func generateNoImplStubProperty(for token: InstanceVariable) {
guard token.accessibility != .Private else { return }
code += ""
code += "\(token.accessibility.sourceName)\(token.overriding ? "override " : "")var \(token.name): \(token.type) {"
code.nest {
code += "get {"
code.nest("return DefaultValueRegistry.defaultValue(for: (\(token.type)).self)")
code += "}"
if token.readOnly == false {
code += "set {"
code += "}"
}
}
code += "}"
}
private func generateNoImplStubMethod(for token: Method) {
guard token.accessibility != .Private else { return }
guard !token.isInit else { return }
let override = token is ClassMethod ? "override " : ""
let parametersSignature = token.parameters.enumerated().map { "\($1.labelAndName): \($1.type)" }.joined(separator: ", ")
code += ""
code += "\(token.accessibility.sourceName)\(override)func \(token.rawName)(\(parametersSignature))\(token.returnSignature) {"
code.nest("return DefaultValueRegistry.defaultValue(for: (\(token.returnType)).self)")
code += "}"
}
private func mockClassName(of originalName: String) -> String {
return "Mock" + originalName
}
private func stubClassName(of originalName: String) -> String {
return originalName + "Stub"
}
private func stubbingProxyName(of originalName: String) -> String {
return "__StubbingProxy_" + originalName
}
private func verificationProxyName(of originalName: String) -> String {
return "__VerificationProxy_" + originalName
}
private func matchableGenerics(with parameters: [MethodParameter]) -> String {
guard parameters.isEmpty == false else { return "" }
let genericParameters = (1...parameters.count).map { "M\($0): Cuckoo.Matchable" }.joined(separator: ", ")
return "<\(genericParameters)>"
}
private func matchableGenerics(where parameters: [MethodParameter]) -> String {
guard parameters.isEmpty == false else { return "" }
let whereClause = parameters.enumerated().map { "M\($0 + 1).MatchedType == \(genericSafeType(from: $1.typeWithoutAttributes))" }.joined(separator: ", ")
return " where \(whereClause)"
}
private func matchableParameterSignature(with parameters: [MethodParameter]) -> String {
guard parameters.isEmpty == false else { return "" }
return parameters.enumerated().map { "\($1.labelAndName): M\($0 + 1)" }.joined(separator: ", ")
}
private func parameterMatchers(for parameters: [MethodParameter]) -> String {
guard parameters.isEmpty == false else { return "" }
let tupleType = parameters.map { $0.typeWithoutAttributes }.joined(separator: ", ")
let matchers = parameters.enumerated().map { "wrap(matchable: \($1.name)) { $0\(parameters.count > 1 ? ".\($0)" : "") }" }.joined(separator: ", ")
return "let matchers: [Cuckoo.ParameterMatcher<(\(genericSafeType(from: tupleType)))>] = [\(matchers)]"
}
private func genericSafeType(from type: String) -> String {
return type.replacingOccurrences(of: "!", with: "?")
}
}