From d8cd1c05c37ebd977fa5f749afac10c8077375af Mon Sep 17 00:00:00 2001 From: Jack Rosen Date: Fri, 18 Oct 2024 16:50:07 -0400 Subject: [PATCH 1/2] Add InjectableInit Macro --- Sources/WhoopDIKit/Macros.swift | 4 +++ .../InjectableInitMacro.swift | 13 ++++++++ .../WhoopDIKitMacros/InjectableMacro.swift | 29 ++++++++++++++++ Sources/WhoopDIKitMacros/Plugin.swift | 3 +- .../Support/VariableDeclaration.swift | 18 ++++++++++ .../Injectable/InjectableTests.swift | 33 +++++++++++++++++++ 6 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 Sources/WhoopDIKitMacros/InjectableInitMacro.swift diff --git a/Sources/WhoopDIKit/Macros.swift b/Sources/WhoopDIKit/Macros.swift index 9e63ab0..eba7942 100644 --- a/Sources/WhoopDIKit/Macros.swift +++ b/Sources/WhoopDIKit/Macros.swift @@ -10,3 +10,7 @@ public macro Injectable() = #externalMacro(module: "WhoopDIKitMacros", type: "In /// The `@InjectableName` macro is used as a marker for the `@Injectable` protocol to add a `WhoopDI.inject(name)` in the inject method @attached(peer) public macro InjectableName(name: String) = #externalMacro(module: "WhoopDIKitMacros", type: "InjectableNameMacro") + +/// The `@InjectableInit` macro is used as a marker for the `@Injectable` protocol to use as the init for the static `inject` function +@attached(peer) +public macro InjectableInit() = #externalMacro(module: "WhoopDIKitMacros", type: "InjectableInitMacro") diff --git a/Sources/WhoopDIKitMacros/InjectableInitMacro.swift b/Sources/WhoopDIKitMacros/InjectableInitMacro.swift new file mode 100644 index 0000000..a2f2329 --- /dev/null +++ b/Sources/WhoopDIKitMacros/InjectableInitMacro.swift @@ -0,0 +1,13 @@ +import SwiftSyntax +import SwiftSyntaxMacros +import SwiftSyntaxMacroExpansion + +struct InjectableInitMacro: PeerMacro { + static func expansion(of node: SwiftSyntax.AttributeSyntax, providingPeersOf declaration: some SwiftSyntax.DeclSyntaxProtocol, in context: some SwiftSyntaxMacros.MacroExpansionContext) throws -> [SwiftSyntax.DeclSyntax] { + guard declaration.kind == .initializerDecl else { + throw MacroExpansionErrorMessage("@InjectableInit can only be applied to an initializer") + } + + return [] + } +} diff --git a/Sources/WhoopDIKitMacros/InjectableMacro.swift b/Sources/WhoopDIKitMacros/InjectableMacro.swift index 104c36a..569b5c3 100644 --- a/Sources/WhoopDIKitMacros/InjectableMacro.swift +++ b/Sources/WhoopDIKitMacros/InjectableMacro.swift @@ -11,6 +11,35 @@ struct InjectableMacro: ExtensionMacro, MemberMacro { throw MacroExpansionErrorMessage("@Injectable needs to be declared on a concrete type, not a protocol") } + let allInjectableInits = declaration.allInjectableInits + + if allInjectableInits.isEmpty { + return try createInitializerAndInject(declaration: declaration) + } else if allInjectableInits.count > 1 { + throw MacroExpansionErrorMessage("Only one initializer with the `@InjectableInit` macro is allowed") + } else { + let initValue = allInjectableInits[0] + return try createInject(from: initValue, declaration: declaration) + } + } + + private static func createInject(from initValue: InitializerDeclSyntax, declaration: some DeclGroupSyntax) throws -> [DeclSyntax] { + let allArgs = initValue.signature.parameterClause.parameters.map { parameter in + "\(parameter.firstName.text == "_" ? "" : "\(parameter.firstName.text): ")container.inject()" + }.joined(separator: ", ") + + let accessLevel = self.accessLevel(declaration: declaration) ?? "internal" + + return [ + """ + \(raw: accessLevel) static func inject(container: Container) -> Self { + Self.init(\(raw: allArgs)) + } + """ + ] + } + + private static func createInitializerAndInject(declaration: some DeclGroupSyntax) throws -> [DeclSyntax] { let allVariables = declaration.allMemberVariables // Create the initializer args in the form `name: type = default` diff --git a/Sources/WhoopDIKitMacros/Plugin.swift b/Sources/WhoopDIKitMacros/Plugin.swift index 90e89f0..9123b93 100644 --- a/Sources/WhoopDIKitMacros/Plugin.swift +++ b/Sources/WhoopDIKitMacros/Plugin.swift @@ -5,6 +5,7 @@ import SwiftSyntaxMacros struct WhoopDIKitPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ InjectableMacro.self, - InjectableNameMacro.self + InjectableNameMacro.self, + InjectableInitMacro.self ] } diff --git a/Sources/WhoopDIKitMacros/Support/VariableDeclaration.swift b/Sources/WhoopDIKitMacros/Support/VariableDeclaration.swift index cb94195..e6b7cff 100644 --- a/Sources/WhoopDIKitMacros/Support/VariableDeclaration.swift +++ b/Sources/WhoopDIKitMacros/Support/VariableDeclaration.swift @@ -35,6 +35,24 @@ extension DeclGroupSyntax { } } + var allInjectableInits: [InitializerDeclSyntax] { + self.memberBlock.members.compactMap { member in + if let initSyntax = member.decl.as(InitializerDeclSyntax.self), + initSyntax.attributes.contains(where: { element in + switch element { + case .attribute(let syntax): + return syntax.attributeName.as(IdentifierTypeSyntax.self)?.name.text == "InjectableInit" + default: + return false + } + }) { + return initSyntax + } else { + return nil + } + } + } + private func injectableName(variableSyntax: VariableDeclSyntax) -> String? { variableSyntax.attributes.compactMap { (attribute) -> String? in switch attribute { diff --git a/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift b/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift index 45f16b9..c192c91 100644 --- a/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift +++ b/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift @@ -41,6 +41,39 @@ final class InjectableTests: XCTestCase { macros: ["Injectable": InjectableMacro.self, "InjectableName": InjectableNameMacro.self]) } + func testBasicInjectWithInjectableInit() { + assertMacroExpansion( + """ + @Injectable struct TestThing { + let bestThing: Int + + @InjectableInit + internal init(notReal: Int, _ extraArg: String) { + self.bestThing = notReal + } + } + """, + + expandedSource: + """ + struct TestThing { + let bestThing: Int + + internal init(notReal: Int, _ extraArg: String) { + self.bestThing = notReal + } + + internal static func inject(container: Container) -> Self { + Self.init(notReal: container.inject(), container.inject()) + } + } + + extension TestThing : Injectable { + } + """, + macros: ["Injectable": InjectableMacro.self, "InjectableName": InjectableNameMacro.self, "InjectableInit": InjectableInitMacro.self]) + } + func testInjectWithSpecifiers() { assertMacroExpansion( """ From 746d907525e73659f6591d6d5edc561d9d58e72b Mon Sep 17 00:00:00 2001 From: Jack Rosen Date: Mon, 21 Oct 2024 09:49:08 -0400 Subject: [PATCH 2/2] Fix spacing --- Tests/WhoopDIKitTests/Injectable/InjectableTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift b/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift index c192c91..851fea1 100644 --- a/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift +++ b/Tests/WhoopDIKitTests/Injectable/InjectableTests.swift @@ -58,7 +58,6 @@ final class InjectableTests: XCTestCase { """ struct TestThing { let bestThing: Int - internal init(notReal: Int, _ extraArg: String) { self.bestThing = notReal }