diff --git a/Sources/SafeDICore/Models/TypeDescription.swift b/Sources/SafeDICore/Models/TypeDescription.swift index 24e564dc..bff741dd 100644 --- a/Sources/SafeDICore/Models/TypeDescription.swift +++ b/Sources/SafeDICore/Models/TypeDescription.swift @@ -200,7 +200,7 @@ public enum TypeDescription: Codable, Hashable, Comparable, Sendable { } } - var isOptional: Bool { + public var isOptional: Bool { switch self { case .any, .array, @@ -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, diff --git a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift index 4eda247f..dabde52d 100644 --- a/Sources/SafeDIMacros/Macros/InstantiableMacro.swift +++ b/Sources/SafeDIMacros/Macros/InstantiableMacro.swift @@ -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 } } @@ -339,6 +346,7 @@ public struct InstantiableMacro: MemberMacro { private enum InstantiableError: Error, CustomStringConvertible { case decoratingIncompatibleType + case fulfillingAdditionalTypesContainsOptional case fulfillingAdditionalTypesArgumentInvalid case tooManyInstantiateMethods @@ -346,6 +354,8 @@ public struct InstantiableMacro: MemberMacro { 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: diff --git a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift index ae7f6a23..2ca6fe30 100644 --- a/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift +++ b/Tests/SafeDIMacrosTests/InstantiableMacroTests.swift @@ -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.self]) + public final class ExampleService: Instantiable {} + """ + } diagnostics: { + """ + @Instantiable(fulfillingAdditionalTypes: [Optional.self]) + ┬─────────────────────────────────────────────────────────────────── + ╰─ 🛑 The argument `fulfillingAdditionalTypes` must not include optionals + public final class ExampleService: Instantiable {} """ } } @@ -794,7 +826,7 @@ final class InstantiableMacroTests: XCTestCase { """ let fulfillingAdditionalTypes: [Any.Type] = [AnyObject.self] @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) - public final class ExampleService {} + public final class ExampleService: Instantiable {} """ } diagnostics: { """ @@ -802,7 +834,7 @@ final class InstantiableMacroTests: XCTestCase { @Instantiable(fulfillingAdditionalTypes: fulfillingAdditionalTypes) ┬────────────────────────────────────────────────────────────────── ╰─ 🛑 The argument `fulfillingAdditionalTypes` must be an inlined array - public final class ExampleService {} + public final class ExampleService: Instantiable {} """ } } @@ -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 {} """ } }