├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── DependenciesMacro │ └── DependenciesMacro.swift ├── DependenciesMacroClient │ └── main.swift ├── DependenciesMacroPlugin │ ├── DependenciesMacro.swift │ ├── DependenciesMacroDiagnostic.swift │ ├── DependenciesMacroPlugin.swift │ ├── DependencyValueMacroDiagnostic.swift │ └── DependencyValuesMacro.swift └── PublicInitMacroPlugin │ ├── PublicInitMacro.swift │ ├── PublicInitMacroDiagnostic.swift │ └── PublicInitMacroPlugin.swift └── Tests ├── DependenciesMacroTests ├── DependenciesMacroTests.swift └── DependencyValueMacroTests.swift └── PublicInitMacroTests └── PublicInitMacroTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ryu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "combine-schedulers", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/combine-schedulers", 7 | "state" : { 8 | "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-clocks", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-clocks", 16 | "state" : { 17 | "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", 18 | "version" : "1.0.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-concurrency-extras", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras", 25 | "state" : { 26 | "revision" : "ea631ce892687f5432a833312292b80db238186a", 27 | "version" : "1.0.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-dependencies", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/pointfreeco/swift-dependencies", 34 | "state" : { 35 | "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", 36 | "version" : "1.0.0" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-macro-testing", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/pointfreeco/swift-macro-testing.git", 43 | "state" : { 44 | "revision" : "101d84b5fd6df3d70435ab13b5495ff77f930248", 45 | "version" : "0.1.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-snapshot-testing", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 52 | "state" : { 53 | "revision" : "696b86a6d151578bca7c1a2a3ed419a5f834d40f", 54 | "version" : "1.13.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-syntax", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/apple/swift-syntax.git", 61 | "state" : { 62 | "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", 63 | "version" : "509.0.0" 64 | } 65 | }, 66 | { 67 | "identity" : "xctest-dynamic-overlay", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 70 | "state" : { 71 | "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", 72 | "version" : "1.0.2" 73 | } 74 | } 75 | ], 76 | "version" : 2 77 | } 78 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let package = Package( 8 | name: "swift-dependencies-macro", 9 | platforms: [ 10 | .macOS(.v10_15), 11 | .iOS(.v13), 12 | .tvOS(.v13), 13 | .watchOS(.v6), 14 | .macCatalyst(.v13) 15 | ], 16 | products: [ 17 | // Products define the executables and libraries a package produces, making them visible to other packages. 18 | .library( 19 | name: "DependenciesMacro", 20 | targets: ["DependenciesMacro"] 21 | ), 22 | .executable( 23 | name: "DependenciesMacroClient", 24 | targets: ["DependenciesMacroClient"] 25 | ), 26 | ], 27 | dependencies: [ 28 | // Depend on the Swift 5.9 release of SwiftSyntax 29 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), 30 | .package(url: "https://github.com/pointfreeco/swift-macro-testing.git", from: "0.1.0"), 31 | .package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.0.0") 32 | ], 33 | targets: [ 34 | // Targets are the basic building blocks of a package, defining a module or a test suite. 35 | // Targets can depend on other targets in this package and products from dependencies. 36 | // Macro implementation that performs the source transformation of a macro. 37 | .macro( 38 | name: "DependenciesMacroPlugin", 39 | dependencies: [ 40 | .product(name: "SwiftSyntax", package: "swift-syntax"), 41 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 42 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 43 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 44 | ] 45 | ), 46 | .macro( 47 | name: "PublicInitMacroPlugin", 48 | dependencies: [ 49 | .product(name: "SwiftSyntax", package: "swift-syntax"), 50 | .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), 51 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 52 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 53 | ] 54 | ), 55 | 56 | // Library that exposes a macro as part of its API, which is used in client programs. 57 | .target( 58 | name: "DependenciesMacro", 59 | dependencies: [ 60 | "DependenciesMacroPlugin", 61 | "PublicInitMacroPlugin", 62 | .product(name: "Dependencies", package: "swift-dependencies") 63 | ] 64 | ), 65 | 66 | // A client of the library, which is able to use the macro in its own code. 67 | .executableTarget( 68 | name: "DependenciesMacroClient", 69 | dependencies: [ 70 | "DependenciesMacro", 71 | .product(name: "Dependencies", package: "swift-dependencies") 72 | ] 73 | ), 74 | 75 | // A test target used to develop the macro implementation. 76 | .testTarget( 77 | name: "DependenciesMacroTests", 78 | dependencies: [ 79 | "DependenciesMacroPlugin", 80 | .product(name: "MacroTesting", package: "swift-macro-testing") 81 | ] 82 | ), 83 | .testTarget( 84 | name: "PublicInitMacroTests", 85 | dependencies: [ 86 | "PublicInitMacroPlugin", 87 | .product(name: "MacroTesting", package: "swift-macro-testing") 88 | ] 89 | ), 90 | ] 91 | ) 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DependenciesMacro 2 | Macro for convenient use of swift-dependencies 3 | 4 | ## Usage 5 | ```Swift 6 | import Dependencies 7 | import DependenciesMacro 8 | 9 | @PublicInit 10 | @Dependencies 11 | public struct TestClient { 12 | public var request: @Sendable (_ request: Request) -> Void 13 | } 14 | 15 | @DependencyValue(TestClient.self) 16 | public extension DependencyValues {} 17 | ``` 18 | `@PublicInit` is a Member Macro and provides a public initializer. 19 | This macro can be applied only to public structs.
20 | `@Dependencies` is an Extension Macro that can conform TestClient to TestDependencyKey. This macro can also be applied only to structs.
21 | `@DependencyValue` is a Member Macro; by using a Macro for the extension of DependencyValues, you can add a property of the type specified in the argument 22 | 23 | In this example, the macro is expanded as follows. 24 | ```Swift 25 | public struct TestClient { 26 | public var request: @Sendable (_ request: Request) -> Void 27 | public init( 28 | request: @Sendable @escaping (_ request: Request) -> Void 29 | ) { 30 | self.request = request 31 | } 32 | } 33 | extension TestClient: TestDependencyKey { 34 | public static let testValue = TestClient( 35 | request: unimplemented("\(Self.self).request") 36 | ) 37 | } 38 | 39 | public extension DependencyValues { 40 | var testClient: TestClient { 41 | get { 42 | self[TestClient.self] 43 | } 44 | set { 45 | self[TestClient.self] = newValue 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | ## Installation 52 | This library can only be installed from swift package manager. 53 | ```Swift 54 | .package(url: "https://github.com/Ryu0118/swift-dependencies-macro", from: "0.2.2") 55 | ``` 56 | -------------------------------------------------------------------------------- /Sources/DependenciesMacro/DependenciesMacro.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | 3 | @attached(member, names: named(init)) 4 | public macro PublicInit() = #externalMacro(module: "PublicInitMacroPlugin", type: "PublicInitMacro") 5 | 6 | @attached(extension, conformances: TestDependencyKey, names: named(testValue)) 7 | public macro Dependencies() = #externalMacro(module: "DependenciesMacroPlugin", type: "DependenciesMacro") 8 | 9 | @attached(member, names: arbitrary) 10 | public macro DependencyValue(_ type: T.Type) = #externalMacro(module: "DependenciesMacroPlugin", type: "DependencyValuesMacro") 11 | -------------------------------------------------------------------------------- /Sources/DependenciesMacroClient/main.swift: -------------------------------------------------------------------------------- 1 | import DependenciesMacro 2 | import Dependencies 3 | 4 | @PublicInit 5 | @Dependencies 6 | public struct TestClient { 7 | public var request: @Sendable (_ request: String) -> Void 8 | } 9 | 10 | @DependencyValue(TestClient.self) 11 | public extension DependencyValues { 12 | } 13 | -------------------------------------------------------------------------------- /Sources/DependenciesMacroPlugin/DependenciesMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | 5 | public struct DependenciesMacro {} 6 | 7 | extension DependenciesMacro: ExtensionMacro { 8 | public static func expansion( 9 | of node: AttributeSyntax, 10 | attachedTo declaration: some DeclGroupSyntax, 11 | providingExtensionsOf type: some TypeSyntaxProtocol, 12 | conformingTo protocols: [TypeSyntax], 13 | in context: some MacroExpansionContext 14 | ) throws -> [ExtensionDeclSyntax] { 15 | let structDecl = try decodeExpansion(of: node, attachedTo: declaration, in: context) 16 | let storedPropertyBindings = structDecl.memberBlock.members 17 | .compactMap { $0.decl.as(VariableDeclSyntax.self) } 18 | .filter { !$0.modifiers.contains { $0.as(DeclModifierSyntax.self)?.name.text == "static" } } 19 | .map(\.bindings) 20 | .flatMap { $0 } 21 | .filter { $0.accessorBlock == nil } 22 | let modifier = declaration.modifiers 23 | .compactMap { $0.as(DeclModifierSyntax.self)?.name.text } 24 | .first ?? "internal" 25 | let arguments = storedPropertyBindings 26 | .compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text } 27 | .map { "\($0): unimplemented(\"\\(Self.self).\($0)\")" } 28 | .joined(separator: ", \n") 29 | 30 | let testDependencyKeyExtension = try ExtensionDeclSyntax( 31 | """ 32 | extension \(type.trimmed): TestDependencyKey { 33 | \(raw: modifier) static let testValue = \(type.trimmed)( 34 | \(raw: arguments) 35 | ) 36 | } 37 | """ 38 | ) 39 | 40 | return [ 41 | testDependencyKeyExtension, 42 | ] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/DependenciesMacroPlugin/DependenciesMacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | import SwiftDiagnostics 4 | 5 | public enum DependenciesMacroDiagnostic { 6 | case notStruct 7 | } 8 | 9 | extension DependenciesMacroDiagnostic: DiagnosticMessage { 10 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic { 11 | Diagnostic(node: Syntax(node), message: self) 12 | } 13 | 14 | public var message: String { 15 | switch self { 16 | case .notStruct: 17 | "Dependencies Macro can only be applied to struct." 18 | } 19 | } 20 | 21 | public var severity: DiagnosticSeverity { .error } 22 | 23 | public var diagnosticID: MessageID { 24 | switch self { 25 | case .notStruct: 26 | MessageID(domain: "DependenciesMacroDiagnostic", id: "notStruct") 27 | } 28 | } 29 | } 30 | 31 | public extension DependenciesMacro { 32 | static func decodeExpansion( 33 | of syntax: AttributeSyntax, 34 | attachedTo declaration: some DeclGroupSyntax, 35 | in context: some MacroExpansionContext 36 | ) throws -> StructDeclSyntax { 37 | guard let structDecl = declaration.as(StructDeclSyntax.self) else { 38 | if let actorDecl = declaration.as(ActorDeclSyntax.self) { 39 | throw DiagnosticsError( 40 | diagnostics: [ 41 | DependenciesMacroDiagnostic.notStruct.diagnose(at: actorDecl.actorKeyword) 42 | ] 43 | ) 44 | } 45 | else if let classDecl = declaration.as(ClassDeclSyntax.self) { 46 | throw DiagnosticsError( 47 | diagnostics: [ 48 | DependenciesMacroDiagnostic.notStruct.diagnose(at: classDecl.classKeyword) 49 | ] 50 | ) 51 | } 52 | else if let enumDecl = declaration.as(EnumDeclSyntax.self) { 53 | throw DiagnosticsError( 54 | diagnostics: [ 55 | DependenciesMacroDiagnostic.notStruct.diagnose(at: enumDecl.enumKeyword) 56 | ] 57 | ) 58 | } 59 | else { 60 | throw DiagnosticsError( 61 | diagnostics: [ 62 | DependenciesMacroDiagnostic.notStruct.diagnose(at: declaration) 63 | ] 64 | ) 65 | } 66 | } 67 | 68 | return structDecl 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/DependenciesMacroPlugin/DependenciesMacroPlugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct DependenciesMacroPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | DependenciesMacro.self, 8 | DependencyValuesMacro.self 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /Sources/DependenciesMacroPlugin/DependencyValueMacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | import SwiftDiagnostics 4 | 5 | public enum DependencyValuesMacroDiagnostic { 6 | case notExtension 7 | case notDependencyValues 8 | case invalidArgument 9 | } 10 | 11 | extension DependencyValuesMacroDiagnostic: DiagnosticMessage { 12 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic { 13 | Diagnostic(node: Syntax(node), message: self) 14 | } 15 | 16 | public var message: String { 17 | switch self { 18 | case .notExtension: 19 | "DependencyValue Macro can only be applied to extension." 20 | 21 | case .invalidArgument: 22 | "Invalid argument." 23 | 24 | case .notDependencyValues: 25 | "DependencyValue Macro can only be applied to extension of DependencyValues" 26 | } 27 | } 28 | 29 | public var severity: DiagnosticSeverity { .error } 30 | 31 | public var diagnosticID: MessageID { 32 | switch self { 33 | case .notExtension: 34 | MessageID(domain: "DependencyValuesMacroDiagnostic", id: "notExtension") 35 | 36 | case .invalidArgument: 37 | MessageID(domain: "DependencyValuesMacroDiagnostic", id: "invalidArgument") 38 | 39 | case .notDependencyValues: 40 | MessageID(domain: "DependencyValuesMacroDiagnostic", id: "invalidArgument") 41 | } 42 | } 43 | } 44 | 45 | public extension DependencyValuesMacro { 46 | static func decodeExpansion( 47 | of syntax: AttributeSyntax, 48 | attachedTo declaration: some DeclGroupSyntax, 49 | in context: some MacroExpansionContext 50 | ) throws -> (decl: ExtensionDeclSyntax, type: String) { 51 | guard let extensionDecl = declaration.as(ExtensionDeclSyntax.self) else { 52 | if let actorDecl = declaration.as(ActorDeclSyntax.self) { 53 | throw DiagnosticsError( 54 | diagnostics: [ 55 | DependencyValuesMacroDiagnostic.notExtension.diagnose(at: actorDecl.actorKeyword) 56 | ] 57 | ) 58 | } 59 | else if let classDecl = declaration.as(ClassDeclSyntax.self) { 60 | throw DiagnosticsError( 61 | diagnostics: [ 62 | DependencyValuesMacroDiagnostic.notExtension.diagnose(at: classDecl.classKeyword) 63 | ] 64 | ) 65 | } 66 | else if let enumDecl = declaration.as(EnumDeclSyntax.self) { 67 | throw DiagnosticsError( 68 | diagnostics: [ 69 | DependencyValuesMacroDiagnostic.notExtension.diagnose(at: enumDecl.enumKeyword) 70 | ] 71 | ) 72 | } 73 | else if let structDecl = declaration.as(StructDeclSyntax.self) { 74 | throw DiagnosticsError( 75 | diagnostics: [ 76 | DependencyValuesMacroDiagnostic.notExtension.diagnose(at: structDecl.structKeyword) 77 | ] 78 | ) 79 | } 80 | else { 81 | throw DiagnosticsError( 82 | diagnostics: [ 83 | DependencyValuesMacroDiagnostic.notExtension.diagnose(at: declaration) 84 | ] 85 | ) 86 | } 87 | } 88 | 89 | guard extensionDecl.extendedType.as(IdentifierTypeSyntax.self)?.name.text == "DependencyValues" else { 90 | throw DiagnosticsError( 91 | diagnostics: [ 92 | DependencyValuesMacroDiagnostic.notDependencyValues.diagnose(at: extensionDecl.extendedType) 93 | ] 94 | ) 95 | } 96 | 97 | guard case .argumentList(let arguments) = syntax.arguments, 98 | let type = arguments.first?.expression.as(MemberAccessExprSyntax.self)?.base?.as(DeclReferenceExprSyntax.self)?.baseName.text, 99 | arguments.count == 1 100 | else { 101 | throw DiagnosticsError( 102 | diagnostics: [ 103 | DependencyValuesMacroDiagnostic.invalidArgument.diagnose(at: declaration) 104 | ] 105 | ) 106 | } 107 | 108 | return (extensionDecl, type) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Sources/DependenciesMacroPlugin/DependencyValuesMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | import SwiftDiagnostics 5 | import SwiftCompilerPluginMessageHandling 6 | 7 | public struct DependencyValuesMacro: MemberMacro { 8 | public static func expansion( 9 | of node: AttributeSyntax, 10 | providingMembersOf declaration: some DeclGroupSyntax, 11 | in context: some MacroExpansionContext 12 | ) throws -> [DeclSyntax] { 13 | let (_, typeName) = try decodeExpansion( 14 | of: node, 15 | attachedTo: declaration, 16 | in: context 17 | ) 18 | 19 | let variableName = typeName.initialLowerCased() 20 | 21 | return [ 22 | DeclSyntax( 23 | """ 24 | var \(raw: variableName): \(raw: typeName) { 25 | get { self[\(raw: typeName).self] } 26 | set { self[\(raw: typeName).self] = newValue } 27 | } 28 | """ 29 | ) 30 | ] 31 | } 32 | } 33 | 34 | extension String { 35 | func initialLowerCased() -> String { 36 | return prefix(1).lowercased() + dropFirst() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PublicInitMacroPlugin/PublicInitMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | import SwiftDiagnostics 5 | import SwiftCompilerPluginMessageHandling 6 | 7 | public struct PublicInitMacro: MemberMacro { 8 | public static func expansion( 9 | of node: AttributeSyntax, 10 | providingMembersOf declaration: some DeclGroupSyntax, 11 | in context: some MacroExpansionContext 12 | ) throws -> [DeclSyntax] { 13 | let structDecl = try decodeExpansion(of: node, attachedTo: declaration, in: context) 14 | let storedPropertyBindings = structDecl.memberBlock.members 15 | .compactMap { $0.decl.as(VariableDeclSyntax.self) } 16 | .filter { !$0.modifiers.contains { $0.as(DeclModifierSyntax.self)?.name.text == "static" } } 17 | .map(\.bindings) 18 | .flatMap { $0 } 19 | .filter { $0.accessorBlock == nil } 20 | .compactMap { (binding: PatternBindingSyntax) -> PatternBindingSyntax? in 21 | guard let typeAnnotation = binding.typeAnnotation else { 22 | var newProperty = binding 23 | newProperty.typeAnnotation = TypeAnnotationSyntax( 24 | type: IdentifierTypeSyntax(name: " <#Type#> ") 25 | ) 26 | context.diagnose( 27 | .init( 28 | node: binding._syntaxNode, 29 | message: PublicInitMacroDiagnostic.noExplicitType, 30 | fixIts: [ 31 | FixIt( 32 | message: InsertTypeAnnotationFixItMessage(), 33 | changes: [ 34 | .replace(oldNode: Syntax(binding), newNode: Syntax(newProperty)) 35 | ] 36 | ) 37 | ] 38 | ) 39 | ) 40 | return nil 41 | } 42 | 43 | if let functionType = typeAnnotation.type.as(FunctionTypeSyntax.self) { 44 | return binding.with( 45 | \.typeAnnotation, 46 | typeAnnotation.with( 47 | \.type, 48 | TypeSyntax( 49 | AttributedTypeSyntax( 50 | attributes: AttributeListSyntax { 51 | AttributeSyntax.escaping 52 | }, 53 | baseType: functionType 54 | ) 55 | ) 56 | ) 57 | ) 58 | } else if let attributedType = typeAnnotation.type.as(AttributedTypeSyntax.self) { 59 | return binding.with( 60 | \.typeAnnotation, 61 | typeAnnotation.with( 62 | \.type, 63 | TypeSyntax( 64 | attributedType.with( 65 | \.attributes, AttributeListSyntax { 66 | attributedType.attributes 67 | AttributeSyntax.escaping 68 | } 69 | ) 70 | ) 71 | ) 72 | ) 73 | } else { 74 | return binding 75 | } 76 | } 77 | 78 | let arguments = storedPropertyBindings.map(\.description).joined(separator: ", \n") 79 | let assigns = storedPropertyBindings 80 | .compactMap { $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text } 81 | .map { "self.\($0) = \($0)" } 82 | .joined(separator: "\n") 83 | 84 | return [ 85 | DeclSyntax( 86 | """ 87 | public init( 88 | \(raw: arguments) 89 | ) { 90 | \(raw: assigns) 91 | } 92 | """ 93 | ) 94 | ] 95 | } 96 | } 97 | 98 | extension AttributeSyntax { 99 | static let escaping = AttributeSyntax( 100 | atSign: .atSignToken(), 101 | attributeName: IdentifierTypeSyntax( 102 | name: .identifier("escaping", 103 | trailingTrivia: .space) 104 | ) 105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /Sources/PublicInitMacroPlugin/PublicInitMacroDiagnostic.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxMacros 3 | import SwiftDiagnostics 4 | 5 | public enum PublicInitMacroDiagnostic { 6 | case noExplicitType 7 | case notStruct 8 | case notPublic 9 | } 10 | 11 | extension PublicInitMacroDiagnostic: DiagnosticMessage { 12 | func diagnose(at node: some SyntaxProtocol) -> Diagnostic { 13 | Diagnostic(node: Syntax(node), message: self) 14 | } 15 | 16 | public var message: String { 17 | switch self { 18 | case .noExplicitType: 19 | "PublicInit Macro required stored properties provide explicit typed annotations." 20 | case .notStruct: 21 | "PublicInit Macro can only be applied to struct." 22 | case .notPublic: 23 | "PublicInit Macro can only be applied to public struct." 24 | } 25 | } 26 | 27 | public var severity: DiagnosticSeverity { .error } 28 | 29 | public var diagnosticID: MessageID { 30 | switch self { 31 | case .noExplicitType: 32 | MessageID(domain: "PublicInitMacroDiagnostic", id: "noExplicitType") 33 | case .notStruct: 34 | MessageID(domain: "PublicInitMacroDiagnostic", id: "notStruct") 35 | case .notPublic: 36 | MessageID(domain: "PublicInitMacroDiagnostic", id: "notPublic") 37 | } 38 | } 39 | } 40 | 41 | public extension PublicInitMacro { 42 | static func decodeExpansion( 43 | of syntax: AttributeSyntax, 44 | attachedTo declaration: some DeclGroupSyntax, 45 | in context: some MacroExpansionContext 46 | ) throws -> StructDeclSyntax { 47 | guard let structDecl = declaration.as(StructDeclSyntax.self) else { 48 | if let actorDecl = declaration.as(ActorDeclSyntax.self) { 49 | throw DiagnosticsError( 50 | diagnostics: [ 51 | PublicInitMacroDiagnostic.notStruct.diagnose(at: actorDecl.actorKeyword) 52 | ] 53 | ) 54 | } 55 | else if let classDecl = declaration.as(ClassDeclSyntax.self) { 56 | throw DiagnosticsError( 57 | diagnostics: [ 58 | PublicInitMacroDiagnostic.notStruct.diagnose(at: classDecl.classKeyword) 59 | ] 60 | ) 61 | } 62 | else if let enumDecl = declaration.as(EnumDeclSyntax.self) { 63 | throw DiagnosticsError( 64 | diagnostics: [ 65 | PublicInitMacroDiagnostic.notStruct.diagnose(at: enumDecl.enumKeyword) 66 | ] 67 | ) 68 | } 69 | else { 70 | throw DiagnosticsError( 71 | diagnostics: [ 72 | PublicInitMacroDiagnostic.notStruct.diagnose(at: declaration) 73 | ] 74 | ) 75 | } 76 | } 77 | 78 | guard structDecl.modifiers.map(\.name.text).contains("public") else { 79 | throw DiagnosticsError( 80 | diagnostics: [ 81 | PublicInitMacroDiagnostic.notPublic.diagnose(at: declaration) 82 | ] 83 | ) 84 | } 85 | 86 | return structDecl 87 | } 88 | } 89 | 90 | struct InsertTypeAnnotationFixItMessage: FixItMessage { 91 | var message = "Insert type annotation." 92 | var fixItID = MessageID( 93 | domain: "PublicInitMacro", id: "type-annotation" 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /Sources/PublicInitMacroPlugin/PublicInitMacroPlugin.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntaxMacros 3 | 4 | @main 5 | struct PublicInitMacroPlugin: CompilerPlugin { 6 | let providingMacros: [Macro.Type] = [ 7 | PublicInitMacro.self 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /Tests/DependenciesMacroTests/DependenciesMacroTests.swift: -------------------------------------------------------------------------------- 1 | import DependenciesMacro 2 | import DependenciesMacroPlugin 3 | import MacroTesting 4 | import XCTest 5 | 6 | final class DependenciesMacroTests: XCTestCase { 7 | override func invokeTest() { 8 | withMacroTesting( 9 | macros: ["Dependencies": DependenciesMacro.self] 10 | ) { 11 | super.invokeTest() 12 | } 13 | } 14 | 15 | func testDiagnostic() { 16 | assertMacro { 17 | """ 18 | @Dependencies 19 | class Test {} 20 | """ 21 | } matches: { 22 | """ 23 | @Dependencies 24 | class Test {} 25 | ┬──── 26 | ╰─ 🛑 Dependencies Macro can only be applied to struct. 27 | """ 28 | } 29 | assertMacro { 30 | """ 31 | @Dependencies 32 | enum Test {} 33 | """ 34 | } matches: { 35 | """ 36 | @Dependencies 37 | enum Test {} 38 | ┬─── 39 | ╰─ 🛑 Dependencies Macro can only be applied to struct. 40 | """ 41 | } 42 | assertMacro { 43 | """ 44 | @Dependencies 45 | actor Test {} 46 | """ 47 | } matches: { 48 | """ 49 | @Dependencies 50 | actor Test {} 51 | ┬──── 52 | ╰─ 🛑 Dependencies Macro can only be applied to struct. 53 | """ 54 | } 55 | } 56 | 57 | func testMacro() { 58 | assertMacro { 59 | """ 60 | @Dependencies 61 | public struct TestClient { 62 | static let staticValue = 0 63 | let a: String 64 | let b: () -> Void 65 | let c: @Sendable () -> Void 66 | let d: @Sendable () async -> Void 67 | let e: @Sendable () async throws -> Void 68 | let f: @Sendable (String) async throws -> String 69 | let g: @Sendable (_ arg: String) async throws -> String 70 | var h: @Sendable (_ arg1: String, _ arg2: String) async throws -> String 71 | var i: @Sendable (String, Int) async throws -> String 72 | } 73 | """ 74 | } matches: { 75 | #""" 76 | public struct TestClient { 77 | static let staticValue = 0 78 | let a: String 79 | let b: () -> Void 80 | let c: @Sendable () -> Void 81 | let d: @Sendable () async -> Void 82 | let e: @Sendable () async throws -> Void 83 | let f: @Sendable (String) async throws -> String 84 | let g: @Sendable (_ arg: String) async throws -> String 85 | var h: @Sendable (_ arg1: String, _ arg2: String) async throws -> String 86 | var i: @Sendable (String, Int) async throws -> String 87 | } 88 | 89 | extension TestClient: TestDependencyKey { 90 | public static let testValue = TestClient( 91 | a: unimplemented("\(Self.self).a"), 92 | b: unimplemented("\(Self.self).b"), 93 | c: unimplemented("\(Self.self).c"), 94 | d: unimplemented("\(Self.self).d"), 95 | e: unimplemented("\(Self.self).e"), 96 | f: unimplemented("\(Self.self).f"), 97 | g: unimplemented("\(Self.self).g"), 98 | h: unimplemented("\(Self.self).h"), 99 | i: unimplemented("\(Self.self).i") 100 | ) 101 | } 102 | """# 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Tests/DependenciesMacroTests/DependencyValueMacroTests.swift: -------------------------------------------------------------------------------- 1 | import DependenciesMacro 2 | import DependenciesMacroPlugin 3 | import MacroTesting 4 | import XCTest 5 | 6 | final class DependencyValueMacroTests: XCTestCase { 7 | override func invokeTest() { 8 | withMacroTesting( 9 | macros: ["DependencyValue": DependencyValuesMacro.self] 10 | ) { 11 | super.invokeTest() 12 | } 13 | } 14 | 15 | func testDiagnostic() { 16 | assertMacro { 17 | """ 18 | @DependencyValue(TestClient.self) 19 | public struct DependencyValues {} 20 | """ 21 | } matches: { 22 | """ 23 | @DependencyValue(TestClient.self) 24 | public struct DependencyValues {} 25 | ┬───── 26 | ╰─ 🛑 DependencyValue Macro can only be applied to extension. 27 | """ 28 | } 29 | 30 | assertMacro { 31 | """ 32 | @DependencyValue(TestClient.self) 33 | public class DependencyValues {} 34 | """ 35 | } matches: { 36 | """ 37 | @DependencyValue(TestClient.self) 38 | public class DependencyValues {} 39 | ┬──── 40 | ╰─ 🛑 DependencyValue Macro can only be applied to extension. 41 | """ 42 | } 43 | 44 | assertMacro { 45 | """ 46 | @DependencyValue(TestClient.self) 47 | public actor DependencyValues {} 48 | """ 49 | } matches: { 50 | """ 51 | @DependencyValue(TestClient.self) 52 | public actor DependencyValues {} 53 | ┬──── 54 | ╰─ 🛑 DependencyValue Macro can only be applied to extension. 55 | """ 56 | } 57 | 58 | assertMacro { 59 | """ 60 | @DependencyValue(TestClient.self) 61 | public enum DependencyValues {} 62 | """ 63 | } matches: { 64 | """ 65 | @DependencyValue(TestClient.self) 66 | public enum DependencyValues {} 67 | ┬─── 68 | ╰─ 🛑 DependencyValue Macro can only be applied to extension. 69 | """ 70 | } 71 | 72 | assertMacro { 73 | """ 74 | @DependencyValue(TestClient.self) 75 | public extension Test {} 76 | """ 77 | } matches: { 78 | """ 79 | @DependencyValue(TestClient.self) 80 | public extension Test {} 81 | ┬─── 82 | ╰─ 🛑 DependencyValue Macro can only be applied to extension of DependencyValues 83 | """ 84 | } 85 | } 86 | 87 | func testMacro() { 88 | assertMacro { 89 | """ 90 | @DependencyValue(TestClient.self) 91 | public extension DependencyValues {} 92 | """ 93 | } matches: { 94 | """ 95 | public extension DependencyValues { 96 | 97 | var testClient: TestClient { 98 | get { 99 | self [TestClient.self] 100 | } 101 | set { 102 | self [TestClient.self] = newValue 103 | } 104 | }} 105 | """ 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Tests/PublicInitMacroTests/PublicInitMacroTests.swift: -------------------------------------------------------------------------------- 1 | import DependenciesMacro 2 | import PublicInitMacroPlugin 3 | import MacroTesting 4 | import XCTest 5 | 6 | final class PublicInitMacroTests: XCTestCase { 7 | override func invokeTest() { 8 | withMacroTesting( 9 | macros: ["PublicInit": PublicInitMacro.self] 10 | ) { 11 | super.invokeTest() 12 | } 13 | } 14 | 15 | func testFixIt() { 16 | assertMacro(applyFixIts: true) { 17 | """ 18 | @PublicInit 19 | public struct Test { 20 | var a = true 21 | } 22 | """ 23 | } matches: { 24 | """ 25 | @PublicInit 26 | public struct Test { 27 | var a : <#Type#> = true 28 | } 29 | """ 30 | } 31 | } 32 | 33 | func testDiagnostic() { 34 | assertMacro { 35 | """ 36 | @PublicInit 37 | struct Test {} 38 | """ 39 | } matches: { 40 | """ 41 | @PublicInit 42 | ╰─ 🛑 PublicInit Macro can only be applied to public struct. 43 | struct Test {} 44 | """ 45 | } 46 | 47 | assertMacro { 48 | """ 49 | @PublicInit 50 | public final class Test {} 51 | """ 52 | } matches: { 53 | """ 54 | @PublicInit 55 | public final class Test {} 56 | ┬──── 57 | ╰─ 🛑 PublicInit Macro can only be applied to struct. 58 | """ 59 | } 60 | 61 | assertMacro { 62 | """ 63 | @PublicInit 64 | public actor Test {} 65 | """ 66 | } matches: { 67 | """ 68 | @PublicInit 69 | public actor Test {} 70 | ┬──── 71 | ╰─ 🛑 PublicInit Macro can only be applied to struct. 72 | """ 73 | } 74 | 75 | assertMacro { 76 | """ 77 | @PublicInit 78 | public enum Test {} 79 | """ 80 | } matches: { 81 | """ 82 | @PublicInit 83 | public enum Test {} 84 | ┬─── 85 | ╰─ 🛑 PublicInit Macro can only be applied to struct. 86 | """ 87 | } 88 | } 89 | 90 | func testMacro() { 91 | assertMacro { 92 | """ 93 | @PublicInit 94 | public struct Test { 95 | static let staticValue = 0 96 | let a: String 97 | let b: () -> Void 98 | let c: @Sendable () -> Void 99 | let d: @Sendable () async -> Void 100 | let e: @Sendable () async throws -> Void 101 | let f: @Sendable (String) async throws -> String 102 | let g: @Sendable (_ arg: String) async throws -> String 103 | var h: @Sendable (_ arg1: String, _ arg2: String) async throws -> String 104 | var i: @Sendable (String, Int) async throws -> String 105 | var j: String = "" 106 | } 107 | """ 108 | } matches: { 109 | """ 110 | public struct Test { 111 | static let staticValue = 0 112 | let a: String 113 | let b: () -> Void 114 | let c: @Sendable () -> Void 115 | let d: @Sendable () async -> Void 116 | let e: @Sendable () async throws -> Void 117 | let f: @Sendable (String) async throws -> String 118 | let g: @Sendable (_ arg: String) async throws -> String 119 | var h: @Sendable (_ arg1: String, _ arg2: String) async throws -> String 120 | var i: @Sendable (String, Int) async throws -> String 121 | var j: String = "" 122 | 123 | public init( 124 | a: String, 125 | b: @escaping () -> Void, 126 | c: @Sendable @escaping () -> Void, 127 | d: @Sendable @escaping () async -> Void, 128 | e: @Sendable @escaping () async throws -> Void, 129 | f: @Sendable @escaping (String) async throws -> String, 130 | g: @Sendable @escaping (_ arg: String) async throws -> String, 131 | h: @Sendable @escaping (_ arg1: String, _ arg2: String) async throws -> String, 132 | i: @Sendable @escaping (String, Int) async throws -> String, 133 | j: String = "" 134 | ) { 135 | self.a = a 136 | self.b = b 137 | self.c = c 138 | self.d = d 139 | self.e = e 140 | self.f = f 141 | self.g = g 142 | self.h = h 143 | self.i = i 144 | self.j = j 145 | } 146 | } 147 | """ 148 | } 149 | } 150 | } 151 | --------------------------------------------------------------------------------