Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Generator/Sources/Internal/Crawlers/Crawler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ final class Crawler: SyntaxVisitor {
attributes: attributes(from: node.attributes),
accessibility: accessibility(from: node.modifiers) ?? (container as? HasAccessibility)?.accessibility ?? .internal,
name: node.name.filteredDescription,
genericParameters: (genericParameters(from: node.primaryAssociatedTypeClause?.primaryAssociatedTypes) + associatedTypes(from: node.memberBlock.members)).merged(),
associatedTypes: associatedTypes(from: node.memberBlock.members),
primaryAssociatedTypes: genericParameters(from: node.primaryAssociatedTypeClause?.primaryAssociatedTypes),
genericRequirements: genericRequirements(from: node.genericWhereClause?.requirements),
inheritedTypes: inheritedTypes,
members: []
Expand Down
4 changes: 2 additions & 2 deletions Generator/Sources/Internal/GeneratorHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@

let matchableWhereConstraints = method.signature.parameters.enumerated().map { index, parameter -> String in
let type = parameter.type.isOptional ? "OptionalMatchedType" : "MatchedType"
return "M\(index + 1).\(type) == \(genericSafeType(from: parameter.type.withoutAttributes(except: ["@Sendable"]).unoptionaled.description))"
return "M\(index + 1).\(type) == \(genericSafeType(from: parameter.type.withoutAttributes(except: ["@MainActor", "@Sendable"]).unoptionaled.description))"
}
let methodWhereConstraints = method.signature.whereConstraints
return " where \((matchableWhereConstraints + methodWhereConstraints).joined(separator: ", "))"
Expand All @@ -57,7 +57,7 @@
private static func parameterMatchers(for parameters: [MethodParameter]) -> String {
guard parameters.isEmpty == false else { return "let matchers: [Cuckoo.ParameterMatcher<Void>] = []" }

let tupleType = parameters.map { $0.type.withoutAttributes(except: ["@Sendable"]).description }.joined(separator: ", ")
let tupleType = parameters.map { $0.type.withoutAttributes(except: ["@MainActor", "@Sendable"]).description }.joined(separator: ", ")
let matchers = parameters
// Enumeration is done after filtering out parameters without usable names.
.enumerated()
Expand All @@ -76,7 +76,7 @@
private static func openNestedClosure(for method: Method) -> String {
var fullString = ""
for (index, parameter) in method.signature.parameters.enumerated() {
if !parameter.type.containsAttribute(named: "@escaping"), let closure = parameter.type.findClosure() {

Check warning on line 79 in Generator/Sources/Internal/GeneratorHelper.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

immutable value 'closure' was never used; consider replacing with '_' or removing it

Check warning on line 79 in Generator/Sources/Internal/GeneratorHelper.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

immutable value 'closure' was never used; consider replacing with '_' or removing it

Check warning on line 79 in Generator/Sources/Internal/GeneratorHelper.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

immutable value 'closure' was never used; consider replacing with '_' or removing it

Check warning on line 79 in Generator/Sources/Internal/GeneratorHelper.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

immutable value 'closure' was never used; consider replacing with '_' or removing it
if fullString.isEmpty {
fullString = "\n"
}
Expand Down
14 changes: 10 additions & 4 deletions Generator/Sources/Internal/Templates/MockTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,17 @@ extension Templates {
{% if container.hasParent %}
extension {{ container.parentFullyQualifiedName }} {
{% endif %}
{% if container.hasPrimaryAssociatedTypes %}
// runtime support for constrained protocols with primary associated types
@available(iOS 16, macOS 13, watchOS 9, tvOS 16, *)
{% endif %}
{{ container.accessibility|withSpace }}class {{ container.mockName }}{{ container.genericParameters }}:{% if container.isNSObjectProtocol %} NSObject,{% endif %} {{ container.name }}{% if container.isImplementation %}{{ container.genericArguments }}{% endif %},{% if container.isImplementation %} Cuckoo.ClassMock{% else %} Cuckoo.ProtocolMock{% endif %}, @unchecked Sendable {
{% if container.isGeneric and not container.isImplementation %}
{% if container.isGeneric and not container.isImplementation and not container.hasOnlyPrimaryAssociatedTypes %}
{{ container.accessibility|withSpace }}typealias MocksType = \(typeErasureClassName)
{% else %}
{% elif container.isImplementation %}
{{ container.accessibility|withSpace }}typealias MocksType = {{ container.name }}{{ container.genericArguments }}
{% else %}
{{ container.accessibility|withSpace }}typealias MocksType = any {{ container.name }}{{ container.genericArguments }}
{% endif %}
{{ container.accessibility|withSpace }}typealias Stubbing = __StubbingProxy_{{ container.name }}
{{ container.accessibility|withSpace }}typealias Verification = __VerificationProxy_{{ container.name }}
Expand All @@ -31,7 +37,7 @@ extension {{ container.parentFullyQualifiedName }} {

{{ container.accessibility|withSpace }}let cuckoo_manager = Cuckoo.MockManager.preconfiguredManager ?? Cuckoo.MockManager(hasParent: {{ container.isImplementation }})

{% if container.isGeneric and not container.isImplementation %}
{% if container.isGeneric and not container.isImplementation and not container.hasOnlyPrimaryAssociatedTypes %}
\(Templates.typeErasure.indented())

private var __defaultImplStub: \(typeErasureClassName)?
Expand All @@ -43,7 +49,7 @@ extension {{ container.parentFullyQualifiedName }} {
}

{{ container.accessibility|withSpace }}func enableDefaultImplementation<\(staticGenericParameter): {{ container.name }}>(mutating stub: UnsafeMutablePointer<\(staticGenericParameter)>) where {{ container.genericProtocolIdentity }} {
__defaultImplStub = \(typeErasureClassName)(from: stub, keeping: nil)
__defaultImplStub = \(typeErasureClassName)(from: stub, keeping: stub.pointee)
cuckoo_manager.enableDefaultStubImplementation()
}
{% else %}
Expand Down
19 changes: 16 additions & 3 deletions Generator/Sources/Internal/Templates/TypeErasureTemplate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Foundation
extension Templates {
static let typeErasure = """
{{ container.accessibility|withSpace }}class \(typeErasureClassName): {{ container.name }}, @unchecked Sendable {
private let reference: Any
private let reference: () -> any {{ container.name }}{{ container.genericPrimaryAssociatedTypeArguments }}

{% for property in container.properties %}
private let _getter_storage$${{ property.name }}: () -> {{ property.type }}
Expand All @@ -18,9 +18,9 @@ extension Templates {
}

{% endfor %}
{# For developers: The `keeping reference: Any?` is necessary because when called from the `enableDefaultImplementation(stub:)` method
{# For developers: The `keeping reference: Any` is necessary because when called from the `enableDefaultImplementation(stub:)` method
instead of `enableDefaultImplementation(mutating:)`, we need to prevent the struct getting deallocated. #}
init<\(staticGenericParameter): {{ container.name }}>(from defaultImpl: UnsafeMutablePointer<\(staticGenericParameter)>, keeping reference: @escaping @autoclosure () -> Any?) where {{ container.genericProtocolIdentity }} {
init<\(staticGenericParameter): {{ container.name }}>(from defaultImpl: UnsafeMutablePointer<\(staticGenericParameter)>, keeping reference: @escaping @autoclosure () -> \(staticGenericParameter)) where {{ container.genericProtocolIdentity }} {
self.reference = reference

{% for property in container.properties %}
Expand All @@ -30,7 +30,9 @@ extension Templates {
{% endif %}
{% endfor %}
{% for method in container.methods %}
{% if not method.hasGenericParams %}
_storage${{ forloop.counter }}${{ method.name }} = defaultImpl.pointee.{{ method.name }}
{% endif %}
{% endfor %}
}
{% if container.initializers %}
Expand All @@ -43,9 +45,20 @@ extension Templates {
{% endfor %}

{% for method in container.methods +%}
{% if not method.hasGenericParams %}
private let _storage${{ forloop.counter }}${{ method.name }}: ({{ method.inputTypes }}) {% if method.isAsync %} async{% endif %} {% if method.isThrowing %} throws{% endif %} -> {{ method.returnType }}
{% endif %}
{{ container.accessibility|withSpace }}func {{ method.name|escapeReservedKeywords }}{{ method.signature }} {
{% if method.hasGenericParams and method.hasNonPrimaryAssociatedTypeParams %}
func openExistential<\(staticGenericParameter): {{ container.name }}{{ container.genericPrimaryAssociatedTypeArguments }}>(_ opened: \(staticGenericParameter)) {% if method.isAsync %} async{% endif %} {% if method.isThrowing %} throws{% endif %} -> {{ method.returnType }} {
return {% if method.isThrowing %} try{% endif %} {% if method.isAsync %} await{% endif %} opened.{{ method.name }}{{ method.staticGenericCall }}
}
return {% if method.isThrowing %} try{% endif %} {% if method.isAsync %} await{% endif %} openExistential(reference())
{% elif method.hasGenericParams %}
return {% if method.isThrowing %} try{% endif %} {% if method.isAsync %} await{% endif %} reference().{{ method.name }}({{ method.call }})
{% else %}
return {% if method.isThrowing %} try{% endif %} {% if method.isAsync %} await{% endif %} _storage${{ forloop.counter }}${{ method.name }}({{ method.parameterNames }})
{% endif %}
}
{% endfor %}
}
Expand Down
28 changes: 26 additions & 2 deletions Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,39 @@
var isGeneric: Bool {
!genericParameters.isEmpty
}

var hasPrimaryAssociatedTypes: Bool {
guard let protocolDeclaration = asProtocol else { return false }
return !protocolDeclaration.primaryAssociatedTypes.isEmpty
}

var hasOnlyPrimaryAssociatedTypes: Bool {
guard let protocolDeclaration = asProtocol else { return false }
return protocolDeclaration.primaryAssociatedTypes.count == protocolDeclaration.genericParameters.count
}

func genericsSerialize() -> GeneratorContext {
let genericProtocolIdentity = isProtocol ? genericParameters.map { "\(Templates.staticGenericParameter).\($0.name) == \($0.name)" }.joined(separator: ", ") : nil

let genericProtocolIdentity = isProtocol
? genericParameters
.map { "\(Templates.staticGenericParameter).\($0.name) == \($0.name)" }
.joined(separator: ", ")
: nil
let genericPrimaryAssociatedTypeArguments: String?
if let protocolDeclaration = asProtocol, hasPrimaryAssociatedTypes {
let arguments = protocolDeclaration.primaryAssociatedTypes.map { $0.name }.joined(separator: ", ")
genericPrimaryAssociatedTypeArguments = "<\(arguments)>"
} else {
genericPrimaryAssociatedTypeArguments = nil
}
Comment thread
Brennanium marked this conversation as resolved.

return [
"isGeneric": isGeneric,
"genericParameters": genericParametersString,
"genericArguments": genericArgumentsString,
"hasPrimaryAssociatedTypes": hasPrimaryAssociatedTypes,
"hasOnlyPrimaryAssociatedTypes": hasOnlyPrimaryAssociatedTypes,
"genericProtocolIdentity": genericProtocolIdentity,

Check warning on line 53 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

expression implicitly coerced from 'String?' to 'Any'

Check warning on line 53 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

expression implicitly coerced from 'String?' to 'Any'

Check warning on line 53 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

expression implicitly coerced from 'String?' to 'Any'

Check warning on line 53 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

expression implicitly coerced from 'String?' to 'Any'
"genericPrimaryAssociatedTypeArguments": genericPrimaryAssociatedTypeArguments,

Check warning on line 54 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

expression implicitly coerced from 'String?' to 'Any'

Check warning on line 54 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

expression implicitly coerced from 'String?' to 'Any'

Check warning on line 54 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

expression implicitly coerced from 'String?' to 'Any'

Check warning on line 54 in Generator/Sources/Internal/Tokens/Capabilities/HasGenerics.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

expression implicitly coerced from 'String?' to 'Any'
]
.compactMapValues { $0 }
}
Expand Down
77 changes: 77 additions & 0 deletions Generator/Sources/Internal/Tokens/ComplexType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
self = .attributed(
attributes: [
attributedType.attributes.map { $0.trimmedDescription },
attributedType.specifier.map { [$0.trimmedDescription] } ?? [],

Check warning on line 23 in Generator/Sources/Internal/Tokens/ComplexType.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

'specifier' is deprecated: Access the specifiers list instead

Check warning on line 23 in Generator/Sources/Internal/Tokens/ComplexType.swift

View workflow job for this annotation

GitHub Actions / Build and Test on iOS simulator

'specifier' is deprecated: Access the specifiers list instead

Check warning on line 23 in Generator/Sources/Internal/Tokens/ComplexType.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

'specifier' is deprecated: Access the specifiers list instead

Check warning on line 23 in Generator/Sources/Internal/Tokens/ComplexType.swift

View workflow job for this annotation

GitHub Actions / Build and Test on macOS

'specifier' is deprecated: Access the specifiers list instead
].flatMap { $0 },
baseType: ComplexType(syntax: attributedType.baseType)
)
Expand Down Expand Up @@ -241,6 +241,83 @@
nil
}
}

func replaceType(named typeName: String, with replacement: String) -> ComplexType? {
switch self {
case .attributed(let attributes, let baseType):
return baseType.replaceType(named: typeName, with: replacement)
.map { ComplexType.attributed(attributes: attributes, baseType: $0) }
case .optional(let wrappedType, let isImplicit):
return wrappedType.replaceType(named: typeName, with: replacement)
.map { ComplexType.optional(wrappedType: $0, isImplicit: isImplicit) }
case .array(let elementType):
return elementType.replaceType(named: typeName, with: replacement)
.map { ComplexType.array(elementType: $0) }
case .dictionary(let keyType, let valueType):
let newKey = keyType.replaceType(named: typeName, with: replacement)
let newValue = valueType.replaceType(named: typeName, with: replacement)
if newKey == nil && newValue == nil { return nil }
return .dictionary(
keyType: newKey ?? keyType,
valueType: newValue ?? valueType
)
case .closure(let closure):
var changed = false
let newParams: [ComplexType.Closure.Parameter] = closure.parameters.map { param in
if let newType = param.type.replaceType(named: typeName, with: replacement) {
changed = true
return ComplexType.Closure.Parameter(label: param.label, type: newType)
} else {
return param
}
}
let newReturn = closure.returnType.replaceType(named: typeName, with: replacement)
if !changed && newReturn == nil { return nil }
return .closure(.init(parameters: newParams, effects: closure.effects, returnType: newReturn ?? closure.returnType))
case .type(let name):
return name == typeName ? ComplexType.type(replacement) : nil
}
}

func replaceTypes(named typeNames: [String], with replacement: (String) -> String) -> ComplexType? {
var changed = false
var type = self
for typeName in typeNames {
if let replaced = type.replaceType(named: typeName, with: replacement(typeName)) {
changed = true
type = replaced
}
}

return changed ? type : nil
}

func containsType(named typeName: String) -> Bool {
switch self {
case .attributed(_, let baseType):
baseType.containsType(named: typeName)
case .optional(let wrappedType, _):
wrappedType.containsType(named: typeName)
case .array(let elementType):
elementType.containsType(named: typeName)
case .dictionary(let keyType, let valueType):
keyType.containsType(named: typeName) || valueType.containsType(named: typeName)
case .closure(let closure):
(closure.parameters.map(\.type) + [closure.returnType]).contains(where: { $0.containsType(named: typeName)})
case .type(let name):
name == typeName
}
}

func containsTypes(named typeNames: [String]) -> Bool {
typeNames.contains(where: { containsType(named: $0) })
}
}

extension String {
func forceCast(as type: ComplexType) -> String {
"\(self) as! \(type.withoutAttributes(except: ["@MainActor", "@Sendable"]).description)"
}
}

extension ComplexType.Closure.Effects {
Expand Down
51 changes: 36 additions & 15 deletions Generator/Sources/Internal/Tokens/Method.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,24 +55,42 @@ extension Method {
var hasOptionalParams: Bool {
signature.parameters.contains { $0.type.isOptional }
}

var hasGenericParams: Bool {
!signature.genericParameters.isEmpty
}

var hasNonPrimaryAssociatedTypeParams: Bool {
guard let parent = parent?.asProtocol else { return false }
return signature.containsTypes(named: parent.nonPrimaryAssociatedTypes.map(\.name))
}

func serialize() -> [String : Any] {
let call = signature.parameters
.map { parameter in
let name = escapeReservedKeywords(for: parameter.usableName)
let value = "\(parameter.isInout ? "&" : "")\(name)\(parameter.type.containsAttribute(named: "@autoclosure") ? "()" : "")"
if parameter.name == "_" {
return value
} else {
return "\(parameter.name): \(value)"
}
}
.joined(separator: ", ")

guard let parent else {
fatalError("Failed to find parent of method \(fullSignature). Please file a bug.")
}


let call = signature.parameters
.map(\.call)
.joined(separator: ", ")

let staticGenericCall: String
if let parent = parent.asProtocol, !parent.nonPrimaryAssociatedTypes.isEmpty {
let nonPrimary = parent.nonPrimaryAssociatedTypes.map(\.name)

let staticGenericCallableParameters = signature.parameters
.map { $0.callAndCastTypes(named: nonPrimary, as: { Templates.staticGenericParameter + ".\($0)" }) }
.joined(separator: ", ")

staticGenericCall = if let returnType, returnType.containsTypes(named: nonPrimary) {
"(\(staticGenericCallableParameters))".forceCast(as: returnType)
} else {
"(\(staticGenericCallableParameters))"
}
} else {
staticGenericCall = "(\(call))"
}
Comment thread
Brennanium marked this conversation as resolved.

let stubFunctionPrefix = parent.isClass ? "Class" : "Protocol"
let returnString = returnType?.isVoid == false ? "" : "NoReturn"
let throwingString = isThrowing ? "Throwing" : ""
Expand Down Expand Up @@ -108,16 +126,19 @@ extension Method {
"throwTypeError": signature.throwType?.type ?? "",
"fullyQualifiedName": fullyQualifiedName,
"call": call,
"staticGenericCall": staticGenericCall,
"parameterSignature": signature.parameters.map { $0.description }.joined(separator: ", "),
"parameterSignatureWithoutNames": signature.parameters.map { "\($0.name): \($0.type)" }.joined(separator: ", "),
"argumentSignature": signature.parameters.map { $0.type.description }.joined(separator: ", "),
"stubFunction": stubFunction,
"inputTypes": signature.parameters.map { $0.type.withoutAttributes(except: ["@escaping", "@Sendable"]).description }.joined(separator: ", "),
"genericInputTypes": signature.parameters.map { $0.type.withoutAttributes(except: ["@Sendable"]).description }.joined(separator: ", "),
"inputTypes": signature.parameters.map { $0.type.withoutAttributes(except: ["@escaping", "@MainActor", "@Sendable"]).description }.joined(separator: ", "),
"genericInputTypes": signature.parameters.map { $0.type.withoutAttributes(except: ["@MainActor", "@Sendable"]).description }.joined(separator: ", "),
"isOptional": isOptional,
"hasClosureParams": hasClosureParams,
"hasOptionalParams": hasOptionalParams,
"hasGenericParams": hasGenericParams,
"genericParameters": signature.genericParameters.sourceDescription,
"hasNonPrimaryAssociatedTypeParams": hasNonPrimaryAssociatedTypeParams,
"hasUnavailablePlatforms": hasUnavailablePlatforms,
"unavailablePlatformsCheck": unavailablePlatformsCheck,
]
Expand Down
21 changes: 21 additions & 0 deletions Generator/Sources/Internal/Tokens/MethodParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,27 @@ struct MethodParameter: Token {
var isEscaping: Bool {
type.isClosure && (type.containsAttribute(named: "@escaping") || type.isOptional)
}

var call: String {
let escapedName = escapeReservedKeywords(for: usableName)
let value = "\(isInout ? "&" : "")\(escapedName)\(type.containsAttribute(named: "@autoclosure") ? "()" : "")"
if name == "_" {
return value
} else {
return "\(name): \(value)"
}
}

func callAndCastTypes(named typeNames: [String], as replacement: (String) -> String) -> String {
let replaced = type.replaceTypes(named: typeNames, with: replacement)

let callToCast = call
if let replaced {
return callToCast.forceCast(as: replaced)
} else {
return callToCast
}
}

func serialize() -> [String: Any] {
return [
Expand Down
11 changes: 11 additions & 0 deletions Generator/Sources/Internal/Tokens/MethodSignature.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,14 @@ extension Method.Signature {
&& whereConstraints == other.whereConstraints
}
}

extension Method.Signature {
func containsType(named typeName: String) -> Bool {
parameters.map(\.type)
.contains(where: { $0.containsType(named: typeName) })
}

func containsTypes(named typeNames: [String]) -> Bool {
typeNames.contains(where: { containsType(named: $0) })
}
}
Loading
Loading