Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix associatedtype generics #1345

Merged
merged 14 commits into from
Jun 19, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -246,16 +246,37 @@ class SyntaxTreeCollector: SyntaxVisitor {
let name = node.name.text.trimmed
var typeName: TypeName?
var type: Type?

if let possibleTypeName = node.inheritanceClause?.inheritedTypes.description.trimmed {
var genericRequirements: [GenericRequirement] = []
if let genericWhereClause = node.genericWhereClause {
genericRequirements = genericWhereClause.requirements.compactMap { requirement in
if let sameType = requirement.requirement.as(SameTypeRequirementSyntax.self) {
return GenericRequirement(sameType)
} else if let conformanceType = requirement.requirement.as(ConformanceRequirementSyntax.self) {
return GenericRequirement(conformanceType)
}
return nil
}
}
if let composition = processPossibleProtocolComposition(for: possibleTypeName, localName: "") {
type = composition
} else {
type = Protocol(name: possibleTypeName, genericRequirements: genericRequirements)
}
typeName = TypeName(possibleTypeName)
} else if let possibleTypeName = (node.initializer?.value as? TypeSyntax)?.description.trimmed {
type = processPossibleProtocolComposition(for: possibleTypeName, localName: "")
typeName = TypeName(possibleTypeName)
} else {
type = Type(name: "Any")
typeName = TypeName(name: "Any", actualTypeName: TypeName(name: "Any"))
}

sourceryProtocol.associatedTypes[name] = AssociatedType(name: name, typeName: typeName, type: type)
return .skipChildren
}


public override func visit(_ node: OperatorDeclSyntax) -> SyntaxVisitorContinueKind {
return .skipChildren
}
Expand Down
12 changes: 9 additions & 3 deletions SourceryRuntime/Sources/Common/AST/Actor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@ import Foundation
@objc(SwiftActor) @objcMembers
#endif
public final class Actor: Type {

// sourcery: skipJSExport
public class var kind: String { return "actor" }

/// Returns "actor"
public override var kind: String { return "actor" }
public override var kind: String { Self.kind }

/// Whether type is final
public var isFinal: Bool {
Expand Down Expand Up @@ -36,7 +40,8 @@ public final class Actor: Type {
annotations: [String: NSObject] = [:],
documentation: [String] = [],
isGeneric: Bool = false,
implements: [String: Type] = [:]) {
implements: [String: Type] = [:],
kind: String = Actor.kind) {
super.init(
name: name,
parent: parent,
Expand All @@ -54,7 +59,8 @@ public final class Actor: Type {
annotations: annotations,
documentation: documentation,
isGeneric: isGeneric,
implements: implements
implements: implements,
kind: kind
)
}

Expand Down
11 changes: 8 additions & 3 deletions SourceryRuntime/Sources/Common/AST/Class.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import Foundation
@objc(SwiftClass) @objcMembers
#endif
public final class Class: Type {
// sourcery: skipJSExport
public class var kind: String { return "class" }

/// Returns "class"
public override var kind: String { return "class" }
public override var kind: String { Self.kind }

/// Whether type is final
public var isFinal: Bool {
Expand All @@ -30,7 +33,8 @@ public final class Class: Type {
annotations: [String: NSObject] = [:],
documentation: [String] = [],
isGeneric: Bool = false,
implements: [String: Type] = [:]) {
implements: [String: Type] = [:],
kind: String = Class.kind) {
super.init(
name: name,
parent: parent,
Expand All @@ -48,7 +52,8 @@ public final class Class: Type {
annotations: annotations,
documentation: documentation,
isGeneric: isGeneric,
implements: implements
implements: implements,
kind: kind
)
}

Expand Down
16 changes: 11 additions & 5 deletions SourceryRuntime/Sources/Common/AST/Protocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@

import Foundation

#if canImport(ObjectiveC)

/// :nodoc:
public typealias SourceryProtocol = Protocol

/// Describes Swift protocol
#if canImport(ObjectiveC)
@objcMembers
#endif
public final class Protocol: Type {

// sourcery: skipJSExport
public class var kind: String { return "protocol" }

/// Returns "protocol"
public override var kind: String { return "protocol" }
public override var kind: String { Self.kind }

/// list of all declared associated types with their names as keys
public var associatedTypes: [String: AssociatedType] {
Expand Down Expand Up @@ -52,7 +55,8 @@ public final class Protocol: Type {
modifiers: [SourceryModifier] = [],
annotations: [String: NSObject] = [:],
documentation: [String] = [],
implements: [String: Type] = [:]) {
implements: [String: Type] = [:],
kind: String = Protocol.kind) {
self.associatedTypes = associatedTypes
super.init(
name: name,
Expand All @@ -71,7 +75,8 @@ public final class Protocol: Type {
annotations: annotations,
documentation: documentation,
isGeneric: !associatedTypes.isEmpty || !genericRequirements.isEmpty,
implements: implements
implements: implements,
kind: kind
)
}

Expand Down Expand Up @@ -132,3 +137,4 @@ public final class Protocol: Type {
}
// sourcery:end
}
#endif
11 changes: 8 additions & 3 deletions SourceryRuntime/Sources/Common/AST/ProtocolComposition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import Foundation
#endif
public final class ProtocolComposition: Type {

// sourcery: skipJSExport
public class var kind: String { return "protocolComposition" }

/// Returns "protocolComposition"
public override var kind: String { return "protocolComposition" }
public override var kind: String { Self.kind }

/// The names of the types composed to form this composition
public let composedTypeNames: [TypeName]
Expand All @@ -35,7 +38,8 @@ public final class ProtocolComposition: Type {
isGeneric: Bool = false,
composedTypeNames: [TypeName] = [],
composedTypes: [Type]? = nil,
implements: [String: Type] = [:]) {
implements: [String: Type] = [:],
kind: String = ProtocolComposition.kind) {
self.composedTypeNames = composedTypeNames
self.composedTypes = composedTypes
super.init(
Expand All @@ -51,7 +55,8 @@ public final class ProtocolComposition: Type {
typealiases: typealiases,
annotations: annotations,
isGeneric: isGeneric,
implements: implements
implements: implements,
kind: kind
)
}

Expand Down
11 changes: 8 additions & 3 deletions SourceryRuntime/Sources/Common/AST/Struct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ import Foundation
#endif
public final class Struct: Type {

// sourcery: skipJSExport
public class var kind: String { return "struct" }

/// Returns "struct"
public override var kind: String { return "struct" }
public override var kind: String { Self.kind }

/// :nodoc:
public override init(name: String = "",
Expand All @@ -35,7 +38,8 @@ public final class Struct: Type {
annotations: [String: NSObject] = [:],
documentation: [String] = [],
isGeneric: Bool = false,
implements: [String: Type] = [:]) {
implements: [String: Type] = [:],
kind: String = Struct.kind) {
super.init(
name: name,
parent: parent,
Expand All @@ -53,7 +57,8 @@ public final class Struct: Type {
annotations: annotations,
documentation: documentation,
isGeneric: isGeneric,
implements: implements
implements: implements,
kind: kind
)
}

Expand Down
12 changes: 7 additions & 5 deletions SourceryRuntime/Sources/Common/Composer/Composer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ public enum Composer {
let composed = ParserResultsComposed(parserResult: parserResult)

let resolveType = { (typeName: TypeName, containingType: Type?) -> Type? in
return composed.resolveType(typeName: typeName, containingType: containingType)
composed.resolveType(typeName: typeName, containingType: containingType)
}

let methodResolveType = { (typeName: TypeName, containingType: Type?, method: Method) -> Type? in
return composed.resolveType(typeName: typeName, containingType: containingType, method: method)
composed.resolveType(typeName: typeName, containingType: containingType, method: method)
}

let processType = { (type: Type) in
Expand All @@ -52,7 +52,7 @@ public enum Composer {
}

if let sourceryProtocol = type as? SourceryProtocol {
resolveProtocolTypes(sourceryProtocol, resolve: resolveType)
resolveAssociatedTypes(sourceryProtocol, resolve: resolveType)
}
}

Expand Down Expand Up @@ -180,11 +180,13 @@ public enum Composer {
protocolComposition.composedTypes = composedTypes
}

private static func resolveProtocolTypes(_ sourceryProtocol: SourceryProtocol, resolve: TypeResolver) {
private static func resolveAssociatedTypes(_ sourceryProtocol: SourceryProtocol, resolve: TypeResolver) {
sourceryProtocol.associatedTypes.forEach { (_, value) in
guard let typeName = value.typeName,
let type = resolve(typeName, sourceryProtocol)
else { return }
else {
return
}
value.type = type
}

Expand Down
54 changes: 43 additions & 11 deletions SourceryRuntime/Sources/Common/Composer/ParserResultsComposed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ internal struct ParserResultsComposed {
let parsedTypes: [Type]
let functions: [SourceryMethod]
let resolvedTypealiases: [String: Typealias]
let associatedTypes: [String: AssociatedType]
let unresolvedTypealiases: [String: Typealias]

init(parserResult: FileParserResult) {
Expand All @@ -23,6 +24,7 @@ internal struct ParserResultsComposed {
let aliases = Self.typealiases(parserResult)
resolvedTypealiases = aliases.resolved
unresolvedTypealiases = aliases.unresolved
associatedTypes = Self.extractAssociatedTypes(parserResult)
parsedTypes = parserResult.types

// set definedInType for all methods and variables
Expand Down Expand Up @@ -51,6 +53,11 @@ internal struct ParserResultsComposed {
alias.type = resolveType(typeName: alias.typeName, containingType: alias.parent)
}

/// Map associated types
associatedTypes.forEach {
typeMap[$0.key] = $0.value.type
}
Comment on lines +56 to +59
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@art-divin i'm digging into a regression that I have run into and I want to confirm if this change is the culprit or not...

I have the following example:

protocol FooValue {}

protocol Foo {
    associatedtype Bar: FooValue
    var value: Bar { get }
}

class Baz {
    struct Bar {
    }

    let bar: Bar

    init(bar: Bar) {
        self.bar = bar
    }
}

For the Bar associated type inside the Foo protocol, this change causes typeMap["Bar"] to be set to FooValue.

When I dump the parsed AST for Baz, I get expected values from the type of the stored variable, but I get the wrong type from the initialiser parameter:

(lldb) po result.first!.storedVariables.first!.typeName!.name
"Baz.Bar"
(lldb) po result.first!.initializers.first!.parameters.last!.type!.name
"FooValue"

I have a hunch that the issue comes in two parts:

  1. The initializer parameter type isn't getting the global name assigned to its typeName the same way that the store variable does:
    (lldb) po result.first!.storedVariables.first!.typeName
    Baz.Bar
    (lldb) po result.first!.initializers.first!.parameters.last!.typeName
    Bar
    
  2. Associated types are being treated as globals which is a potential cause for conflict

I can try to dig further, or put this into an issue, but I wanted to check to see if this is something that you're actively aware of atm?

Thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @liamnichols ,

amazing work! Very nice of you sharing this 🤝

I have a hunch that the issue comes in two parts:

Sounds like it, there were and still are a number of features related to associated types which are not supported at all.
Previously they were completely ignored when mocks were being generated during to missing metadata.

I can try to dig further, or put this into an issue, but I wanted to check to see if this is something that you're actively aware of atm?

If you can, sure, that would be awesome -> creating an issue, digging further... Sounds like a plan!
To answer your question, I am not aware of this regression and I really appreciate the code sample of things not working as expected in your comment here.

If you would be able to at least provide "Expected" and "Actual" sections in your issue, say, if you cannot fix this at your convenience,
it would play a crucial role in speeding up the investigation and the fix in future. 🙏


types = unifyTypes()
}

Expand Down Expand Up @@ -96,7 +103,7 @@ internal struct ParserResultsComposed {
resolveExtensionOfNestedType(type)
}

if let resolved = resolveGlobalName(for: oldName, containingType: type.parent, unique: typeMap, modules: modules, typealiases: resolvedTypealiases)?.name {
if let resolved = resolveGlobalName(for: oldName, containingType: type.parent, unique: typeMap, modules: modules, typealiases: resolvedTypealiases, associatedTypes: associatedTypes)?.name {
var moduleName: String = ""
if let module = type.module {
moduleName = "\(module)."
Expand All @@ -116,7 +123,7 @@ internal struct ParserResultsComposed {
// extend all types with their extensions
parsedTypes.forEach { type in
let inheritedTypes: [[String]] = type.inheritedTypes.compactMap { inheritedName in
if let resolvedGlobalName = resolveGlobalName(for: inheritedName, containingType: type.parent, unique: typeMap, modules: modules, typealiases: resolvedTypealiases)?.name {
if let resolvedGlobalName = resolveGlobalName(for: inheritedName, containingType: type.parent, unique: typeMap, modules: modules, typealiases: resolvedTypealiases, associatedTypes: associatedTypes)?.name {
return [resolvedGlobalName]
}
if let baseType = Composer.findBaseType(for: type, name: inheritedName, typesByName: typeMap) {
Expand Down Expand Up @@ -175,6 +182,14 @@ internal struct ParserResultsComposed {
})
}

// extract associated types from all types and add them to types
private static func extractAssociatedTypes(_ parserResult: FileParserResult) -> [String: AssociatedType] {
parserResult.types
.compactMap { $0 as? SourceryProtocol }
.map { $0.associatedTypes }
.flatMap { $0 }.reduce(into: [:]) { $0[$1.key] = $1.value }
}

/// returns typealiases map to their full names, with `resolved` removing intermediate
/// typealises and `unresolved` including typealiases that reference other typealiases.
private static func typealiases(_ parserResult: FileParserResult) -> (resolved: [String: Typealias], unresolved: [String: Typealias]) {
Expand Down Expand Up @@ -207,11 +222,14 @@ internal struct ParserResultsComposed {
}

/// Resolves type identifier for name
func resolveGlobalName(for type: String,
containingType: Type? = nil,
unique: [String: Type]? = nil,
modules: [String: [String: Type]],
typealiases: [String: Typealias]) -> (name: String, typealias: Typealias?)? {
func resolveGlobalName(
for type: String,
containingType: Type? = nil,
unique: [String: Type]? = nil,
modules: [String: [String: Type]],
typealiases: [String: Typealias],
associatedTypes: [String: AssociatedType]
) -> (name: String, typealias: Typealias?)? {
// if the type exists for this name and isn't an extension just return it's name
// if it's extension we need to check if there aren't other options TODO: verify
if let realType = unique?[type], realType.isExtension == false {
Expand All @@ -222,6 +240,13 @@ internal struct ParserResultsComposed {
return (name: alias.type?.globalName ?? alias.typeName.name, typealias: alias)
}

if let associatedType = associatedTypes[type],
let actualType = associatedType.type
{
let typeName = associatedType.typeName ?? TypeName(name: actualType.name)
return (name: actualType.globalName, typealias: Typealias(aliasName: type, typeName: typeName))
}

if let containingType = containingType {
if type == "Self" {
return (name: containingType.globalName, typealias: nil)
Expand All @@ -231,7 +256,7 @@ internal struct ParserResultsComposed {
while currentContainer != nil, let parentName = currentContainer?.globalName {
/// TODO: no parent for sure?
/// manually walk the containment tree
if let name = resolveGlobalName(for: "\(parentName).\(type)", containingType: nil, unique: unique, modules: modules, typealiases: typealiases) {
if let name = resolveGlobalName(for: "\(parentName).\(type)", containingType: nil, unique: unique, modules: modules, typealiases: typealiases, associatedTypes: associatedTypes) {
return name
}

Expand Down Expand Up @@ -565,20 +590,27 @@ internal struct ParserResultsComposed {
}
}

return unique[resolvedIdentifier]
if let associatedType = associatedTypes[resolvedIdentifier] {
return associatedType.type
}

return unique[resolvedIdentifier] ?? unique[typeName.name]
}

private func actualTypeName(for typeName: TypeName,
containingType: Type? = nil) -> TypeName? {
containingType: Type? = nil) -> TypeName? {
let unique = typeMap
let typealiases = resolvedTypealiases
let associatedTypes = associatedTypes

var unwrapped = typeName.unwrappedTypeName
if let generic = typeName.generic {
unwrapped = generic.name
} else if let type = associatedTypes[unwrapped] {
unwrapped = type.name
}

guard let aliased = resolveGlobalName(for: unwrapped, containingType: containingType, unique: unique, modules: modules, typealiases: typealiases) else {
guard let aliased = resolveGlobalName(for: unwrapped, containingType: containingType, unique: unique, modules: modules, typealiases: typealiases, associatedTypes: associatedTypes) else {
return nil
}

Expand Down
Loading
Loading