diff --git a/Sources/MacroToolkit/NormalizedType.swift b/Sources/MacroToolkit/NormalizedType.swift new file mode 100644 index 0000000..81b8d70 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedType.swift @@ -0,0 +1,128 @@ +import SwiftSyntax +import SwiftSyntaxBuilder + +public enum NormalizedType: TypeProtocol, SyntaxExpressibleByStringInterpolation { + /// A composition of two types (e.g. `Encodable & Decodable`). Used to + /// combine protocol requirements. + case composition(NormalizedCompositionType) + /// A some or any protocol type (e.g. `any T` or `some T`). + case someOrAny(NormalizedSomeOrAnyType) + /// A function type (e.g. `() -> ()`). + case function(NormalizedFunctionType) + /// An implicitly unwrapped optional type (e.g. `Int!`). + case implicitlyUnwrappedOptional(NormalizedImplicitlyUnwrappedOptionalType) + /// A member type (e.g. `Array.Element`). + case member(NormalizedMemberType) + /// A placeholder for invalid types that the resilient parser ignored. + case missing(NormalizedMissingType) + /// A pack expansion type (e.g. `repeat each V`). + case packExpansion(NormalizedPackExpansionType) + /// A pack reference type (e.g. `each V`). + case packReference(NormalizedPackReferenceType) + /// A simple type (e.g. `Int` or `Box`). + case simple(NormalizedSimpleType) + /// A suppressed type in a conformance position (e.g. `~Copyable`). + case suppressed(NormalizedSuppressedType) + //// A tuple type (e.g. `(Int, String)`). + case tuple(NormalizedTupleType) + + public var _baseSyntax: TypeSyntax { + let type: any TypeProtocol = switch self { + case .composition(let type as any TypeProtocol), + .someOrAny(let type as any TypeProtocol), + .function(let type as any TypeProtocol), + .implicitlyUnwrappedOptional(let type as any TypeProtocol), + .member(let type as any TypeProtocol), + .missing(let type as any TypeProtocol), + .packExpansion(let type as any TypeProtocol), + .packReference(let type as any TypeProtocol), + .simple(let type as any TypeProtocol), + .suppressed(let type as any TypeProtocol), + .tuple(let type as any TypeProtocol): + type + } + return TypeSyntax(type._baseSyntax) + } + + public var _attributedSyntax: AttributedTypeSyntax? { + let type: any TypeProtocol = switch self { + case .composition(let type as any TypeProtocol), + .someOrAny(let type as any TypeProtocol), + .function(let type as any TypeProtocol), + .implicitlyUnwrappedOptional(let type as any TypeProtocol), + .member(let type as any TypeProtocol), + .missing(let type as any TypeProtocol), + .packExpansion(let type as any TypeProtocol), + .packReference(let type as any TypeProtocol), + .simple(let type as any TypeProtocol), + .suppressed(let type as any TypeProtocol), + .tuple(let type as any TypeProtocol): + type + } + return type._attributedSyntax + } + + /// Wrap a `TypeSyntax` (e.g. `Int?` or `MyStruct<[String]>!`). + public init(_ syntax: TypeSyntax) { + self.init(syntax, attributedSyntax: nil) + } + + public init(_ syntax: TypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + // TODO: Move this weird initializer to an internal protocol if possible + let syntax: TypeSyntaxProtocol = attributedSyntax ?? syntax + if let type = NormalizedCompositionType(syntax) { + self = .composition(type) + } else if let type = NormalizedSomeOrAnyType(syntax) { + self = .someOrAny(type) + } else if let type = NormalizedFunctionType(syntax) { + self = .function(type) + } else if let type = NormalizedImplicitlyUnwrappedOptionalType(syntax) { + self = .implicitlyUnwrappedOptional(type) + } else if let type = NormalizedMemberType(syntax) { + self = .member(type) + } else if let type = NormalizedPackExpansionType(syntax) { + self = .packExpansion(type) + } else if let type = NormalizedPackReferenceType(syntax) { + self = .packReference(type) + } else if let type = NormalizedSimpleType(syntax) { + self = .simple(type) + } else if let type = NormalizedSuppressedType(syntax) { + self = .suppressed(type) + } else if let type = NormalizedTupleType(syntax) { + self = .tuple(type) + } else { + fatalError("TODO: Implement wrappers for all types of type syntax") + } + } + + // TODO: add an optional version to all type syntax wrappers maybe? + /// Allows string interpolation syntax to be used to express type syntax. + public init(stringInterpolation: SyntaxStringInterpolation) { + self.init(TypeSyntax(stringInterpolation: stringInterpolation)) + } + + /// Gets whether the type is optional + public var isOptional: Bool { + if case .simple(let simpleType) = self { + return simpleType.name == "Optional" + } + return false + } + + // TODO: Generate type conversions with macro? + /// Attempts to get the type as a simple type. + public var asSimpleType: NormalizedSimpleType? { + switch self { + case .simple(let type): type + default: nil + } + } + + /// Attempts to get the type as a function type. + public var asFunctionType: NormalizedFunctionType? { + switch self { + case .function(let type): type + default: nil + } + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedCompositionType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedCompositionType.swift new file mode 100644 index 0000000..dabfe3b --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedCompositionType.swift @@ -0,0 +1,12 @@ +import SwiftSyntax + +/// Wraps a composition type (e.g. `ProtocolA & ProtocolB`). +public struct NormalizedCompositionType: TypeProtocol { + public var _baseSyntax: CompositionTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init(_ syntax: CompositionTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedFunctionType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedFunctionType.swift new file mode 100644 index 0000000..34e2218 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedFunctionType.swift @@ -0,0 +1,25 @@ +import SwiftSyntax + +/// Wraps a function type (e.g. `(Int, Double) -> Bool`). +public struct NormalizedFunctionType: TypeProtocol { + // TODO: Should give access to attributes such as `@escaping`. + public var _baseSyntax: FunctionTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + /// Don't supply the `attributedSyntax` parameter, use the `attributedSyntax` initializer instead. + /// It only exists because of protocol conformance. + public init(_ syntax: FunctionTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } + + /// The return type that the function type describes. + public var returnType: NormalizedType { + NormalizedType(_baseSyntax.returnClause.type) + } + + /// The types of the parameters the function type describes. + public var parameters: [NormalizedType] { + _baseSyntax.parameters.map(\.type).map(NormalizedType.init) + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedImplicitlyUnwrappedOptionalType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedImplicitlyUnwrappedOptionalType.swift new file mode 100644 index 0000000..9cc58f6 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedImplicitlyUnwrappedOptionalType.swift @@ -0,0 +1,12 @@ +import SwiftSyntax + +/// Wraps an implicitly unwrapped optional type (e.g. `Int!`). +public struct NormalizedImplicitlyUnwrappedOptionalType: TypeProtocol { + public var _baseSyntax: ImplicitlyUnwrappedOptionalTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init(_ syntax: ImplicitlyUnwrappedOptionalTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedMemberType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedMemberType.swift new file mode 100644 index 0000000..30d8b87 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedMemberType.swift @@ -0,0 +1,15 @@ +import SwiftSyntax + +/// Wraps a member type (e.g. `Array.Element`). +public struct NormalizedMemberType: TypeProtocol { + public var _baseSyntax: MemberTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init( + _ syntax: MemberTypeSyntax, + attributedSyntax: AttributedTypeSyntax? = nil + ) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedMissingType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedMissingType.swift new file mode 100644 index 0000000..d59677e --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedMissingType.swift @@ -0,0 +1,13 @@ +import SwiftSyntax + +/// Wraps a missing type (i.e. a type that was missing in the source but the resilient parser +/// has added a placeholder for). +public struct NormalizedMissingType: TypeProtocol { + public var _baseSyntax: MissingTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init(_ syntax: MissingTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedPackExpansionType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedPackExpansionType.swift new file mode 100644 index 0000000..2a9fa05 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedPackExpansionType.swift @@ -0,0 +1,12 @@ +import SwiftSyntax + +/// Wraps a pack expansion type (e.g. `repeat each V`). +public struct NormalizedPackExpansionType: TypeProtocol { + public var _baseSyntax: PackExpansionTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init(_ syntax: PackExpansionTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedPackReferenceType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedPackReferenceType.swift new file mode 100644 index 0000000..8a20dc4 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedPackReferenceType.swift @@ -0,0 +1,12 @@ +import SwiftSyntax + +/// Wraps an implicitly unwrapped optional type (e.g. `each V`). +public struct NormalizedPackReferenceType: TypeProtocol { + public var _baseSyntax: PackElementTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init(_ syntax: PackElementTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedSimpleType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedSimpleType.swift new file mode 100644 index 0000000..7b18cea --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedSimpleType.swift @@ -0,0 +1,28 @@ +import SwiftSyntax + +/// Wraps a simple type (e.g. `Result`). +public struct NormalizedSimpleType: TypeProtocol { + public var _baseSyntax: IdentifierTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init( + _ syntax: IdentifierTypeSyntax, + attributedSyntax: AttributedTypeSyntax? = nil + ) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } + + /// The base type's name (e.g. for `Array` it would be `"Array"`). + public var name: String { + _baseSyntax.name.description + } + + /// The type's generic arguments if any were supplied (e.g. for + /// `Dictionary` it would be `["Int", "String"]`). + public var genericArguments: [NormalizedType]? { + _baseSyntax.genericArgumentClause.map { clause in + clause.arguments.map(\.argument).map(NormalizedType.init) + } + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedSomeOrAnyType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedSomeOrAnyType.swift new file mode 100644 index 0000000..5d04b99 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedSomeOrAnyType.swift @@ -0,0 +1,15 @@ +import SwiftSyntax + +/// Wraps a `some` or `any` type (i.e. `any Protocol` or `some Protocol`). +public struct NormalizedSomeOrAnyType: TypeProtocol { + public var _baseSyntax: SomeOrAnyTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init( + _ syntax: SomeOrAnyTypeSyntax, + attributedSyntax: AttributedTypeSyntax? = nil + ) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedSuppressedType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedSuppressedType.swift new file mode 100644 index 0000000..84fc389 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedSuppressedType.swift @@ -0,0 +1,12 @@ +import SwiftSyntax + +/// Wraps a suppressed type from a conformance clause (e.g. `~Copyable`). +public struct NormalizedSuppressedType: TypeProtocol { + public var _baseSyntax: SuppressedTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init(_ syntax: SuppressedTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } +} diff --git a/Sources/MacroToolkit/NormalizedTypes/NormalizedTupleType.swift b/Sources/MacroToolkit/NormalizedTypes/NormalizedTupleType.swift new file mode 100644 index 0000000..c62b054 --- /dev/null +++ b/Sources/MacroToolkit/NormalizedTypes/NormalizedTupleType.swift @@ -0,0 +1,19 @@ +import SwiftSyntax + +/// Wraps a tuple type (e.g. `(Int, String)`). +public struct NormalizedTupleType: TypeProtocol { + public var _baseSyntax: TupleTypeSyntax + public var _attributedSyntax: AttributedTypeSyntax? + + public init(_ syntax: TupleTypeSyntax, attributedSyntax: AttributedTypeSyntax? = nil) { + _baseSyntax = syntax + _attributedSyntax = attributedSyntax + } + + var elements: [NormalizedType] { + // TODO: Handle labels and the possible ellipsis + _baseSyntax.elements.map { element in + NormalizedType(element.type) + } + } +} diff --git a/Sources/MacroToolkit/Type.swift b/Sources/MacroToolkit/Type.swift index d4bd394..6a2380b 100644 --- a/Sources/MacroToolkit/Type.swift +++ b/Sources/MacroToolkit/Type.swift @@ -135,25 +135,20 @@ public enum `Type`: TypeProtocol, SyntaxExpressibleByStringInterpolation { /// A normalized description of the type (e.g. for `()` this would be `Void`). public var normalizedDescription: String { - // TODO: Implement proper type normalization - // TODO: Normalize types nested within the type too (e.g. the parameter types of a function type) - if let tupleSyntax = _syntax.as(TupleTypeSyntax.self) { - if tupleSyntax.elements.count == 0 { - return "Void" - } else if tupleSyntax.elements.count == 1, let element = tupleSyntax.elements.first { - // TODO: Can we assume that we won't get a single-element tuple with a label (which would be invalid anyway)? - return element.type.withoutTrivia().description - } else { - return _syntax.withoutTrivia().description - } - } else { - return _syntax.withoutTrivia().description - } + normalized()._syntax.withoutTrivia().description } /// Gets whether the type is a void type (i.e. `Void`, `()`, `(Void)`, `((((()))))`, etc.). public var isVoid: Bool { - normalizedDescription == "Void" + normalizedDescription == "\(Void.self)" + } + + /// Gets whether the type is optional + public var isOptional: Bool { + if case .simple(let normalizedSimpleType) = self.normalized() { + return normalizedSimpleType.name == "Optional" + } + return false } // TODO: Generate type conversions with macro? @@ -174,6 +169,206 @@ public enum `Type`: TypeProtocol, SyntaxExpressibleByStringInterpolation { } // TODO: Implement rest of conversions + + public func normalized() -> NormalizedType { + switch self { + case .array(let type): + var arrayTypeSyntax: ArrayTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + let normalizedElement = Type(arrayTypeSyntax.element).normalized() + arrayTypeSyntax.element = TypeSyntax(normalizedElement._syntax) + attributedTypeSyntax?.baseType = TypeSyntax(arrayTypeSyntax) + + var base = "Array<\(arrayTypeSyntax.element)>" + if let attributedTypeSyntax { + base = base.addingAttributes(from: attributedTypeSyntax) + } + return NormalizedType(stringLiteral: base) + case .classRestriction(let type): + // Not handling `_attributedSyntax` because `classRestriction` cannot have any attribute + let normalizedType: NormalizedType = .simple(.init(.init( + leadingTrivia: type._baseSyntax.leadingTrivia, + name: .identifier("AnyObject"), + trailingTrivia: type._baseSyntax.trailingTrivia + ))) + + return normalizedType + case .composition(let type): + var compositionTypeSyntax: CompositionTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + + let arrayOfCompositionElements = compositionTypeSyntax.elements.map { compositionElement in + let normalizedType = Type(compositionElement.type).normalized() + let updatedElementType = TypeSyntax(normalizedType._syntax) + var newCompositionElement = compositionElement + newCompositionElement.type = updatedElementType + return newCompositionElement + } + + compositionTypeSyntax.elements = .init(arrayOfCompositionElements) + attributedTypeSyntax?.baseType = TypeSyntax(compositionTypeSyntax) + + return .composition(.init(compositionTypeSyntax, attributedSyntax: attributedTypeSyntax)) + case .someOrAny(let type): + var someOrAnyTypeSyntax: SomeOrAnyTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + + let normalizedConstraint = Type(someOrAnyTypeSyntax.constraint).normalized() + someOrAnyTypeSyntax.constraint = TypeSyntax(normalizedConstraint._syntax) + attributedTypeSyntax?.baseType = TypeSyntax(someOrAnyTypeSyntax) + + return .someOrAny(.init(someOrAnyTypeSyntax, attributedSyntax: attributedTypeSyntax)) + case .dictionary(let type): + var dictionaryTypeSyntax: DictionaryTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + let normalizedKey = Type(dictionaryTypeSyntax.key).normalized() + let normalizedValue = Type(dictionaryTypeSyntax.value).normalized() + dictionaryTypeSyntax.key = TypeSyntax(normalizedKey._syntax) + dictionaryTypeSyntax.value = TypeSyntax(normalizedValue._syntax) + attributedTypeSyntax?.baseType = TypeSyntax(dictionaryTypeSyntax) + + var base = "Dictionary<\(dictionaryTypeSyntax.key), \(dictionaryTypeSyntax.value)>" + if let attributedTypeSyntax { + base = base.addingAttributes(from: attributedTypeSyntax) + } + return NormalizedType(stringLiteral: base) + case .function(let type): + var functionTypeSyntax: FunctionTypeSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = nil + if let attributedSyntax = type._attributedSyntax { + functionTypeSyntax = attributedSyntax.baseType.cast(FunctionTypeSyntax.self) + attributedTypeSyntax = attributedSyntax + } else { + functionTypeSyntax = type._baseSyntax + } + let normalizedReturnClause = Type(functionTypeSyntax.returnClause.type).normalized() + let arrayOfTupleElements = functionTypeSyntax.parameters.map { tupleElement in + let normalizedType = Type(tupleElement.type).normalized() + let updatedElementType = TypeSyntax(normalizedType._syntax) + var newTupleElement = tupleElement + newTupleElement.type = updatedElementType + return newTupleElement + } + functionTypeSyntax.parameters = .init(arrayOfTupleElements) + + functionTypeSyntax.returnClause.type = TypeSyntax(normalizedReturnClause._syntax) + attributedTypeSyntax?.baseType = TypeSyntax(functionTypeSyntax) + + return .function(.init(functionTypeSyntax, attributedSyntax: attributedTypeSyntax)) + case .implicitlyUnwrappedOptional(let type): + var implicitlyUnwrappedOptionalTypeSyntax: ImplicitlyUnwrappedOptionalTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + + let normalizedConstraint = Type(implicitlyUnwrappedOptionalTypeSyntax.wrappedType).normalized() + implicitlyUnwrappedOptionalTypeSyntax.wrappedType = TypeSyntax(normalizedConstraint._syntax) + attributedTypeSyntax?.baseType = TypeSyntax(implicitlyUnwrappedOptionalTypeSyntax) + + return .implicitlyUnwrappedOptional(.init(implicitlyUnwrappedOptionalTypeSyntax, attributedSyntax: attributedTypeSyntax)) + case .member(let type): + var memberTypeSyntax: MemberTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + let normalizedBaseType = Type(type._baseSyntax.baseType).normalized() + + memberTypeSyntax.genericArgumentClause = memberTypeSyntax.genericArgumentClause?.normalized() + + memberTypeSyntax.baseType = TypeSyntax(normalizedBaseType._syntax) + attributedTypeSyntax?.baseType = TypeSyntax(memberTypeSyntax) + + return .member(.init(memberTypeSyntax, attributedSyntax: attributedTypeSyntax)) + case .metatype(let type): + let baseType = type._baseSyntax + let memberTypeSyntax = MemberTypeSyntax.init( + leadingTrivia: baseType.leadingTrivia, + baseType: baseType.baseType, + name: baseType.metatypeSpecifier, + trailingTrivia: baseType.trailingTrivia + ) + if var attributedSyntax = type._attributedSyntax { + attributedSyntax.baseType = TypeSyntax(memberTypeSyntax) + return .member(.init(memberTypeSyntax, attributedSyntax: attributedSyntax)) + } else { + return .member(.init(memberTypeSyntax)) + } + case .missing(let type): + return .missing(.init(type._baseSyntax, attributedSyntax: type._attributedSyntax)) + case .optional(let type): + let optionalTypeSyntax: OptionalTypeSyntax = type._baseSyntax + let attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + let normalizedElement = Type(optionalTypeSyntax.wrappedType).normalized() + + let identifierSyntax = IdentifierTypeSyntax( + leadingTrivia: optionalTypeSyntax.leadingTrivia, + name: .identifier("Optional"), + genericArgumentClause: .init( + arguments: .init(arrayLiteral: .init(argument: TypeSyntax(normalizedElement._syntax))) + ), + trailingTrivia: optionalTypeSyntax.leadingTrivia + ) + + if let attributedTypeSyntax { + let normalizedAttributedTypeSyntax = identifierSyntax.addingAttributes(from: attributedTypeSyntax) + return .simple(.init(identifierSyntax, attributedSyntax: normalizedAttributedTypeSyntax)) + } else { + return .simple(.init(identifierSyntax)) + } + case .packExpansion(let type): + // Looks like there can only be simple identifiers in pack expansions, with no generics, and therefore we + // don't ned to recursively normalize + + return .packExpansion(.init(type._baseSyntax, attributedSyntax: type._attributedSyntax)) + case .packReference(let type): + // Looks like there can only be simple identifiers in pack references, with no generics, and therefore we + // don't ned to recursively normalize + + return .packReference(.init(type._baseSyntax, attributedSyntax: type._attributedSyntax)) + case .simple(let type): + if type.name == "Void" { + return .tuple(.init(.init(elements: []))) + } + var identifierTypeSyntax: IdentifierTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + + identifierTypeSyntax.genericArgumentClause = identifierTypeSyntax.genericArgumentClause?.normalized() + + attributedTypeSyntax?.baseType = TypeSyntax(identifierTypeSyntax) + + return .simple(.init(identifierTypeSyntax, attributedSyntax: attributedTypeSyntax)) + case .suppressed(let type): + // Normalizing recursively because it may be needed when https://github.com/apple/swift/issues/62906 + // is fixed. Not handling `_attributedSyntax` because seems like `suppressed` cannot have any attribute + var suppressedTypeSyntax: SuppressedTypeSyntax = type._baseSyntax + + let normalizedConstraint = Type(suppressedTypeSyntax.type).normalized() + suppressedTypeSyntax.type = TypeSyntax(normalizedConstraint._syntax) + + return .suppressed(.init(suppressedTypeSyntax)) + case .tuple(let type): + if type.elements.count == 1 { + let child = type.elements[0] + switch child { + case .tuple, .simple: + return child.normalized() + default: + break + } + } + + var tupleTypeSyntax: TupleTypeSyntax = type._baseSyntax + var attributedTypeSyntax: AttributedTypeSyntax? = type._attributedSyntax + + let arrayOfTupleElements = tupleTypeSyntax.elements.map { tupleElement in + let normalizedType = Type(tupleElement.type).normalized() + let updatedElementType = TypeSyntax(normalizedType._syntax) + var newTupleElement = tupleElement + newTupleElement.type = updatedElementType + return newTupleElement + } + tupleTypeSyntax.elements = .init(arrayOfTupleElements) + + attributedTypeSyntax?.baseType = TypeSyntax(tupleTypeSyntax) + return .tuple(.init(tupleTypeSyntax, attributedSyntax: attributedTypeSyntax)) + } + } } extension Type? { @@ -186,3 +381,58 @@ extension Type? { } } } + +// MARK: Utilities for normalization + +fileprivate extension String { + /// Builds a `String` that can then be used for interpolation attaching the attributes and + /// the specifiers of the attributed type syntax. + /// - Parameter attributedType: The `AttributedTypeSyntax` which `attributes` + /// and `specifier` should be used to prefix the string. + /// - Returns: A `String` with elements from the attributed type syntax attached to the original `String`. + func addingAttributes(from attributedType: AttributedTypeSyntax) -> String { + var updatedString = self + updatedString = "\(attributedType.attributes)\(self)" + + if let specifier = attributedType.specifier { + updatedString = "\(specifier)\(updatedString)" + } + + return updatedString + } +} + +fileprivate extension TypeSyntaxProtocol { + /// Builds a `AttributedTypeSyntax` attaching the attributes and + /// the specifiers of the attributed type syntax. + /// - Parameter attributedType: The `AttributedTypeSyntax` which `attributes` + /// and `specifier` should be attached to the `TypeSyntax`. + /// - Returns: A `AttributedTypeSyntax` with elements from the attributed type syntax attached to the original `TypeSyntax`. + func addingAttributes(from attributedType: AttributedTypeSyntax) -> AttributedTypeSyntax { + return AttributedTypeSyntax( + leadingTrivia: attributedType.leadingTrivia, + specifier: attributedType.specifier, + attributes: attributedType.attributes, + baseType: self, + trailingTrivia: attributedType.trailingTrivia + ) + } +} + +fileprivate extension GenericArgumentClauseSyntax { + /// Normalize the arguments of the generic. + /// - Returns: An updated version of the generic argument clause with + /// the arguments types normalized. + func normalized() -> Self { + var genericArgumentClause = self + let arrayOfGenericArgumentClauseArguments = genericArgumentClause.arguments.map { tupleElement in + let normalizedType = Type(tupleElement.argument).normalized() + let updatedElementType = TypeSyntax(normalizedType._syntax) + var newTupleElement = tupleElement + newTupleElement.argument = updatedElementType + return newTupleElement + } + genericArgumentClause.arguments = .init(arrayOfGenericArgumentClauseArguments) + return genericArgumentClause + } +} diff --git a/Tests/MacroToolkitTests/MacroToolkitTests.swift b/Tests/MacroToolkitTests/MacroToolkitTests.swift index 69e848e..aacc080 100644 --- a/Tests/MacroToolkitTests/MacroToolkitTests.swift +++ b/Tests/MacroToolkitTests/MacroToolkitTests.swift @@ -678,4 +678,237 @@ final class MacroToolkitTests: XCTestCase { macros: testMacros ) } + + func testNormalizedDecriptionMultipleVoid() { + let type: `Type` = "((()))" + + XCTAssertEqual(type.normalizedDescription, "()") + } + + func testIsVoid() { + let type: `Type` = "(())" + + XCTAssert(type.isVoid) + } + + func testIsOptional() { + let type1: `Type` = "Int?" + let type2: `Type` = "Optional" + let type3: `Type` = "Array" + + XCTAssert(type1.isOptional) + XCTAssert(type2.isOptional) + XCTAssertFalse(type3.isOptional) + } + + func testNormalizationVoid() { + let type: `Type` = "((()))" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "()") + } + + func testNormalizationMultipleVoidWithOptional() { + let type: `Type` = "(((((()))?)))" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "(Optional<()>)") + } + + func testNormalizationTuple() { + let type1: `Type` = "(Optional<()>)" + let type2: `Type` = "((Int) -> (Int)) -> String" + + let normalizedType1 = type1.normalized() + let normalizedType2 = type2.normalized() + + XCTAssertEqual(normalizedType1.description, "Optional<()>") + XCTAssertEqual(normalizedType2.description, "((Int) -> Int) -> String") + XCTAssertEqual(normalizedType1.description, "\(((Optional<()>)).self)") + XCTAssertEqual(normalizedType2.description, "\((((Int) -> (Int)) -> String).self)") + } + + func testNormalizationNestedArrays() { + let type: `Type` = "[[Int]]" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "Array>") + } + + func testNormalizationClassRestriction() { + let decl: DeclSyntax = """ + protocol TestProtocol: class { } + """ + + guard let inheritedTypes = decl.as(ProtocolDeclSyntax.self)?.inheritanceClause?.inheritedTypes, + let classRestrictionTypeSyntax = inheritedTypes.first?.type.as(ClassRestrictionTypeSyntax.self) else { + XCTFail("Expected class restriction in inheritance clause") + return + } + + let type: `Type` = .classRestriction(.init(classRestrictionTypeSyntax)) + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "AnyObject") + } + + func testNormalizationComposition() { + let type1: `Type` = "any Decodable & Identifiable" + let type2: `Type` = "(inout any Decodable & Identifiable) -> Void" + let type3: `Type` = "(inout Decodable & Codable) -> ([Int])" + let type4: `Type` = "(Encodable & Sequence) & Equatable" + + let normalizedType1 = type1.normalized() + let normalizedType2 = type2.normalized() + let normalizedType3 = type3.normalized() + let normalizedType4 = type4.normalized() + + XCTAssertEqual(normalizedType1.description, "any Decodable & Identifiable") + XCTAssertEqual(normalizedType2.description, "(inout any Decodable & Identifiable) -> ()") + XCTAssertEqual(normalizedType3.description, "(inout Decodable & Codable) -> (Array)") + XCTAssertEqual(normalizedType4.description, "(Encodable & Sequence>) & Equatable") + } + + func testNormalizationSomeOrAny() { + let typeCompsed: `Type` = "any Decodable & Identifiable" + let typeGeneric: `Type` = "some Sequence<[String]>" + + let normalizedTypeComposed = typeCompsed.normalized() + let normalizedTypeGeneric = typeGeneric.normalized() + + XCTAssertEqual(normalizedTypeComposed.description, "any Decodable & Identifiable") + XCTAssertEqual(normalizedTypeGeneric.description, "some Sequence>") + } + + func testNormalizationSimpleDictionary() { + let decl: DeclSyntax = """ + var items: [String: Int] + """ + + guard let variable = Decl(decl).asVariable else { + XCTFail("Expected decl to be variable") + return + } + + guard let type = variable.bindings[0].type else { + XCTFail("Expected type of variable") + return + } + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "Dictionary") + } + + func testNormalizationDictionaryWithNestedArray() { + let type: `Type` = "[String: [[Any]]]" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "Dictionary>>") + } + + func testNormalizationFunction() { + let type: `Type` = "(testParameter: [Int]) -> Void" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "(testParameter: Array) -> ()") + } + + func testNormalizationImplicitylUnwrappedOptional() { + let type1: `Type` = "MyClass<[String]>!" + let type2: `Type` = "((inout [String: Int]) -> Void)!" + + let normalizedType1 = type1.normalized() + let normalizedType2 = type2.normalized() + + XCTAssertEqual(normalizedType1.description, "MyClass>!") + XCTAssertEqual(normalizedType2.description, "((inout Dictionary) -> ())!") + } + + func testNormalizationMemeberWithGeneric() { + let type: `Type` = "(inout TestClass.TestMemberStruct<[any Hashable]>) -> ()" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "(inout TestClass.TestMemberStruct>) -> ()") + } + + func testNormalizationMetatype() { + let type: `Type` = "(inout TestClass.Type) -> Void" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "(inout TestClass.Type) -> ()") + } + + func testNormalizationOptional() { + let type: `Type` = "((inout Int?) -> Void)?" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "Optional<((inout Optional) -> ())>") + } + + func testNormalizationPackExpansion() { + let type1: `Type` = "(repeat each Elements)" + let type2: `Type` = "(keyPath: KeyPath<(repeat each Elements), Value>) -> Value" + + let normalizedType1 = type1.normalized() + let normalizedType2 = type2.normalized() + + XCTAssertEqual(normalizedType1.description, "(repeat each Elements)") + XCTAssertEqual(normalizedType2.description, "(keyPath: KeyPath<(repeat each Elements), Value>) -> Value") + } + + func testNormalizationPackReference() { + let type: `Type` = "Tuple" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "Tuple") + } + + func testNormalizationSimple() { + let type: `Type` = "MyClass<[String]>" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "MyClass>") + } + + func testNormalizationSuppressed() { + let type: `Type` = "~Copyable" + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "~Copyable") + } + + func testNormalizationAttributedTuple() { + let declSyntax: DeclSyntax = """ + let interestingType: (inout [Int], String) -> Float = { _, _ in + return 3 + } + """ + + guard let variable = Decl(declSyntax).asVariable else { + XCTFail("Expected decl to be variable") + return + } + + guard let type = variable.bindings[0].type else { + XCTFail("Expected type in bindings") + return + } + + let normalizedType = type.normalized() + + XCTAssertEqual(normalizedType.description, "(inout Array, String) -> Float") + } }