Skip to content

Commit

Permalink
Support implicitly unwrapped optionals for AutoStubbable
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmya committed Aug 1, 2023
1 parent e262fd3 commit bf43c75
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 104 deletions.
6 changes: 6 additions & 0 deletions Sources/MockDeclarations/MockModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,9 @@ struct MockModelWithProtocolProperty {
struct MockModelWithClosure {
let doSomething: (() -> Void)
}

// sourcery: AutoStubbable
struct MockModelWithImplicitlyUnwrappedOptional {
var property: Int
var implicitlyUnwrappedProperty: Int!
}
102 changes: 0 additions & 102 deletions Sources/Templates/AutoStubbable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,105 +16,3 @@ enum AutoStubbable {
return lines.joined(separator: .newLine)
}
}

extension Type {
func generateStub(types: Types) -> [String] {
var lines: [String] = []
lines.append("\(accessLevel) extension \(name) {")

let initMethodLines = initMethods.enumerated().map { index, method in
var lines: [String] = []
lines.append("static func \(stubMethodName(index: index, count: initMethods.count))(".addingIndent())
let methodParameterLines = method.parameters.map { parameter in
"\(parameter.argumentLabel ?? parameter.name): \(parameter.typeName.generateStubbableName(type: parameter.type)) = \(parameter.typeName.generateDefaultValue(type: parameter.type, includeComplexType: true, types: types))".addingIndent(count: 2)
}
let joinedMethodParameterLines = methodParameterLines.joined(separator: ",\n")
lines.append(joinedMethodParameterLines)
lines.append(") -> \(name)\(method.isFailableInitializer ? "?" : "") {".addingIndent())
lines.append("\(name)(".addingIndent(count: 2))
let methodAssignmentLines = method.parameters.map { parameter in
"\(parameter.argumentLabel ?? parameter.name): \(parameter.argumentLabel ?? parameter.name)".addingIndent(count: 3)
}
let joinedMethodAssignmentLines = methodAssignmentLines.joined(separator: ",\n")
lines.append(joinedMethodAssignmentLines)
lines.append(")".addingIndent(count: 2))
lines.append("}".addingIndent())
return lines
}
lines.append(contentsOf: initMethodLines.joined(separator: [.emptyLine]))

if initMethods.isEmpty {
lines.append("static func \(stubMethodName(index: 0, count: 1))(".addingIndent())
let availableVariables = storedVariables.filter { !$0.hasDefaultValue }
let variableLines = availableVariables.map { variable in
"\(variable.name): \(variable.typeName.generateStubbableName(type: variable.type)) = \(variable.typeName.generateDefaultValue(type: variable.type, includeComplexType: true, types: types))".addingIndent(count: 2)
}
let joinedVariableLines = variableLines.joined(separator: ",\n")
lines.append(joinedVariableLines)
lines.append(") -> \(name) {".addingIndent())
lines.append("\(name)(".addingIndent(count: 2))
let variableAssignmentLines = availableVariables.map { variable in
"\(variable.name): \(variable.name)".addingIndent(count: 3)
}
let joinedVariableAssignmentLines = variableAssignmentLines.joined(separator: ",\n")
lines.append(joinedVariableAssignmentLines)
lines.append(")".addingIndent(count: 2))
lines.append("}".addingIndent())
}
lines.append("}")
return lines
}
}

// TODO: Move and improve this stuff
func stubMethodName(index: Int, count: Int) -> String {
count > 1 ? "stub\(index)" : "stub"
}

func generateStubbableInit(objectType: Type, variables: [Variable]) -> String {
guard !variables.isEmpty else {
return "\(objectType.name)()"
}

let initVariables = variables.filter { !$0.isImplicitlyUnwrappedOptional && !$0.hasDefaultValue }

return generateStubbableInit(objectType: objectType, parameterNames: initVariables.map { $0.name })
}

func generateStubbableInit(objectType: Type, parameterNames: [String]) -> String {
let implicitlyUnwrappedVariables = objectType.storedVariables.filter { $0.isImplicitlyUnwrappedOptional }
let containsImplicitlyUnwrappedOptionals = !implicitlyUnwrappedVariables.isEmpty

guard !parameterNames.isEmpty || containsImplicitlyUnwrappedOptionals else {
return "\(objectType.name)()"
}

var objectInit: String = containsImplicitlyUnwrappedOptionals ? "let object = \(objectType.name)(" : "\(objectType.name)("

if parameterNames.isEmpty {
objectInit.append(")")
} else {
parameterNames.enumerated().forEach { index, element in
let isLastElement = index + 1 == parameterNames.count
let suffix: String = isLastElement ? "" : ","
let line = "\(element): \(element)\(suffix)".addingIndent(count: 3)
objectInit.append(contentsOf: "\n\(line)")

if isLastElement {
objectInit.append(contentsOf: "\n")
objectInit.append(contentsOf: ")".addingIndent(count: 2))
}
}
}

if containsImplicitlyUnwrappedOptionals {
for variable in implicitlyUnwrappedVariables {
objectInit.append(contentsOf: "\n")
objectInit.append(contentsOf: "object.\(variable.name) = \(variable.name)".addingIndent(count: 2))
}
objectInit.append(contentsOf: "\n")
objectInit.append(contentsOf: "return object".addingIndent(count: 2))
}

return objectInit
}
4 changes: 4 additions & 0 deletions Sources/Templates/Extensions/MethodParameter+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ extension MethodParameter {
}
return type
}

func generateInitAssignment(types: Types) -> String {
"\(argumentLabel ?? name): \(typeName.generateStubbableName(type: type)) = \(typeName.generateDefaultValue(type: type, includeComplexType: true, types: types))"
}
}
94 changes: 94 additions & 0 deletions Sources/Templates/Extensions/Type+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,98 @@ extension Type {
var initMethods: [Method] {
methods.filter { ($0.isInitializer || $0.isFailableInitializer) && !$0.isConvenienceInitializer }
}

func generateStub(types: Types) -> [String] {
var lines: [String] = []
lines.append("\(accessLevel) extension \(name) {")

let initMethodLines = initMethods.enumerated().map { index, method in
generateInitMethodStub(types: types, index: index, method: method)
}
lines.append(contentsOf: initMethodLines.joined(separator: [.emptyLine]))

// If we have an explicit init method the compiler won't synthesize a memberwise initialiser.
if initMethods.isEmpty {
lines.append(contentsOf: generateMemberwiseInitMethodStub(types: types))
}
lines.append("}")
return lines
}
}

private extension Type {
/// Generate a stub for an initialiser that's generated by default for `structs` that don't have explicit initialisers.
/// The init is generated based on the order of the properties of the struct.
func generateMemberwiseInitMethodStub(types: Types) -> [String] {
var lines: [String] = []
lines.append("static func stub(".addingIndent())
let availableVariables = storedVariables.filter { !$0.hasDefaultValue }
let variableLines = availableVariables.map { variable in
variable.generateInitAssignment(types: types).addingIndent(count: 2)
}
let joinedVariableLines = variableLines.joined(separator: ",\n")
lines.append(joinedVariableLines)
lines.append(") -> \(name) {".addingIndent())
lines.append("\(name)(".addingIndent(count: 2))
let variableAssignmentLines = availableVariables.map { variable in
"\(variable.name): \(variable.name)".addingIndent(count: 3)
}
let joinedVariableAssignmentLines = variableAssignmentLines.joined(separator: ",\n")
lines.append(joinedVariableAssignmentLines)
lines.append(")".addingIndent(count: 2))
lines.append("}".addingIndent())
return lines
}

/// Generate a stub for an explicit init method. If a type has multiple init methods the stub method will be suffixed with a number.
func generateInitMethodStub(types: Types, index: Int, method: Method) -> [String] {
let implicitlyUnwrappedVariables = storedVariables.filter { $0.isImplicitlyUnwrappedOptional }
var lines: [String] = []
lines.append("static func \(stubMethodName(index: index, count: initMethods.count))(".addingIndent())
var initParameters: [String] = method.parameters.map { $0.generateInitAssignment(types: types).addingIndent(count: 2) }
initParameters.append(contentsOf: implicitlyUnwrappedVariables.map { $0.generateInitAssignment(types: types).addingIndent(count: 2) })
lines.append(initParameters.joined(separator: ",\n"))
lines.append(") -> \(name)\(method.isFailableInitializer ? "?" : "") {".addingIndent())
let parameterNames = method.parameters.map { parameter in
parameter.argumentLabel ?? parameter.name
}
lines.append(generateStubbableInit(parameterNames: parameterNames))
lines.append("}".addingIndent())
return lines
}

func generateStubbableInit(parameterNames: [String]) -> String {
let implicitlyUnwrappedVariables = storedVariables.filter { $0.isImplicitlyUnwrappedOptional }
let containsImplicitlyUnwrappedOptionals = !implicitlyUnwrappedVariables.isEmpty

guard !parameterNames.isEmpty || containsImplicitlyUnwrappedOptionals else {
return "\(name)()"
}

var lines: [String] = []
let variableDeclaration = self is Struct ? "var" : "let"
var objectInit: String = containsImplicitlyUnwrappedOptionals ? "\(variableDeclaration) object = \(name)(" : "\(name)("
if parameterNames.isEmpty {
objectInit.append(")")
}
lines.append(objectInit.addingIndent(count: 2))

let parameterLines = parameterNames.map { "\($0): \($0)".addingIndent(count: 3) }
lines.append(parameterLines.joined(separator: "," + .newLine))
if !parameterLines.isEmpty {
lines.append(")".addingIndent(count: 2))
}

if containsImplicitlyUnwrappedOptionals {
let implicitUnwrappedVariableLines = implicitlyUnwrappedVariables.map { "object.\($0.name) = \($0.name)".addingIndent(count: 2) }
lines.append(implicitUnwrappedVariableLines.joined(separator: .newLine))
lines.append("return object".addingIndent(count: 2))
}

return lines.joined(separator: .newLine)
}

func stubMethodName(index: Int, count: Int) -> String {
count > 1 ? "stub\(index)" : "stub"
}
}
4 changes: 4 additions & 0 deletions Sources/Templates/Extensions/Variable+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ extension Variable {
func generateMock(types: Types) -> String {
isMutable ? generateMutableMock(types: types) : generateComputedMock(types: types)
}

func generateInitAssignment(types: Types) -> String {
"\(name): \(typeName.generateStubbableName(type: type)) = \(typeName.generateDefaultValue(type: type, includeComplexType: true, types: types))"
}
}

private extension Variable {
Expand Down
19 changes: 17 additions & 2 deletions Tests/TemplateTests/AutoStubbable.generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,28 @@ internal extension MockModelWithClosure {
}
}

internal extension MockModelWithImplicitlyUnwrappedOptional {
static func stub(
property: Int = 0,
implicitlyUnwrappedProperty: Int = 0
) -> MockModelWithImplicitlyUnwrappedOptional {
MockModelWithImplicitlyUnwrappedOptional(
property: property,
implicitlyUnwrappedProperty: implicitlyUnwrappedProperty
)
}
}

internal extension MockModelWithInitMethodAndImplicitlyUnwrappedOptionalDeclaration {
static func stub(
property: Int = 0
property: Int = 0,
implicitlyUnwrappedProperty: Int = 0
) -> MockModelWithInitMethodAndImplicitlyUnwrappedOptionalDeclaration {
MockModelWithInitMethodAndImplicitlyUnwrappedOptionalDeclaration(
var object = MockModelWithInitMethodAndImplicitlyUnwrappedOptionalDeclaration(
property: property
)
object.implicitlyUnwrappedProperty = implicitlyUnwrappedProperty
return object
}
}

Expand Down

0 comments on commit bf43c75

Please sign in to comment.