Skip to content

Commit

Permalink
Keep track of Void TypeDescription, and unwrap single-element Tuple t…
Browse files Browse the repository at this point in the history
…ypes (#42)
  • Loading branch information
dfed authored Jan 25, 2024
1 parent aa60c3a commit faeb519
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 20 deletions.
6 changes: 4 additions & 2 deletions Sources/SafeDICore/Models/Initializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,8 @@ extension TypeDescription {
.dictionary,
.tuple,
.closure,
.unknown:
.unknown,
.void:
self == argumentTypeDescription
&& argumentSpecifier == nil
&& (argumentAttributes ?? []).contains("escaping")
Expand All @@ -326,7 +327,8 @@ extension TypeDescription {
.array,
.dictionary,
.tuple,
.unknown:
.unknown,
.void:
self == argument
}
}
Expand Down
7 changes: 4 additions & 3 deletions Sources/SafeDICore/Models/Property.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
.array,
.dictionary,
.tuple,
.unknown:
.unknown,
.void:
FunctionParameterSyntax(
firstName: .identifier(label),
colon: .colonToken(trailingTrivia: .space),
Expand All @@ -123,7 +124,7 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
} else {
return .constant
}
case .any, .array, .attributed, .closure, .composition, .dictionary, .implicitlyUnwrappedOptional, .metatype, .nested, .optional, .some, .tuple, .unknown:
case .any, .array, .attributed, .closure, .composition, .dictionary, .implicitlyUnwrappedOptional, .metatype, .nested, .optional, .some, .tuple, .unknown, .void:
return .constant
}
}
Expand All @@ -133,7 +134,7 @@ public struct Property: Codable, Hashable, Comparable, Sendable {
case let .simple(_, generics),
let .nested(_, _, generics):
return generics
case .any, .array, .attributed, .closure, .composition, .dictionary, .implicitlyUnwrappedOptional, .metatype, .optional, .some, .tuple, .unknown:
case .any, .array, .attributed, .closure, .composition, .dictionary, .implicitlyUnwrappedOptional, .metatype, .optional, .some, .tuple, .unknown, .void:
return []
}
}
Expand Down
76 changes: 63 additions & 13 deletions Sources/SafeDICore/Models/TypeDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import SwiftSyntax

/// An enum that describes a parsed type in a canonical form.
public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
/// The Void or () type.
case void(VoidSpelling)
/// A root type with possible generics. e.g. Int, or Array<Int>
indirect case simple(name: String, generics: [TypeDescription])
/// A nested type with possible generics. e.g. Array.Element or Swift.Array<Element>
Expand Down Expand Up @@ -64,6 +66,8 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
/// A canonical representation of this type that can be used in source code.
public var asSource: String {
switch self {
case let .void(representation):
return representation.description
case let .simple(name, generics):
if generics.isEmpty {
return name
Expand Down Expand Up @@ -137,6 +141,32 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
lhs.asSource < rhs.asSource
}

public enum VoidSpelling: String, Codable, Hashable, Sendable, CustomStringConvertible {
/// The `()` spelling.
case tuple
/// The `Void` spelling.
case identifier

public static func == (lhs: VoidSpelling, rhs: VoidSpelling) -> Bool {
// Void is functionally equivalent no matter how it is spelled.
true
}

public func hash(into hasher: inout Hasher) {
// Void representations have an equivalent hash because they are equivalent types.
hasher.combine(0)
}

public var description: String {
switch self {
case .identifier:
"Void"
case .tuple:
"()"
}
}
}

public struct TupleElement: Codable, Hashable, Sendable {
public let label: String?
public let typeDescription: TypeDescription
Expand All @@ -156,7 +186,8 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
.simple,
.some,
.tuple,
.unknown:
.unknown,
.void:
return false
case .optional:
return true
Expand All @@ -177,7 +208,8 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
.optional,
.simple,
.some,
.tuple:
.tuple,
.void:
return false
case .unknown:
return true
Expand All @@ -204,7 +236,7 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
let .optional(typeDescription),
let .some(typeDescription):
return typeDescription.asInstantiatedType
case .array, .attributed, .closure, .composition, .dictionary, .metatype, .nested, .tuple, .unknown:
case .array, .attributed, .closure, .composition, .dictionary, .metatype, .nested, .tuple, .unknown, .void:
return self
}
}
Expand All @@ -221,9 +253,14 @@ extension TypeSyntax {
if let genericArgumentClause = typeIdentifier.genericArgumentClause {
genericTypeVisitor.walk(genericArgumentClause)
}
return .simple(
name: typeIdentifier.name.text,
generics: genericTypeVisitor.genericArguments)
if genericTypeVisitor.genericArguments.isEmpty && typeIdentifier.name.text == "Void" {
return .void(.identifier)
} else {
return .simple(
name: typeIdentifier.name.text,
generics: genericTypeVisitor.genericArguments
)
}

} else if let typeIdentifier = MemberTypeSyntax(self) {
let genericTypeVisitor = GenericArgumentVisitor(viewMode: .sourceAccurate)
Expand All @@ -233,7 +270,8 @@ extension TypeSyntax {
return .nested(
name: typeIdentifier.name.text,
parentType: typeIdentifier.baseType.typeDescription,
generics: genericTypeVisitor.genericArguments)
generics: genericTypeVisitor.genericArguments
)

} else if let typeIdentifiers = CompositionTypeSyntax(self) {
return .composition(UnorderedEquatingCollection(typeIdentifiers.elements.map { $0.type.typeDescription }))
Expand Down Expand Up @@ -263,23 +301,34 @@ extension TypeSyntax {
return .attributed(
typeIdentifier.baseType.typeDescription,
specifier: typeIdentifier.specifier?.text,
attributes: attributes.isEmpty ? nil : attributes)
attributes: attributes.isEmpty ? nil : attributes
)

} else if let typeIdentifier = ArrayTypeSyntax(self) {
return .array(element: typeIdentifier.element.typeDescription)

} else if let typeIdentifier = DictionaryTypeSyntax(self) {
return .dictionary(
key: typeIdentifier.key.typeDescription,
value: typeIdentifier.value.typeDescription)
value: typeIdentifier.value.typeDescription
)

} else if let typeIdentifier = TupleTypeSyntax(self) {
return .tuple(typeIdentifier.elements.map {
let elements = typeIdentifier.elements.map {
TypeDescription.TupleElement(
label: $0.secondName?.text ?? $0.firstName?.text,
typeDescription: $0.type.typeDescription
)
})
}
if elements.isEmpty {
return .void(.tuple)
} else if elements.count == 1 {
// A type wrapped in a tuple is equivalent to the underlying type.
// To avoid handling complex comparisons later, just strip the type.
return elements[0].typeDescription
} else {
return .tuple(elements)
}

} else if ClassRestrictionTypeSyntax(self) != nil {
// A class restriction is the same as requiring inheriting from AnyObject:
Expand All @@ -291,7 +340,8 @@ extension TypeSyntax {
arguments: typeIdentifier.parameters.map { $0.type.typeDescription },
isAsync: typeIdentifier.effectSpecifiers?.asyncSpecifier != nil,
doesThrow: typeIdentifier.effectSpecifiers?.throwsSpecifier != nil,
returnType: typeIdentifier.returnClause.type.typeDescription)
returnType: typeIdentifier.returnClause.type.typeDescription
)

} else {
// The description is a source-accurate description of this node, so it is a reasonable fallback.
Expand Down Expand Up @@ -353,7 +403,7 @@ extension ExprSyntax {
name: name,
parentType: parentType, generics: genericTypeVisitor.genericArguments
)
case .any, .array, .attributed, .closure, .composition, .dictionary, .implicitlyUnwrappedOptional, .metatype, .optional, .some, .tuple, .unknown:
case .any, .array, .attributed, .closure, .composition, .dictionary, .implicitlyUnwrappedOptional, .metatype, .optional, .some, .tuple, .unknown, .void:
return .unknown(text: trimmedDescription)
}
} else if let tupleExpr = TupleExprSyntax(self) {
Expand Down
2 changes: 1 addition & 1 deletion Sources/SafeDIMacros/Macros/InjectableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public struct InjectableMacro: PeerMacro {
switch TypeSyntax(stringLiteral: stringLiteral).typeDescription {
case .simple:
break
case .nested, .composition, .optional, .implicitlyUnwrappedOptional, .some, .any, .metatype, .attributed, .array, .dictionary, .tuple, .closure, .unknown:
case .nested, .composition, .optional, .implicitlyUnwrappedOptional, .some, .any, .metatype, .attributed, .array, .dictionary, .tuple, .closure, .unknown, .void:
throw InjectableError.fulfilledByTypeArgumentInvalidTypeDescription
}
} else {
Expand Down
93 changes: 92 additions & 1 deletion Tests/SafeDICoreTests/TypeDescriptionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ import XCTest

final class TypeDescriptionTests: XCTestCase {

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingAVoidTypeIdentifierSyntax_findsTheType() throws {
let content = """
var void: Void = ()
"""

let visitor = TypeIdentifierSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))

let typeDescription = try XCTUnwrap(visitor.typeIdentifier)
XCTAssertFalse(typeDescription.isUnknown, "Type description is not of known type!")
XCTAssertEqual(typeDescription.asSource, "Void")
}

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingATypeIdentifierSyntax_findsTheType() throws {
let content = """
var int: Int = 1
Expand Down Expand Up @@ -302,6 +315,45 @@ final class TypeDescriptionTests: XCTestCase {
XCTAssertEqual(typeDescription.asSource, "Dictionary<Int, Dictionary<Int, String>>")
}

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingAVoidTupleTypeSyntax_findsTheType() throws {
let content = """
var voidTuple: ()
"""

let visitor = TupleTypeSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))

let typeDescription = try XCTUnwrap(visitor.tupleTypeIdentifier)
XCTAssertFalse(typeDescription.isUnknown, "Type description is not of known type!")
XCTAssertEqual(typeDescription.asSource, "()")
}

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingASpelledOutVoidWrappedInTupleTypeSyntax_findsTheType() throws {
let content = """
var voidTuple: (Void)
"""

let visitor = TupleTypeSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))

let typeDescription = try XCTUnwrap(visitor.tupleTypeIdentifier)
XCTAssertFalse(typeDescription.isUnknown, "Type description is not of known type!")
XCTAssertEqual(typeDescription.asSource, "Void")
}

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingAVoidWrappedInTupleTypeSyntax_findsTheType() throws {
let content = """
var voidTuple: (())
"""

let visitor = TupleTypeSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))

let typeDescription = try XCTUnwrap(visitor.tupleTypeIdentifier)
XCTAssertFalse(typeDescription.isUnknown, "Type description is not of known type!")
XCTAssertEqual(typeDescription.asSource, "()")
}

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingATupleTypeSyntax_findsTheType() throws {
let content = """
var tuple: (Int, String)
Expand All @@ -315,6 +367,19 @@ final class TypeDescriptionTests: XCTestCase {
XCTAssertEqual(typeDescription.asSource, "(Int, String)")
}

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingASigleElementTupleTypeSyntax_findsTheType() throws {
let content = """
var tupleWrappedString: (String)
"""

let visitor = TupleTypeSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))

let typeDescription = try XCTUnwrap(visitor.tupleTypeIdentifier)
XCTAssertFalse(typeDescription.isUnknown, "Type description is not of known type!")
XCTAssertEqual(typeDescription.asSource, "String")
}

func test_typeDescription_whenCalledOnATypeSyntaxNodeRepresentingAClassRestrictionTypeSyntax_findsTheType() throws {
let content = """
protocol SomeObject: class {}
Expand Down Expand Up @@ -353,6 +418,18 @@ final class TypeDescriptionTests: XCTestCase {
XCTAssertEqual(typeDescription.asSource, "(Int, Double) throws -> String")
}

func test_typeDescription_whenCalledOnAExprSyntaxNodeRepresentingAVoidType_findsTheType() throws {
let content = """
let type: Void.Type = Void.self
"""
let visitor = MemberAccessExprSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))

let typeDescription = try XCTUnwrap(visitor.typeDescription)
XCTAssertFalse(typeDescription.isUnknown, "Type description is not of known type!")
XCTAssertEqual(typeDescription.asSource, "Void")
}

func test_typeDescription_whenCalledOnAExprSyntaxNodeRepresentingASimpleType_findsTheType() throws {
let content = """
let type: Any.Type = String.self
Expand Down Expand Up @@ -511,7 +588,7 @@ final class TypeDescriptionTests: XCTestCase {

func test_typeDescription_whenCalledOnAExprSyntaxNodeRepresentingAThrowingClosureType_findsTheType() throws {
let content = """
let test: Any.Type = (() throws -> ()).self
let test: Any.Type = (((() throws -> ()))).self
"""
let visitor = MemberAccessExprSyntaxVisitor(viewMode: .sourceAccurate)
visitor.walk(Parser.parse(source: content))
Expand Down Expand Up @@ -547,6 +624,20 @@ final class TypeDescriptionTests: XCTestCase {
XCTAssertEqual(typeDescription.asSource, "<[]>")
}

func test_equality_isTrueWhenComparingDifferentVoidSpellings() {
XCTAssertEqual(
TypeDescription.void(.identifier),
TypeDescription.void(.tuple)
)
}

func test_equality_isTrueWhenComparingDifferentVoidSpellingsInHashedCollections() {
XCTAssertEqual(
Set([TypeDescription.void(.identifier)]),
Set([TypeDescription.void(.tuple)])
)
}

func test_equality_isTrueWhenComparingLexigraphicallyEquivalentCompositions() {
XCTAssertEqual(
TypeDescription.composition([
Expand Down
26 changes: 26 additions & 0 deletions Tests/SafeDIMacrosTests/InstantiableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,32 @@ final class InstantiableMacroTests: XCTestCase {
}
}

func test_declaration_doesNotGenerateRequiredInitializerIfItAlreadyExistsWithTupleWrappedClosureDependency() {
assertMacro {
"""
@Instantiable
public struct ExampleService {
public init(closure: @escaping @Sendable () -> Void) {
self.closure = closure
}
@Forwarded
let closure: (() -> Void)
}
"""
} expansion: {
"""
public struct ExampleService {
public init(closure: @escaping @Sendable () -> Void) {
self.closure = closure
}
let closure: (() -> Void)
public typealias ForwardedArguments = () -> Void
}
"""
}
}

func test_declaration_generatesRequiredInitializerWithDependencies() {
assertMacro {
"""
Expand Down

0 comments on commit faeb519

Please sign in to comment.