Skip to content

Commit

Permalink
Explicitly disallow optionals in 'fulfillingAdditionalTypes' parameter (
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed authored Apr 22, 2024
1 parent 289d2f4 commit adeeee4
Show file tree
Hide file tree
Showing 3 changed files with 61 additions and 12 deletions.
17 changes: 12 additions & 5 deletions Sources/SafeDICore/Models/TypeDescription.swift
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable {
}
}

var isOptional: Bool {
public var isOptional: Bool {
switch self {
case .any,
.array,
Expand Down Expand Up @@ -438,10 +438,17 @@ extension ExprSyntax {
genericTypeVisitor.walk(genericExpr.genericArgumentClause)
switch genericExpr.expression.typeDescription {
case let .simple(name, _):
return .simple(
name: name,
generics: genericTypeVisitor.genericArguments
)
if name == "Optional",
genericTypeVisitor.genericArguments.count == 1,
let firstGenericArgument = genericTypeVisitor.genericArguments.first
{
return .optional(firstGenericArgument)
} else {
return .simple(
name: name,
generics: genericTypeVisitor.genericArguments
)
}
case let .nested(name, parentType, _):
return .nested(
name: name,
Expand Down
12 changes: 11 additions & 1 deletion Sources/SafeDIMacros/Macros/InstantiableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,14 @@ public struct InstantiableMacro: MemberMacro {
declaration.attributes.instantiableMacro
)?.fulfillingAdditionalTypes
{
if ArrayExprSyntax(fulfillingAdditionalTypesArgument) == nil {
if let arrayExpression = ArrayExprSyntax(fulfillingAdditionalTypesArgument) {
if arrayExpression
.elements
.contains(where: { $0.expression.typeDescription.isOptional })
{
throw InstantiableError.fulfillingAdditionalTypesContainsOptional
}
} else {
throw InstantiableError.fulfillingAdditionalTypesArgumentInvalid
}
}
Expand Down Expand Up @@ -339,13 +346,16 @@ public struct InstantiableMacro: MemberMacro {

private enum InstantiableError: Error, CustomStringConvertible {
case decoratingIncompatibleType
case fulfillingAdditionalTypesContainsOptional
case fulfillingAdditionalTypesArgumentInvalid
case tooManyInstantiateMethods

var description: String {
switch self {
case .decoratingIncompatibleType:
"@\(InstantiableVisitor.macroName) must decorate an extension on a type or a class, struct, or actor declaration"
case .fulfillingAdditionalTypesContainsOptional:
"The argument `fulfillingAdditionalTypes` must not include optionals"
case .fulfillingAdditionalTypesArgumentInvalid:
"The argument `fulfillingAdditionalTypes` must be an inlined array"
case .tooManyInstantiateMethods:
Expand Down
44 changes: 38 additions & 6 deletions Tests/SafeDIMacrosTests/InstantiableMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -777,14 +777,46 @@ final class InstantiableMacroTests: XCTestCase {
assertMacro {
"""
@Instantiable
public enum ExampleService {}
public enum ExampleService: Instantiable {}
"""
} diagnostics: {
"""
@Instantiable
┬────────────
╰─ 🛑 @Instantiable must decorate an extension on a type or a class, struct, or actor declaration
public enum ExampleService {}
public enum ExampleService: Instantiable {}
"""
}
}

func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIncludesAShortformOptional() {
assertMacro {
"""
@Instantiable(fulfillingAdditionalTypes: [AnyObject?.self])
public final class ExampleService: Instantiable {}
"""
} diagnostics: {
"""
@Instantiable(fulfillingAdditionalTypes: [AnyObject?.self])
┬──────────────────────────────────────────────────────────
╰─ 🛑 The argument `fulfillingAdditionalTypes` must not include optionals
public final class ExampleService: Instantiable {}
"""
}
}

func test_declaration_throwsErrorWhenFulfillingAdditionalTypesIncludesALongformOptional() {
assertMacro {
"""
@Instantiable(fulfillingAdditionalTypes: [Optional<AnyObject>.self])
public final class ExampleService: Instantiable {}
"""
} diagnostics: {
"""
@Instantiable(fulfillingAdditionalTypes: [Optional<AnyObject>.self])
┬───────────────────────────────────────────────────────────────────
╰─ 🛑 The argument `fulfillingAdditionalTypes` must not include optionals
public final class ExampleService: Instantiable {}
"""
}
}
Expand All @@ -794,15 +826,15 @@ final class InstantiableMacroTests: XCTestCase {
"""
let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self]
@Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes)
public final class ExampleService {}
public final class ExampleService: Instantiable {}
"""
} diagnostics: {
"""
let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self]
@Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes)
┬──────────────────────────────────────────────────────────────────
╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array
public final class ExampleService {}
public final class ExampleService: Instantiable {}
"""
}
}
Expand All @@ -811,14 +843,14 @@ final class InstantiableMacroTests: XCTestCase {
assertMacro {
"""
@Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }())
public final class ExampleService {}
public final class ExampleService: Instantiable {}
"""
} diagnostics: {
"""
@Instantiable(fulfillingAdditionalTypes: { [AnyObject.self] }())
┬───────────────────────────────────────────────────────────────
╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array
public final class ExampleService {}
public final class ExampleService: Instantiable {}
"""
}
}
Expand Down

0 comments on commit adeeee4

Please sign in to comment.