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
Add generics to types #660
Add generics to types #660
Conversation
Generated by 🚫 Danger |
@truizlop looks good, could you add changelog info? |
@truizlop @krzysztofzablocki I don't feel like this is an extensible solution though. How are we going to extend it to support generic constraints, i.e.? I think we should provide full information about generic parameters like we do for variable/parameters types. And I feel it will make sence to implement it so that we can parse generic parameters of both type and methods declarations, because the syntax is the same. |
BTW SourceKit in Swift 4.1 (or 4.2 not so sure) provides some of that information |
@marcelofabri It's probably only for 4.2, I quickly checked on Swift 4.2 with SourceKitten 0.20 and there is nothing helpful |
Yeah, just double checked and it's for 4.2: struct Foo<T: Equatable> {} {
"key.diagnostic_stage" : "source.diagnostic.stage.swift.parse",
"key.length" : 27,
"key.offset" : 0,
"key.substructure" : [
{
"key.accessibility" : "source.lang.swift.accessibility.internal",
"key.bodylength" : 0,
"key.bodyoffset" : 26,
"key.kind" : "source.lang.swift.decl.struct",
"key.length" : 27,
"key.name" : "Foo",
"key.namelength" : 3,
"key.nameoffset" : 7,
"key.offset" : 0,
"key.substructure" : [
{
"key.elements" : [
{
"key.kind" : "source.lang.swift.structure.elem.typeref",
"key.length" : 9,
"key.offset" : 14
}
],
"key.inheritedtypes" : [
{
"key.name" : "Equatable"
}
],
"key.kind" : "source.lang.swift.decl.generic_type_param",
"key.length" : 12,
"key.name" : "T",
"key.namelength" : 1,
"key.nameoffset" : 11,
"key.offset" : 11
}
]
}
]
} It doesn't provide any information about constraints added using |
@ilyapuchka fair point, @truizlop would you be able to add support for that? |
Yes, I can take care of that. Is there any class that you think I can reuse for this task, or should I create a new one? I am not 100% familiar with the project, so perhaps you can advise in this regard. Also, should we include |
7b0a222
to
fc7ebe8
Compare
@@ -0,0 +1,25 @@ | |||
import Foundation | |||
|
|||
@objcMembers public final class Generic: NSObject, SourceryModel { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Undocumented symbol. |
@ilyapuchka @krzysztofzablocki @marcelofabri please take a look at the new changes I introduced. These changes are able to parse constraints on generics on types and methods, and also in where clauses. Before going any further, I'd like your opininon on it. There is still one case that I am not sure how to handle. When there is a constraint on a protocol with an associated type, how should we model it? |
var openingAngles = 1 | ||
while openingAngles > 0 && end != source.endIndex { | ||
end = source.index(after: end) | ||
if source[end] == ">" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it really possible to have nested generic blocks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if they appear on the constraints. Something like: class Foo<A, T : Bar<A>>
is syntactically correct.
declaration = String(declaration[declaration.startIndex ..< whereStart]) | ||
} | ||
|
||
if let start = source.index(of: "<") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this seems to work on class MyClass: SuperClass<Generic>
when it shouldn't, so we should check for inheritance part and drop it first.
SourceryRuntime/Sources/Class.swift
Outdated
@@ -24,7 +24,8 @@ import Foundation | |||
typealiases: [Typealias] = [], | |||
attributes: [String: Attribute] = [:], | |||
annotations: [String: NSObject] = [:], | |||
isGeneric: Bool = false) { | |||
isGeneric: Bool = false, | |||
genericTypes: [Generic] = []) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think genericTypeParameters
is a name that is more aligned with Swift terminology, same applies to type of elements, GenericTypeParameter
|
||
it("extracts generics correctly") { | ||
let result = parse(""" | ||
class Foo<A : Equatable & Codable, B : Baz<A>,C> : Bar<B> where C : CustomStringConvertible, B : Comparable & Decodable, C : CustomDebugStringConvertible { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would be interesting to add some line breaks in where
clause to make sure it's parsed correctly from multiple lines
|
||
@objcMembers public final class Generic: NSObject, SourceryModel { | ||
public let name: String | ||
public var constraints: [TypeName] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should also infer type
of the constraint, when it is known, so this should be probably
class GenericTypeParameterConstraint {
let typeName: TypeName
let type: Type?
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know how you are dealing with this in the rest of the project. Can you provide some hints on how to infer the type?
Sourcery/Parsing/FileParser.swift
Outdated
|
||
fileprivate func parseGenerics(declaration: String, whereClause: String) -> [Generic] { | ||
func extractGenerics(source: String) -> [Generic] { | ||
return source.components(separatedBy: ",") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should use commaSeparated
method, it will ensure that separators are processed correctly, otherwise things like Foo<A: [(String, String)]>
will be parsed incorrectly
Sourcery/Parsing/FileParser.swift
Outdated
if let generic = filtered.first { | ||
return generic | ||
} | ||
fatalError("Undeclared generic found: \(name)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should be Log.error()
instead
Sourcery/Parsing/FileParser.swift
Outdated
var genericTypes: [Generic] = [] | ||
if let genericSource = extract(.name, from: source), | ||
let parameterStartIndex = genericSource.index(of: "("), | ||
let whereClauseSource = extract(.nameSuffixUpToBody, from: source) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this might not work with protocols where methods don't have body. we should probably extract it along with extracting return type (above), which handles this case
|
||
/// Represents an inheritance/implementation constraint in a generic type parameter | ||
@objcMembers public final class GenericTypeParameterConstraint: NSObject, SourceryModel { | ||
public let name: TypeName |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Undocumented symbol. |
/// Represents an inheritance/implementation constraint in a generic type parameter | ||
@objcMembers public final class GenericTypeParameterConstraint: NSObject, SourceryModel { | ||
public let name: TypeName | ||
public let type: Type? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 | Undocumented symbol. |
@ilyapuchka I have addressed all the comments in your review, except for the type inference, which right now I am not sure how to do. Any guidance would be appreciated. |
@ilyapuchka @krzysztofzablocki could anyone take a look at the most recent changes? |
@truizlop I'll try to find time to look at that this weekend. For inference look at |
Hello guys! Hitting the same wall, writing templates that depend on the type of generic argument of type is currently what I want with my code generation, but cannot do because generic arguments are not exposed, only the fact that they are generic. Is there something I can do to help move this PR forward? |
I'd love to move it forward as well, as this has been sitting here for a very long time. I was waiting for a review, and I know that we need to add support for the inference of types, which was not added yet. Perhaps you can take a look at that? |
it("doesn't extract generics if they are concrete parameters to the supertype") { | ||
let result = parse("class Foo : Bar<Int> {}") | ||
let expected = Class(name: "Foo", isGeneric: false) | ||
expected.inheritedTypes = ["Bar<Int>"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ideally the generic type information should be still available in this case. In the end if Boo
is defined as Boo<Value>
, then Foo.Value
can be used in the Foo
implementation. And it should have a concrete type information too.
} | ||
} | ||
""") | ||
let expected = Class(name: "Foo", isGeneric: true, genericTypeParameters: [GenericTypeParameter(typeName: TypeName("A"), constraints: [GenericTypeParameterConstraint(name: TypeName("Equatable")), GenericTypeParameterConstraint(name: TypeName("Codable"))]), GenericTypeParameter(typeName: TypeName("B"), constraints: [GenericTypeParameterConstraint(name: TypeName("Baz<A>")), GenericTypeParameterConstraint(name: TypeName("Comparable")), GenericTypeParameterConstraint(name: TypeName("Decodable"))]), GenericTypeParameter(typeName: TypeName("C"), constraints: [GenericTypeParameterConstraint(name: TypeName("CustomStringConvertible")), GenericTypeParameterConstraint(name: TypeName("CustomDebugStringConvertible"))])]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
some code formatting is probably needed here to make it more readable
Ok, guys, so I've managed to make some progress on this task here in my fork - https://github.com/DenHeadless/Sourcery/tree/trl/add-generics-to-types Stuff that was made:
Unresolved issues
@ilyapuchka @truizlop Hope for your help in this one, should I open another PR that subsumes this one, or lets somehow pull my changes into this PR? |
Given the discussion is already here, it would be nice if you can merge your branch with mine and we'll continue here. I can try to take care of the broken tests (probably there has been some change since last time I checked it out). After that, we can decide how to move it forward. |
Small update: I was able to resolve almost all issues I mentioned in previous post. I am continuing to add tests and improving generic parsing system to be recursive, and handle all cases from simple to more complex, for example Hope to get more updates soon. |
Hello guys, sorry it took so long, generic handling turned out to be much harder task than initially anticipated. I also diverged from this PR and current code base quite a bit, so I opened a new PR to clearly view and discuss all changes - #707. Would love to hear your thoughts and feedback about it @truizlop @ilyapuchka. |
Thanks @DenHeadless, I'll take a look at your PR. |
As stated in issue #551, current type declaration only provides a boolean flag stating whether a type has a generic declaration or not. This PR parses the list of generic types associated to a type declaration as an
Array<String>
. They can be accessed during generation through the propertytype.genericTypes
. Default value (when type is not generic) is an empty array.