diff --git a/README.md b/README.md index 0203d80..a338a07 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,9 @@ import DependenciesMacro public struct TestClient { public var request: @Sendable (_ request: Request) -> Void } + +@DependencyValue(TestClient.self) +public extension DependencyValues {} ``` `@PublicInit` is a Member Macro and provides a public initializer. This macro can be applied only to public structs. @@ -31,17 +34,18 @@ extension TestClient: TestDependencyKey { request: unimplemented("\(Self.self).request") ) } + +public extension DependencyValues { + var testClient: TestClient { + get { + self[TestClient.self] + } + set { + self[TestClient.self] = newValue + } + } +} ``` -> [!WARNING] -> The following DependencyValues extension are not generated by macro -> ```Swift -> public extension DependencyValues { -> var testClient: TestClient { -> get { self[TestClient.self] } -> set { self[TestClient.self] = newValue } -> } -> } -> ``` ## Installation This library can only be installed from swift package manager. diff --git a/Sources/DependenciesMacro/DependenciesMacro.swift b/Sources/DependenciesMacro/DependenciesMacro.swift index 50ba790..2227b99 100644 --- a/Sources/DependenciesMacro/DependenciesMacro.swift +++ b/Sources/DependenciesMacro/DependenciesMacro.swift @@ -5,3 +5,6 @@ public macro PublicInit() = #externalMacro(module: "PublicInitMacroPlugin", type @attached(extension, conformances: TestDependencyKey, names: named(testValue)) public macro Dependencies() = #externalMacro(module: "DependenciesMacroPlugin", type: "DependenciesMacro") + +@attached(member, names: arbitrary) +public macro DependencyValue(_ type: T.Type) = #externalMacro(module: "DependenciesMacroPlugin", type: "DependencyValuesMacro") diff --git a/Sources/DependenciesMacroClient/main.swift b/Sources/DependenciesMacroClient/main.swift index fe208b5..d113819 100644 --- a/Sources/DependenciesMacroClient/main.swift +++ b/Sources/DependenciesMacroClient/main.swift @@ -7,9 +7,6 @@ public struct TestClient { public var request: @Sendable (_ request: String) -> Void } +@DependencyValue(TestClient.self) public extension DependencyValues { - var testClient: TestClient { - get { self[TestClient.self] } - set { self[TestClient.self] = newValue } - } } diff --git a/Sources/DependenciesMacroPlugin/DependenciesMacro.swift b/Sources/DependenciesMacroPlugin/DependenciesMacro.swift index c2fd18a..ec4ee9f 100644 --- a/Sources/DependenciesMacroPlugin/DependenciesMacro.swift +++ b/Sources/DependenciesMacroPlugin/DependenciesMacro.swift @@ -2,9 +2,7 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct DependenciesMacro { - -} +public struct DependenciesMacro {} extension DependenciesMacro: ExtensionMacro { public static func expansion( @@ -42,8 +40,3 @@ extension DependenciesMacro: ExtensionMacro { ] } } -extension String { - func initialLowerCased() -> String { - return self.prefix(1).lowercased() + self.dropFirst() - } -} diff --git a/Sources/DependenciesMacroPlugin/DependenciesMacroDiagnostic.swift b/Sources/DependenciesMacroPlugin/DependenciesMacroDiagnostic.swift index 6283c87..b7b08d1 100644 --- a/Sources/DependenciesMacroPlugin/DependenciesMacroDiagnostic.swift +++ b/Sources/DependenciesMacroPlugin/DependenciesMacroDiagnostic.swift @@ -14,7 +14,7 @@ extension DependenciesMacroDiagnostic: DiagnosticMessage { public var message: String { switch self { case .notStruct: - "PublicInit Macro can only be applied to struct." + "Dependencies Macro can only be applied to struct." } } diff --git a/Sources/DependenciesMacroPlugin/DependenciesMacroPlugin.swift b/Sources/DependenciesMacroPlugin/DependenciesMacroPlugin.swift index f2bdf3e..07884d5 100644 --- a/Sources/DependenciesMacroPlugin/DependenciesMacroPlugin.swift +++ b/Sources/DependenciesMacroPlugin/DependenciesMacroPlugin.swift @@ -4,6 +4,7 @@ import SwiftSyntaxMacros @main struct DependenciesMacroPlugin: CompilerPlugin { let providingMacros: [Macro.Type] = [ - DependenciesMacro.self + DependenciesMacro.self, + DependencyValuesMacro.self ] } diff --git a/Sources/DependenciesMacroPlugin/DependencyValueMacroDiagnostic.swift b/Sources/DependenciesMacroPlugin/DependencyValueMacroDiagnostic.swift new file mode 100644 index 0000000..ba161ae --- /dev/null +++ b/Sources/DependenciesMacroPlugin/DependencyValueMacroDiagnostic.swift @@ -0,0 +1,110 @@ +import SwiftSyntax +import SwiftSyntaxMacros +import SwiftDiagnostics + +public enum DependencyValuesMacroDiagnostic { + case notExtension + case notDependencyValues + case invalidArgument +} + +extension DependencyValuesMacroDiagnostic: DiagnosticMessage { + func diagnose(at node: some SyntaxProtocol) -> Diagnostic { + Diagnostic(node: Syntax(node), message: self) + } + + public var message: String { + switch self { + case .notExtension: + "DependencyValue Macro can only be applied to extension." + + case .invalidArgument: + "Invalid argument." + + case .notDependencyValues: + "DependencyValue Macro can only be applied to extension of DependencyValues" + } + } + + public var severity: DiagnosticSeverity { .error } + + public var diagnosticID: MessageID { + switch self { + case .notExtension: + MessageID(domain: "DependencyValuesMacroDiagnostic", id: "notExtension") + + case .invalidArgument: + MessageID(domain: "DependencyValuesMacroDiagnostic", id: "invalidArgument") + + case .notDependencyValues: + MessageID(domain: "DependencyValuesMacroDiagnostic", id: "invalidArgument") + } + } +} + +public extension DependencyValuesMacro { + static func decodeExpansion( + of syntax: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> (decl: ExtensionDeclSyntax, type: String) { + guard let extensionDecl = declaration.as(ExtensionDeclSyntax.self) else { + if let actorDecl = declaration.as(ActorDeclSyntax.self) { + throw DiagnosticsError( + diagnostics: [ + DependencyValuesMacroDiagnostic.notExtension.diagnose(at: actorDecl.actorKeyword) + ] + ) + } + else if let classDecl = declaration.as(ClassDeclSyntax.self) { + throw DiagnosticsError( + diagnostics: [ + DependencyValuesMacroDiagnostic.notExtension.diagnose(at: classDecl.classKeyword) + ] + ) + } + else if let enumDecl = declaration.as(EnumDeclSyntax.self) { + throw DiagnosticsError( + diagnostics: [ + DependencyValuesMacroDiagnostic.notExtension.diagnose(at: enumDecl.enumKeyword) + ] + ) + } + else if let structDecl = declaration.as(StructDeclSyntax.self) { + throw DiagnosticsError( + diagnostics: [ + DependencyValuesMacroDiagnostic.notExtension.diagnose(at: structDecl.structKeyword) + ] + ) + } + else { + throw DiagnosticsError( + diagnostics: [ + DependencyValuesMacroDiagnostic.notExtension.diagnose(at: declaration) + ] + ) + } + } + + guard extensionDecl.extendedType.as(IdentifierTypeSyntax.self)?.name.text == "DependencyValues" else { + throw DiagnosticsError( + diagnostics: [ + DependencyValuesMacroDiagnostic.notDependencyValues.diagnose(at: extensionDecl.extendedType) + ] + ) + } + + guard case .argumentList(let arguments) = syntax.arguments, + let type = arguments.first?.expression.as(MemberAccessExprSyntax.self)?.base?.as(DeclReferenceExprSyntax.self)?.baseName.text, + arguments.count == 1 + else { + throw DiagnosticsError( + diagnostics: [ + DependencyValuesMacroDiagnostic.invalidArgument.diagnose(at: declaration) + ] + ) + } + + return (extensionDecl, type) + } +} diff --git a/Sources/DependenciesMacroPlugin/DependencyValuesMacro.swift b/Sources/DependenciesMacroPlugin/DependencyValuesMacro.swift new file mode 100644 index 0000000..9fbb180 --- /dev/null +++ b/Sources/DependenciesMacroPlugin/DependencyValuesMacro.swift @@ -0,0 +1,38 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftDiagnostics +import SwiftCompilerPluginMessageHandling + +public struct DependencyValuesMacro: MemberMacro { + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: some DeclGroupSyntax, + in context: some MacroExpansionContext + ) throws -> [DeclSyntax] { + let (_, typeName) = try decodeExpansion( + of: node, + attachedTo: declaration, + in: context + ) + + let variableName = typeName.initialLowerCased() + + return [ + DeclSyntax( + """ + var \(raw: variableName): \(raw: typeName) { + get { self[\(raw: typeName).self] } + set { self[\(raw: typeName).self] = newValue } + } + """ + ) + ] + } +} + +extension String { + func initialLowerCased() -> String { + return prefix(1).lowercased() + dropFirst() + } +} diff --git a/Tests/DependenciesMacroTests/DependenciesMacroTests.swift b/Tests/DependenciesMacroTests/DependenciesMacroTests.swift index ed3716f..6637b54 100644 --- a/Tests/DependenciesMacroTests/DependenciesMacroTests.swift +++ b/Tests/DependenciesMacroTests/DependenciesMacroTests.swift @@ -23,7 +23,7 @@ final class DependenciesMacroTests: XCTestCase { @Dependencies class Test {} ┬──── - ╰─ 🛑 PublicInit Macro can only be applied to struct. + ╰─ 🛑 Dependencies Macro can only be applied to struct. """ } assertMacro { @@ -36,7 +36,7 @@ final class DependenciesMacroTests: XCTestCase { @Dependencies enum Test {} ┬─── - ╰─ 🛑 PublicInit Macro can only be applied to struct. + ╰─ 🛑 Dependencies Macro can only be applied to struct. """ } assertMacro { @@ -49,7 +49,7 @@ final class DependenciesMacroTests: XCTestCase { @Dependencies actor Test {} ┬──── - ╰─ 🛑 PublicInit Macro can only be applied to struct. + ╰─ 🛑 Dependencies Macro can only be applied to struct. """ } } diff --git a/Tests/DependenciesMacroTests/DependencyValueMacroTests.swift b/Tests/DependenciesMacroTests/DependencyValueMacroTests.swift new file mode 100644 index 0000000..6996cdb --- /dev/null +++ b/Tests/DependenciesMacroTests/DependencyValueMacroTests.swift @@ -0,0 +1,108 @@ +import DependenciesMacro +import DependenciesMacroPlugin +import MacroTesting +import XCTest + +final class DependencyValueMacroTests: XCTestCase { + override func invokeTest() { + withMacroTesting( + macros: ["DependencyValue": DependencyValuesMacro.self] + ) { + super.invokeTest() + } + } + + func testDiagnostic() { + assertMacro { + """ + @DependencyValue(TestClient.self) + public struct DependencyValues {} + """ + } matches: { + """ + @DependencyValue(TestClient.self) + public struct DependencyValues {} + ┬───── + ╰─ 🛑 DependencyValue Macro can only be applied to extension. + """ + } + + assertMacro { + """ + @DependencyValue(TestClient.self) + public class DependencyValues {} + """ + } matches: { + """ + @DependencyValue(TestClient.self) + public class DependencyValues {} + ┬──── + ╰─ 🛑 DependencyValue Macro can only be applied to extension. + """ + } + + assertMacro { + """ + @DependencyValue(TestClient.self) + public actor DependencyValues {} + """ + } matches: { + """ + @DependencyValue(TestClient.self) + public actor DependencyValues {} + ┬──── + ╰─ 🛑 DependencyValue Macro can only be applied to extension. + """ + } + + assertMacro { + """ + @DependencyValue(TestClient.self) + public enum DependencyValues {} + """ + } matches: { + """ + @DependencyValue(TestClient.self) + public enum DependencyValues {} + ┬─── + ╰─ 🛑 DependencyValue Macro can only be applied to extension. + """ + } + + assertMacro { + """ + @DependencyValue(TestClient.self) + public extension Test {} + """ + } matches: { + """ + @DependencyValue(TestClient.self) + public extension Test {} + ┬─── + ╰─ 🛑 DependencyValue Macro can only be applied to extension of DependencyValues + """ + } + } + + func testMacro() { + assertMacro { + """ + @DependencyValue(TestClient.self) + public extension DependencyValues {} + """ + } matches: { + """ + public extension DependencyValues { + + var testClient: TestClient { + get { + self [TestClient.self] + } + set { + self [TestClient.self] = newValue + } + }} + """ + } + } +}