├── .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 |
--------------------------------------------------------------------------------