-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
19 changed files
with
697 additions
and
31 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
Sources/SpyableMacro/Extensions/FunctionDeclSyntax+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import SwiftSyntax | ||
|
||
extension FunctionDeclSyntax { | ||
/// The name of each generic type used. Ex: the set `[T, U]` in `func foo<T, U>()`. | ||
var genericTypes: Set<String> { | ||
Set(genericParameterClause?.parameters.map { $0.name.text } ?? []) | ||
} | ||
|
||
/// If the function declaration requires being cast to a type, this will specify that type. | ||
/// Namely, this will apply to situations where generics are used in the function, and properties are consequently stored with generic types replaced with `Any`. | ||
/// | ||
/// Ex: `func foo() -> T` will create `var fooReturnValue: Any!`, which will be used in the spy method implementation as `fooReturnValue as! T` | ||
var forceCastType: TypeSyntax? { | ||
guard !genericTypes.isEmpty, | ||
let returnType = signature.returnClause?.type, | ||
returnType.containsGenericType(from: genericTypes) == true else { | ||
return nil | ||
} | ||
return returnType.trimmed | ||
} | ||
} |
136 changes: 136 additions & 0 deletions
136
Sources/SpyableMacro/Extensions/TypeSyntax+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import SwiftSyntax | ||
|
||
extension TypeSyntax { | ||
|
||
/// Returns `self`, cast to the first supported `TypeSyntaxSupportingGenerics` type that `self` can be cast to, or `nil` if `self` matches none. | ||
private var asTypeSyntaxSupportingGenerics: TypeSyntaxSupportingGenerics? { | ||
for typeSyntax in typeSyntaxesSupportingGenerics { | ||
guard let cast = self.as(typeSyntax.self) else { continue } | ||
return cast | ||
} | ||
return nil | ||
} | ||
|
||
/// An array of all of the `TypeSyntax`s that are used to compose this object. | ||
/// | ||
/// Ex: If this `TypeSyntax` represents a `TupleTypeSyntax`, `(A, B)`, this will return the two type syntaxes, `A` & `B`. | ||
private var nestedTypeSyntaxes: [Self] { | ||
// TODO: An improvement upon this could be to throw an error here, instead of falling back to an empty array. This could be ultimately used to emit a diagnostic about the unsupported TypeSyntax for a better user experience. | ||
asTypeSyntaxSupportingGenerics?.nestedTypeSyntaxes ?? [] | ||
} | ||
|
||
/// Type erases generic types by substituting their names with `Any`. | ||
/// | ||
/// Ex: If this `TypeSyntax` represents a `TupleTypeSyntax`,`(A, B)`, it will be turned into `(Any, B)` if `genericTypes` contains `"A"`. | ||
/// - Parameter genericTypes: A list of generic type names to check against. | ||
/// - Returns: This object, but with generic types names replaced with `Any`. | ||
func erasingGenericTypes(_ genericTypes: Set<String>) -> Self { | ||
guard !genericTypes.isEmpty else { return self } | ||
|
||
// TODO: An improvement upon this could be to throw an error here, instead of falling back to `self`. This could be ultimately used to emit a diagnostic about the unsupported TypeSyntax for a better user experience. | ||
return TypeSyntax(fromProtocol: asTypeSyntaxSupportingGenerics?.erasingGenericTypes(genericTypes)) ?? self | ||
} | ||
|
||
/// Recurses through type syntaxes to find all `IdentifierTypeSyntax` leaves, and checks each of them to see if its name exists in `genericTypes`. | ||
/// | ||
/// Ex: If this `TypeSyntax` represents a `TupleTypeSyntax`,`(A, B)`, it will return `true` if `genericTypes` contains `"A"`. | ||
/// - Parameter genericTypes: A list of generic type names to check against. | ||
/// - Returns: Whether or not this `TypeSyntax` contains a type matching a name in `genericTypes`. | ||
func containsGenericType(from genericTypes: Set<String>) -> Bool { | ||
guard !genericTypes.isEmpty else { return false } | ||
|
||
return if let type = self.as(IdentifierTypeSyntax.self), | ||
genericTypes.contains(type.name.text) { | ||
true | ||
} else { | ||
nestedTypeSyntaxes.contains { $0.containsGenericType(from: genericTypes) } | ||
} | ||
} | ||
} | ||
|
||
// MARK: - TypeSyntaxSupportingGenerics | ||
|
||
/// Conform type syntaxes to this protocol and add them to `typeSyntaxesSupportingGenerics` to support having their generics scanned or type-erased. | ||
/// | ||
/// - Warning: We are warned in the documentation of `TypeSyntaxProtocol`, "Do not conform to this protocol yourself". However, we don't use this protocol for anything other than defining additional behavior on particular conformers to `TypeSyntaxProtocol`; we're not using this to define a new type syntax. | ||
private protocol TypeSyntaxSupportingGenerics: TypeSyntaxProtocol { | ||
/// Type syntaxes that can be found nested within this type. | ||
/// | ||
/// Ex: A `TupleTypeSyntax` representing `(A, (B, C))` would have the two nested type syntaxes: `IdentityTypeSyntax`, which would represent `A`, and `TupleTypeSyntax` would represent `(B, C)`, which would in turn have its own `nestedTypeSyntaxes`. | ||
var nestedTypeSyntaxes: [TypeSyntax] { get } | ||
|
||
/// Returns `self` with generics replaced with `Any`, when the generic identifiers exist in `genericTypes`. | ||
func erasingGenericTypes(_ genericTypes: Set<String>) -> Self | ||
} | ||
|
||
private let typeSyntaxesSupportingGenerics: [TypeSyntaxSupportingGenerics.Type] = [ | ||
IdentifierTypeSyntax.self, // Start with IdentifierTypeSyntax for the sake of efficiency when looping through this array, as it's the most common TypeSyntax. | ||
ArrayTypeSyntax.self, | ||
GenericArgumentClauseSyntax.self, | ||
TupleTypeSyntax.self, | ||
] | ||
|
||
extension IdentifierTypeSyntax: TypeSyntaxSupportingGenerics { | ||
fileprivate var nestedTypeSyntaxes: [TypeSyntax] { | ||
genericArgumentClause?.nestedTypeSyntaxes ?? [] | ||
} | ||
fileprivate func erasingGenericTypes(_ genericTypes: Set<String>) -> Self { | ||
var copy = self | ||
if genericTypes.contains(name.text) { | ||
copy = copy.with(\.name.tokenKind, .identifier("Any")) | ||
} | ||
if let genericArgumentClause { | ||
copy = copy.with( | ||
\.genericArgumentClause, | ||
genericArgumentClause.erasingGenericTypes(genericTypes) | ||
) | ||
} | ||
return copy | ||
} | ||
} | ||
|
||
extension ArrayTypeSyntax: TypeSyntaxSupportingGenerics { | ||
fileprivate var nestedTypeSyntaxes: [TypeSyntax] { | ||
[element] | ||
} | ||
fileprivate func erasingGenericTypes(_ genericTypes: Set<String>) -> Self { | ||
with(\.element, element.erasingGenericTypes(genericTypes)) | ||
} | ||
} | ||
|
||
extension GenericArgumentClauseSyntax: TypeSyntaxSupportingGenerics { | ||
fileprivate var nestedTypeSyntaxes: [TypeSyntax] { | ||
arguments.map { $0.argument } | ||
} | ||
fileprivate func erasingGenericTypes(_ genericTypes: Set<String>) -> Self { | ||
with( | ||
\.arguments, | ||
GenericArgumentListSyntax { | ||
for argumentElement in arguments { | ||
argumentElement.with( | ||
\.argument, | ||
argumentElement.argument.erasingGenericTypes(genericTypes) | ||
) | ||
} | ||
} | ||
) | ||
} | ||
} | ||
|
||
extension TupleTypeSyntax: TypeSyntaxSupportingGenerics { | ||
fileprivate var nestedTypeSyntaxes: [TypeSyntax] { | ||
elements.map { $0.type } | ||
} | ||
fileprivate func erasingGenericTypes(_ genericTypes: Set<String>) -> Self { | ||
with( | ||
\.elements, | ||
TupleTypeElementListSyntax { | ||
for element in elements { | ||
element.with( | ||
\.type, | ||
element.type.erasingGenericTypes(genericTypes)) | ||
} | ||
} | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.