├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Examples
├── .gitignore
├── .swiftpm
│ └── xcode
│ │ └── package.xcworkspace
│ │ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
├── LICENSE.txt
├── Package.swift
├── Sources
│ ├── Example
│ │ └── Example.swift
│ ├── ExampleClient
│ │ └── main.swift
│ └── ExampleMacros
│ │ └── ExampleMacro.swift
└── Tests
│ └── StringifyTests
│ └── StringifyTests.swift
├── Package.swift
├── README.md
├── Sources
└── SwiftCompilerPlugin
│ ├── AttachedMacro.swift
│ ├── CompilerPlugin.swift
│ ├── CompilerPluginMessageHandler+Macros.swift
│ ├── CompilerPluginMessageHandler.swift
│ ├── FreestandingMacro.swift
│ ├── Macro.swift
│ └── PluginMessages.swift
└── Tests
└── SwiftCompilerPluginTests
└── SwiftCompilerPluginTests.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 |
--------------------------------------------------------------------------------
/Examples/.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 |
--------------------------------------------------------------------------------
/Examples/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Examples/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
204 |
205 | ## Runtime Library Exception to the Apache 2.0 License: ##
206 |
207 |
208 | As an exception, if you use this Software to compile your source code and
209 | portions of this Software are embedded into the binary product as a result,
210 | you may redistribute such product without providing attribution as would
211 | otherwise be required by Sections 4(a), 4(b) and 4(d) of the License.
212 |
--------------------------------------------------------------------------------
/Examples/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: "Examples",
9 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)],
10 | products: [
11 | // Products define the executables and libraries a package produces, making them visible to other packages.
12 | .library(
13 | name: "Example",
14 | targets: ["Example"]
15 | ),
16 | .executable(
17 | name: "ExampleClient",
18 | targets: ["ExampleClient"]
19 | ),
20 | ],
21 | dependencies: [
22 | // Depend on the latest Swift 5.9 prerelease of SwiftSyntax
23 | .package(name: "SwiftCompilerPlugin", path: ".."),
24 | ],
25 | targets: [
26 | // Targets are the basic building blocks of a package, defining a module or a test suite.
27 | // Targets can depend on other targets in this package and products from dependencies.
28 | // Macro implementation that performs the source transformation of a macro.
29 | .macro(
30 | name: "ExampleMacros",
31 | dependencies: [
32 | .product(name: "SwiftCompilerPlugin", package: "SwiftCompilerPlugin")
33 | ]
34 | ),
35 |
36 | // Library that exposes a macro as part of its API, which is used in client programs.
37 | .target(name: "Example", dependencies: ["ExampleMacros"]),
38 |
39 | // A client of the library, which is able to use the macro in its own code.
40 | .executableTarget(name: "ExampleClient", dependencies: ["Example"]),
41 |
42 | // A test target used to develop the macro implementation.
43 | .testTarget(
44 | name: "ExampleTests",
45 | dependencies: [
46 | "ExampleMacros",
47 | // .product(name: "SwiftSyntaxMacrosTestSupport", package: "SwiftCompilerPlugin"),
48 | ]
49 | ),
50 | ]
51 | )
52 |
--------------------------------------------------------------------------------
/Examples/Sources/Example/Example.swift:
--------------------------------------------------------------------------------
1 | // The Swift Programming Language
2 | // https://docs.swift.org/swift-book
3 |
4 | /// A macro that produces both a value and a string containing the
5 | /// source code that generated the value. For example,
6 | ///
7 | /// #Example(x + y)
8 | ///
9 | /// produces a tuple `(x + y, "x + y")`.
10 |
11 | // Creates a piece of code that returns a value
12 | @freestanding(expression)
13 | public macro ExpressionMacro(_ value: T) -> (T, String) = #externalMacro(module: "ExampleMacros", type: "ExpressionMacro")
14 |
15 | // Creates one or more declarations
16 | @freestanding(declaration)
17 | public macro DeclarationMacro(_ message: String) = #externalMacro(module: "ExampleMacros", type: "DeclarationMacro")
18 |
19 | // Adds new declarations alongside the declaration it’s applied to
20 | @attached(peer, names: overloaded)
21 | public macro PeerMacro() = #externalMacro(module: "ExampleMacros", type: "PeerMacro")
22 |
23 | // Adds accessors to a property
24 | @attached(accessor)
25 | public macro AccessorMacro(_ key: String? = nil) = #externalMacro(module: "ExampleMacros", type: "AccessorMacro")
26 |
27 | // Adds attributes to the declarations in the type/extension it’s applied to
28 | @attached(memberAttribute)
29 | public macro MemberAttributeMacro() = #externalMacro(module: "ExampleMacros", type: "MemberAttributeMacro")
30 |
31 | // Adds new declarations inside the type/extension it’s applied to
32 | @attached(member)
33 | public macro MemberMacro() = #externalMacro(module: "ExampleMacros", type: "MemberMacro")
34 |
35 | // Adds conformances to the type/extension it’s applied to
36 | @attached(extension, conformances:Codable)
37 | public macro ExtensionMacro() = #externalMacro(module: "ExampleMacros", type: "ExtensionMacro")
38 |
--------------------------------------------------------------------------------
/Examples/Sources/ExampleClient/main.swift:
--------------------------------------------------------------------------------
1 | import Example
2 | import Foundation
3 |
4 | let a = 17
5 | let b = 25
6 |
7 | let (result, code) = #ExpressionMacro(a + b)
8 |
9 | print("The value \(result) was produced by the code \"\(code)\"")
10 |
11 | #DeclarationMacro("unsupported configuration")
12 |
13 |
14 | // https://jllnmercier.medium.com/swift-peer-macros-1cb1469d83fe
15 | class Webservice {
16 | @PeerMacro
17 | func fetch(completion: (Data) -> Void) {
18 | completion(Data())
19 | }
20 | }
21 |
22 | @MemberAttributeMacro
23 | struct Animal {
24 | var age = 10
25 | var name: String
26 | }
27 |
28 | class User {
29 | @AccessorMacro var username = "Taylor"
30 | var age = 26
31 | }
32 |
33 |
34 | extension User {
35 | @ExtensionMacro
36 | private struct B {}
37 | }
38 |
39 | print(User().username)
40 |
--------------------------------------------------------------------------------
/Examples/Sources/ExampleMacros/ExampleMacro.swift:
--------------------------------------------------------------------------------
1 | import SwiftCompilerPlugin
2 |
3 | /// Implementation of the `Example` macro, which takes an expression
4 | /// of any type and produces a tuple containing the value of that expression
5 | /// and the source code that produced the value. For example
6 | ///
7 | /// #Example(x + y)
8 | ///
9 | /// will expand to
10 | ///
11 | /// (x + y, "x + y")
12 | public struct ExpressionMacro: FreestandingMacro {
13 | public static func expandFreestandingMacro(
14 | macro: PluginMessage.MacroReference,
15 | macroRole: PluginMessage.MacroRole?,
16 | discriminator: String,
17 | expandingSyntax: PluginMessage.Syntax
18 | ) throws -> (expandedSource: String?,
19 | diagnostics: [PluginMessage.Diagnostic]) {
20 | let source = expandingSyntax.source
21 | let start = source.index(source.startIndex, offsetBy: "#ExpressionMacro(".count)
22 | let end = source.index(source.endIndex, offsetBy: 0 - ")".count)
23 | let argument = source[start.. (expandedSource: String?, diagnostics: [SwiftCompilerPlugin.PluginMessage.Diagnostic]) {
31 | return ("", [])
32 | }
33 | }
34 |
35 | public struct PeerMacro: AttachedMacro {
36 | public static func expandAttachedMacro(macro: SwiftCompilerPlugin.PluginMessage.MacroReference, macroRole: SwiftCompilerPlugin.PluginMessage.MacroRole, discriminator: String, attributeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, declSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, parentDeclSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, extendedTypeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, conformanceListSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?) throws -> (expandedSource: String?, diagnostics: [SwiftCompilerPlugin.PluginMessage.Diagnostic]) {
37 | return ("", [])
38 | }
39 | }
40 |
41 | public struct AccessorMacro: AttachedMacro {
42 | public static func expandAttachedMacro(macro: SwiftCompilerPlugin.PluginMessage.MacroReference, macroRole: SwiftCompilerPlugin.PluginMessage.MacroRole, discriminator: String, attributeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, declSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, parentDeclSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, extendedTypeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, conformanceListSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?) throws -> (expandedSource: String?, diagnostics: [SwiftCompilerPlugin.PluginMessage.Diagnostic]) {
43 | return ("get { \"some value\"} set {}", [])
44 | }
45 | }
46 |
47 | public struct MemberAttributeMacro: AttachedMacro {
48 | public static func expandAttachedMacro(macro: SwiftCompilerPlugin.PluginMessage.MacroReference, macroRole: SwiftCompilerPlugin.PluginMessage.MacroRole, discriminator: String, attributeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, declSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, parentDeclSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, extendedTypeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, conformanceListSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?) throws -> (expandedSource: String?, diagnostics: [SwiftCompilerPlugin.PluginMessage.Diagnostic]) {
49 | return ("", [])
50 | }
51 | }
52 |
53 | public struct MemberMacro: AttachedMacro {
54 | public static func expandAttachedMacro(macro: SwiftCompilerPlugin.PluginMessage.MacroReference, macroRole: SwiftCompilerPlugin.PluginMessage.MacroRole, discriminator: String, attributeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, declSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, parentDeclSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, extendedTypeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, conformanceListSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?) throws -> (expandedSource: String?, diagnostics: [SwiftCompilerPlugin.PluginMessage.Diagnostic]) {
55 | return ("", [])
56 | }
57 | }
58 |
59 | public struct ExtensionMacro: AttachedMacro {
60 | public static func expandAttachedMacro(macro: SwiftCompilerPlugin.PluginMessage.MacroReference, macroRole: SwiftCompilerPlugin.PluginMessage.MacroRole, discriminator: String, attributeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, declSyntax: SwiftCompilerPlugin.PluginMessage.Syntax, parentDeclSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, extendedTypeSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?, conformanceListSyntax: SwiftCompilerPlugin.PluginMessage.Syntax?) throws -> (expandedSource: String?, diagnostics: [SwiftCompilerPlugin.PluginMessage.Diagnostic]) {
61 | return ("", [])
62 | }
63 | }
64 |
65 | @main
66 | struct ExamplePlugin: CompilerPlugin {
67 |
68 |
69 | let providingMacros: [Macro.Type] = [
70 | ExpressionMacro.self,
71 | DeclarationMacro.self,
72 | PeerMacro.self,
73 | AccessorMacro.self,
74 | MemberAttributeMacro.self,
75 | MemberMacro.self,
76 | ExtensionMacro.self,
77 | ]
78 | }
79 |
--------------------------------------------------------------------------------
/Examples/Tests/StringifyTests/StringifyTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | // Macro implementations build for the host, so the corresponding module is not available when cross-compiling. Cross-compiled tests may still make use of the macro itself in end-to-end tests.
4 | //#if canImport(ExampleMacros)
5 | //import ExampleMacros
6 | //
7 | //let testMacros: [String: Macro.Type] = [
8 | // "Example": ExampleMacro.self,
9 | //]
10 | //#endif
11 |
12 | final class ExampleTests: XCTestCase {
13 | // func testMacro() throws {
14 | // #if canImport(ExampleMacros)
15 | // assertMacroExpansion(
16 | // """
17 | // #Example(a + b)
18 | // """,
19 | // expandedSource: """
20 | // (a + b, "a + b")
21 | // """,
22 | // macros: testMacros
23 | // )
24 | // #else
25 | // throw XCTSkip("macros are only supported when running tests for the host platform")
26 | // #endif
27 | // }
28 | //
29 | // func testMacroWithStringLiteral() throws {
30 | // #if canImport(ExampleMacros)
31 | // assertMacroExpansion(
32 | // #"""
33 | // #Example("Hello, \(name)")
34 | // """#,
35 | // expandedSource: #"""
36 | // ("Hello, \(name)", #""Hello, \(name)""#)
37 | // """#,
38 | // macros: testMacros
39 | // )
40 | // #else
41 | // throw XCTSkip("macros are only supported when running tests for the host platform")
42 | // #endif
43 | // }
44 | }
45 |
--------------------------------------------------------------------------------
/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 |
6 | let package = Package(
7 | name: "SwiftCompilerPlugin",
8 | products: [
9 | // Products define the executables and libraries a package produces, making them visible to other packages.
10 | .library(
11 | name: "SwiftCompilerPlugin",
12 | targets: ["SwiftCompilerPlugin"]),
13 | ],
14 | targets: [
15 | // Targets are the basic building blocks of a package, defining a module or a test suite.
16 | // Targets can depend on other targets in this package and products from dependencies.
17 | .target(
18 | name: "SwiftCompilerPlugin"),
19 | .testTarget(
20 | name: "SwiftCompilerPluginTests",
21 | dependencies: ["SwiftCompilerPlugin"]),
22 | ]
23 | )
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwiftCompilerPlugin
2 | Swift macros without requiring swift-syntax
3 |
4 | This project extracts the bare minimum from [swift-syntax](https://github.com/apple/swift-syntax) to be able to build a macro in Xcode 15. All you get are the strings, no fancy object trees.
5 |
6 | If nothing else it will allow you to see what is *really* sent to your plugin before it's converted to structures etc.
7 |
8 | ### Example
9 |
10 | Here's the classic example from the default Xcode template which implements `stringify` renamed here to be `ExpressionMacro`.
11 |
12 | ```swift
13 | @freestanding(expression)
14 | public macro ExpressionMacro(_ value: T) -> (T, String) = #externalMacro(module: "ExampleMacros", type: "ExpressionMacro")
15 |
16 | ...
17 |
18 | let (result, code) = #ExpressionMacro(a + b)
19 | (a + b, "a + b")
20 | /* {
21 | "expandFreestandingMacro" : {
22 | "macro" : {
23 | "moduleName" : "ExampleMacros",
24 | "name" : "ExpressionMacro",
25 | "typeName" : "ExpressionMacro"
26 | },
27 | "syntax" : {
28 | "kind" : "expression",
29 | "location" : {
30 | "fileID" : "ExampleClient\/main.swift",
31 | "fileName" : "•••/SwiftCompilerPlugin\/Examples\/Sources\/ExampleClient\/main.swift",
32 | "line" : 7,
33 | "offset" : 78,
34 | "column" : 22
35 | },
36 | "source" : "#ExpressionMacro(a + b)"
37 | },
38 | "discriminator" : "$s13ExampleClient33_C7C48FD1C44CD1E5F2188E7B86F2D462Ll15ExpressionMacrofMf_",
39 | "macroRole" : "expression"
40 | }
41 | }*/
42 | ```
43 |
--------------------------------------------------------------------------------
/Sources/SwiftCompilerPlugin/AttachedMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
3 | // Licensed under Apache License v2.0 with Runtime Library Exception
4 | //
5 | // See https://swift.org/LICENSE.txt for license information
6 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
7 | //
8 | //===----------------------------------------------------------------------===//
9 |
10 | /// Describes a macro that is attached, meaning that it is used with
11 | /// custom attribute syntax and attached to another entity.
12 | public protocol AttachedMacro: Macro {
13 | static func expandAttachedMacro(
14 | macro: PluginMessage.MacroReference,
15 | macroRole: PluginMessage.MacroRole,
16 | discriminator: String,
17 | attributeSyntax: PluginMessage.Syntax,
18 | declSyntax: PluginMessage.Syntax,
19 | parentDeclSyntax: PluginMessage.Syntax?,
20 | extendedTypeSyntax: PluginMessage.Syntax?,
21 | conformanceListSyntax: PluginMessage.Syntax?
22 | ) throws -> (
23 | expandedSource: String?,
24 | diagnostics: [PluginMessage.Diagnostic]
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/SwiftCompilerPlugin/CompilerPlugin.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift open source project
4 | //
5 | // Copyright (c) 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See http://swift.org/LICENSE.txt for license information
9 | // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 | // NOTE: This basic plugin mechanism is mostly copied from
13 | // https://github.com/apple/swift-package-manager/blob/main/Sources/PackagePlugin/Plugin.swift
14 |
15 | @_implementationOnly import Foundation
16 | #if os(Windows)
17 | @_implementationOnly import ucrt
18 | #endif
19 |
20 | //
21 | // This source file contains the main entry point for compiler plugins.
22 | // A plugin receives messages from the "plugin host" (typically
23 | // 'swift-frontend'), and sends back messages in return based on its actions.
24 | //
25 | // Depending on the platform, plugins are invoked in a sandbox that blocks
26 | // network access and prevents any file system changes.
27 | //
28 | // The host process and the plugin communicate using messages in the form of
29 | // length-prefixed JSON-encoded Swift enums. The host sends messages to the
30 | // plugin through its standard-input pipe, and receives messages through the
31 | // plugin's standard-output pipe. The plugin's standard-error is considered
32 | // to be free-form textual console output.
33 | //
34 | // Within the plugin process, `stdout` is redirected to `stderr` so that print
35 | // statements from the plugin are treated as plain-text output, and `stdin` is
36 | // closed so that any attempts by the plugin logic to read from console result
37 | // in errors instead of blocking the process. The original `stdin` and `stdout`
38 | // are duplicated for use as messaging pipes, and are not directly used by the
39 | // plugin logic.
40 | //
41 | // The exit code of the plugin process indicates whether the plugin invocation
42 | // is considered successful. A failure result should also be accompanied by an
43 | // emitted error diagnostic, so that errors are understandable by the user.
44 | //
45 | // Using standard input and output streams for messaging avoids having to make
46 | // allowances in the sandbox for other channels of communication, and seems a
47 | // more portable approach than many of the alternatives. This is all somewhat
48 | // temporary in any case — in the long term, something like distributed actors
49 | // or something similar can hopefully replace the custom messaging.
50 | //
51 | // Usage:
52 | // struct MyPlugin: CompilerPlugin {
53 | // var providingMacros: [Macros.Type] = [
54 | // StringifyMacro.self
55 | // ]
56 | public protocol CompilerPlugin {
57 | init()
58 |
59 | var providingMacros: [Macro.Type] { get }
60 | }
61 |
62 | extension CompilerPlugin {
63 | func resolveMacro(moduleName: String, typeName: String) -> Macro.Type? {
64 | let qualifedName = "\(moduleName).\(typeName)"
65 |
66 | for type in providingMacros {
67 | // FIXME: Is `String(reflecting:)` stable?
68 | // Getting the module name and type name should be more robust.
69 | let name = String(reflecting: type)
70 | if name == qualifedName {
71 | return type
72 | }
73 | }
74 | return nil
75 | }
76 |
77 | // @testable
78 | public func _resolveMacro(moduleName: String, typeName: String) -> Macro.Type? {
79 | resolveMacro(moduleName: moduleName, typeName: typeName)
80 | }
81 | }
82 |
83 | struct MacroProviderAdapter: PluginProvider {
84 | let plugin: Plugin
85 | init(plugin: Plugin) {
86 | self.plugin = plugin
87 | }
88 | func resolveMacro(moduleName: String, typeName: String) -> Macro.Type? {
89 | plugin.resolveMacro(moduleName: moduleName, typeName: typeName)
90 | }
91 | }
92 |
93 | extension CompilerPlugin {
94 |
95 | /// Main entry point of the plugin — sets up a communication channel with
96 | /// the plugin host and runs the main message loop.
97 | public static func main() throws {
98 | // Duplicate the `stdin` file descriptor, which we will then use for
99 | // receiving messages from the plugin host.
100 | let inputFD = dup(fileno(stdin))
101 | guard inputFD >= 0 else {
102 | internalError("Could not duplicate `stdin`: \(describe(errno: errno)).")
103 | }
104 |
105 | // Having duplicated the original standard-input descriptor, we close
106 | // `stdin` so that attempts by the plugin to read console input (which
107 | // are usually a mistake) return errors instead of blocking.
108 | guard close(fileno(stdin)) >= 0 else {
109 | internalError("Could not close `stdin`: \(describe(errno: errno)).")
110 | }
111 |
112 | // Duplicate the `stdout` file descriptor, which we will then use for
113 | // sending messages to the plugin host.
114 | let outputFD = dup(fileno(stdout))
115 | guard outputFD >= 0 else {
116 | internalError("Could not dup `stdout`: \(describe(errno: errno)).")
117 | }
118 |
119 | // Having duplicated the original standard-output descriptor, redirect
120 | // `stdout` to `stderr` so that all free-form text output goes there.
121 | guard dup2(fileno(stderr), fileno(stdout)) >= 0 else {
122 | internalError("Could not dup2 `stdout` to `stderr`: \(describe(errno: errno)).")
123 | }
124 |
125 | // Turn off full buffering so printed text appears as soon as possible.
126 | // Windows is much less forgiving than other platforms. If line
127 | // buffering is enabled, we must provide a buffer and the size of the
128 | // buffer. As a result, on Windows, we completely disable all
129 | // buffering, which means that partial writes are possible.
130 | #if os(Windows)
131 | setvbuf(stdout, nil, _IONBF, 0)
132 | #else
133 | setvbuf(stdout, nil, _IOLBF, 0)
134 | #endif
135 |
136 | // Open a message channel for communicating with the plugin host.
137 | let connection = PluginHostConnection(
138 | inputStream: FileHandle(fileDescriptor: inputFD),
139 | outputStream: FileHandle(fileDescriptor: outputFD)
140 | )
141 |
142 | // Handle messages from the host until the input stream is closed,
143 | // indicating that we're done.
144 | let provider = MacroProviderAdapter(plugin: Self())
145 | let impl = CompilerPluginMessageHandler(connection: connection, provider: provider)
146 | do {
147 | try impl.main()
148 | } catch {
149 | // Emit a diagnostic and indicate failure to the plugin host,
150 | // and exit with an error code.
151 | internalError(String(describing: error))
152 | }
153 | }
154 |
155 | // Private function to report internal errors and then exit.
156 | fileprivate static func internalError(_ message: String) -> Never {
157 | fputs("Internal Error: \(message)\n", stderr)
158 | exit(1)
159 | }
160 |
161 | // Private function to construct an error message from an `errno` code.
162 | fileprivate static func describe(errno: Int32) -> String {
163 | if let cStr = strerror(errno) { return String(cString: cStr) }
164 | return String(describing: errno)
165 | }
166 | }
167 |
168 | internal struct PluginHostConnection: MessageConnection {
169 | let inputStream: FileHandle
170 | let outputStream: FileHandle
171 |
172 | func sendMessage(_ message: TX) throws {
173 | // Encode the message as JSON.
174 | let payload = try JSONEncoder().encode(message)
175 |
176 | // Write the header (a 64-bit length field in little endian byte order).
177 | var count = UInt64(payload.count).littleEndian
178 | let header = Swift.withUnsafeBytes(of: &count) { Data($0) }
179 | precondition(header.count == 8)
180 |
181 | // Write the header and payload.
182 | try outputStream._write(contentsOf: header)
183 | try outputStream._write(contentsOf: payload)
184 | }
185 |
186 | func waitForNextMessage(_ ty: RX.Type) throws -> RX? {
187 | // Read the header (a 64-bit length field in little endian byte order).
188 | guard
189 | let header = try inputStream._read(upToCount: 8),
190 | header.count != 0
191 | else {
192 | return nil
193 | }
194 | guard header.count == 8 else {
195 | throw PluginMessageError.truncatedHeader
196 | }
197 |
198 | // Decode the count.
199 | let count = header.withUnsafeBytes {
200 | UInt64(littleEndian: $0.load(as: UInt64.self))
201 | }
202 | guard count >= 2 else {
203 | throw PluginMessageError.invalidPayloadSize
204 | }
205 |
206 | // Read the JSON payload.
207 | guard
208 | let payload = try inputStream._read(upToCount: Int(count)),
209 | payload.count == count
210 | else {
211 | throw PluginMessageError.truncatedPayload
212 | }
213 |
214 | // Decode and return the message.
215 | return try JSONDecoder().decode(RX.self, from: payload)
216 | }
217 |
218 | enum PluginMessageError: Swift.Error {
219 | case truncatedHeader
220 | case invalidPayloadSize
221 | case truncatedPayload
222 | }
223 | }
224 |
225 | private extension FileHandle {
226 | func _write(contentsOf data: Data) throws {
227 | if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
228 | return try self.write(contentsOf: data)
229 | } else {
230 | return self.write(data)
231 | }
232 | }
233 |
234 | func _read(upToCount count: Int) throws -> Data? {
235 | if #available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) {
236 | return try self.read(upToCount: count)
237 | } else {
238 | return self.readData(ofLength: 8)
239 | }
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/Sources/SwiftCompilerPlugin/CompilerPluginMessageHandler+Macros.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by John Scott on 02/09/2023.
6 | //
7 |
8 | @_implementationOnly import Foundation
9 |
10 | extension CompilerPluginMessageHandler {
11 | /// Expand `@freestainding(XXX)` macros.
12 | func expandFreestandingMacro(
13 | message: HostToPluginMessage,
14 | macro: PluginMessage.MacroReference,
15 | macroRole: PluginMessage.MacroRole?,
16 | discriminator: String,
17 | expandingSyntax: PluginMessage.Syntax
18 | ) throws {
19 | let response: PluginToHostMessage
20 | var diagnostics: [PluginMessage.Diagnostic]
21 | var expandedSource: String? = nil
22 | let macroType = provider.resolveMacro(moduleName: macro.moduleName, typeName: macro.typeName)
23 | if let macroType = macroType as? FreestandingMacro.Type {
24 | let result = try macroType.expandFreestandingMacro(macro: macro, macroRole: macroRole, discriminator: discriminator, expandingSyntax: expandingSyntax)
25 | expandedSource = result.expandedSource
26 | diagnostics = result.diagnostics
27 | } else {
28 | diagnostics = []
29 | diagnostics.append(PluginMessage.Diagnostic(message: "\(#function) is not implemented for moduleName \(macro.moduleName) typeName: \(macro.typeName)", severity: .error, position: PluginMessage.Diagnostic.Position(fileName: expandingSyntax.location.fileName, offset: expandingSyntax.location.offset), highlights: [], notes: [], fixIts: []))
30 | }
31 |
32 | expandedSource = expandedSource.map{
33 | $0 + "\n/* \(prettyDescription(message))*/"
34 | }
35 |
36 | if (expandedSource == nil) {
37 | expandedSource = "/* \(prettyDescription(message))*/"
38 | }
39 |
40 | if hostCapability.hasExpandMacroResult {
41 | response = .expandMacroResult(expandedSource: expandedSource, diagnostics: diagnostics)
42 | } else {
43 | // TODO: Remove this when all compilers have 'hasExpandMacroResult'.
44 | response = .expandFreestandingMacroResult(expandedSource: expandedSource, diagnostics: diagnostics)
45 | }
46 | try self.sendMessage(response)
47 | }
48 |
49 | /// Expand `@attached(XXX)` macros.
50 | func expandAttachedMacro(
51 | message: HostToPluginMessage,
52 | macro: PluginMessage.MacroReference,
53 | macroRole: PluginMessage.MacroRole,
54 | discriminator: String,
55 | attributeSyntax: PluginMessage.Syntax,
56 | declSyntax: PluginMessage.Syntax,
57 | parentDeclSyntax: PluginMessage.Syntax?,
58 | extendedTypeSyntax: PluginMessage.Syntax?,
59 | conformanceListSyntax: PluginMessage.Syntax?
60 | ) throws {
61 | let response: PluginToHostMessage
62 | var diagnostics: [PluginMessage.Diagnostic]
63 | var expandedSource: String? = nil
64 | let macroType = provider.resolveMacro(moduleName: macro.moduleName, typeName: macro.typeName)
65 | if let macroType = macroType as? AttachedMacro.Type {
66 | let result = try macroType.expandAttachedMacro(macro: macro, macroRole: macroRole, discriminator: discriminator, attributeSyntax: attributeSyntax, declSyntax: declSyntax, parentDeclSyntax: parentDeclSyntax, extendedTypeSyntax: extendedTypeSyntax, conformanceListSyntax: conformanceListSyntax)
67 | expandedSource = result.expandedSource
68 | diagnostics = result.diagnostics
69 | } else {
70 | diagnostics = []
71 | diagnostics.append(PluginMessage.Diagnostic(message: "\(#function) is not implemented for moduleName \(macro.moduleName) typeName: \(macro.typeName)", severity: .error, position: PluginMessage.Diagnostic.Position(fileName: attributeSyntax.location.fileName, offset: attributeSyntax.location.offset), highlights: [], notes: [], fixIts: []))
72 | }
73 |
74 | expandedSource = expandedSource.map{
75 | $0 + "\n/* \(prettyDescription(message))*/"
76 | }
77 |
78 | if (expandedSource == nil) {
79 | expandedSource = "/* \(prettyDescription(message))*/"
80 | }
81 |
82 | if hostCapability.hasExpandMacroResult {
83 | response = .expandMacroResult(expandedSource: expandedSource, diagnostics: diagnostics)
84 | } else {
85 | response = .expandAttachedMacroResult(expandedSources: expandedSource.map({[$0]}), diagnostics: diagnostics)
86 | }
87 | try self.sendMessage(response)
88 | }
89 |
90 | func prettyDescription(_ value: T) -> String {
91 | let encoder = JSONEncoder()
92 | encoder.outputFormatting = [.prettyPrinted]
93 | let data = try! encoder.encode(value)
94 | let string = String(data: data, encoding: .utf8)
95 | return string!
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/SwiftCompilerPlugin/CompilerPluginMessageHandler.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift open source project
4 | //
5 | // Copyright (c) 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See http://swift.org/LICENSE.txt for license information
9 | // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | /// Optional features.
14 | public enum PluginFeature: String {
15 | case loadPluginLibrary = "load-plugin-library"
16 | }
17 |
18 | /// A type that provides the actual plugin functions.
19 | public protocol PluginProvider {
20 | /// Resolve macro type by the module name and the type name.
21 | func resolveMacro(moduleName: String, typeName: String) -> Macro.Type?
22 |
23 | /// Load dynamic link library at `libraryPath`. Implementations can use
24 | /// `moduleName` to associate the loaded library with it.
25 | func loadPluginLibrary(libraryPath: String, moduleName: String) throws
26 |
27 | /// Optional plugin features. This is sent to the host so the it can decide
28 | /// the behavior depending on these.
29 | var features: [PluginFeature] { get }
30 | }
31 |
32 | /// Low level message connection to the plugin host.
33 | /// This encapsulates the connection and the message serialization.
34 | public protocol MessageConnection {
35 | /// Send a message to the peer.
36 | func sendMessage(_ message: TX) throws
37 | /// Wait until receiving a message from the peer, and return it.
38 | func waitForNextMessage(_ type: RX.Type) throws -> RX?
39 | }
40 |
41 | /// Represent the capability of the plugin host (i.e. compiler).
42 | struct HostCapability {
43 | var protocolVersion: Int
44 |
45 | // Create an "oldest" capability.
46 | init() {
47 | self.protocolVersion = 0
48 | }
49 |
50 | init(_ message: PluginMessage.HostCapability) {
51 | self.protocolVersion = message.protocolVersion
52 | }
53 |
54 | /// Compiler accept 'expandMacroResult' response message.
55 | var hasExpandMacroResult: Bool { protocolVersion >= 5 }
56 | }
57 |
58 | /// 'CompilerPluginMessageHandler' is a type that listens to the message
59 | /// connection and dispatches them to the actual plugin provider, then send back
60 | /// the response.
61 | ///
62 | /// The low level connection and the provider is injected by the client.
63 | public class CompilerPluginMessageHandler {
64 | /// Message channel for bidirectional communication with the plugin host.
65 | let connection: Connection
66 |
67 | /// Object to provide actual plugin functions.
68 | let provider: Provider
69 |
70 | /// Plugin host capability
71 | var hostCapability: HostCapability
72 |
73 | public init(connection: Connection, provider: Provider) {
74 | self.connection = connection
75 | self.provider = provider
76 | self.hostCapability = HostCapability()
77 | }
78 | }
79 |
80 | extension CompilerPluginMessageHandler {
81 | func sendMessage(_ message: PluginToHostMessage) throws {
82 | try connection.sendMessage(message)
83 | }
84 |
85 | func waitForNextMessage() throws -> HostToPluginMessage? {
86 | try connection.waitForNextMessage(HostToPluginMessage.self)
87 | }
88 |
89 | /// Run the main message listener loop.
90 | /// Returns when the message connection was closed.
91 | /// Throws an error when it failed to send/receive the message, or failed
92 | /// to serialize/deserialize the message.
93 | public func main() throws {
94 | while let message = try self.waitForNextMessage() {
95 | try handleMessage(message)
96 | }
97 | }
98 |
99 | /// Handles a single message received from the plugin host.
100 | fileprivate func handleMessage(_ message: HostToPluginMessage) throws {
101 | switch message {
102 | case .getCapability(let hostCapability):
103 | // Remember the peer capability if provided.
104 | if let hostCapability = hostCapability {
105 | self.hostCapability = .init(hostCapability)
106 | }
107 |
108 | // Return the plugin capability.
109 | let capability = PluginMessage.PluginCapability(
110 | protocolVersion: PluginMessage.PROTOCOL_VERSION_NUMBER,
111 | features: provider.features.map({ $0.rawValue })
112 | )
113 | try self.sendMessage(.getCapabilityResult(capability: capability))
114 |
115 | case .expandFreestandingMacro(let macro, let macroRole, let discriminator, let expandingSyntax):
116 | try expandFreestandingMacro(
117 | message: message,
118 | macro: macro,
119 | macroRole: macroRole,
120 | discriminator: discriminator,
121 | expandingSyntax: expandingSyntax
122 | )
123 |
124 | case .expandAttachedMacro(
125 | let macro,
126 | let macroRole,
127 | let discriminator,
128 | let attributeSyntax,
129 | let declSyntax,
130 | let parentDeclSyntax,
131 | let extendedTypeSyntax,
132 | let conformanceListSyntax
133 | ):
134 | try expandAttachedMacro(
135 | message: message,
136 | macro: macro,
137 | macroRole: macroRole,
138 | discriminator: discriminator,
139 | attributeSyntax: attributeSyntax,
140 | declSyntax: declSyntax,
141 | parentDeclSyntax: parentDeclSyntax,
142 | extendedTypeSyntax: extendedTypeSyntax,
143 | conformanceListSyntax: conformanceListSyntax
144 | )
145 |
146 | case .loadPluginLibrary(let libraryPath, let moduleName):
147 | var diags: [PluginMessage.Diagnostic] = []
148 | do {
149 | try provider.loadPluginLibrary(libraryPath: libraryPath, moduleName: moduleName)
150 | } catch {
151 | diags.append(
152 | PluginMessage.Diagnostic(
153 | message: String(describing: error),
154 | severity: .error,
155 | position: .invalid,
156 | highlights: [],
157 | notes: [],
158 | fixIts: []
159 | )
160 | )
161 | }
162 | try self.sendMessage(.loadPluginLibraryResult(loaded: diags.isEmpty, diagnostics: diags));
163 | }
164 | }
165 | }
166 |
167 | struct UnimplementedError: Error, CustomStringConvertible {
168 | var description: String { "unimplemented" }
169 | }
170 |
171 | /// Default implementation of 'PluginProvider' requirements.
172 | public extension PluginProvider {
173 | var features: [PluginFeature] {
174 | // No optional features by default.
175 | return []
176 | }
177 |
178 | func loadPluginLibrary(libraryPath: String, moduleName: String) throws {
179 | // This should be unreachable. The host should not call 'loadPluginLibrary'
180 | // unless the feature is not declared.
181 | throw UnimplementedError()
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/Sources/SwiftCompilerPlugin/FreestandingMacro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
3 | // Licensed under Apache License v2.0 with Runtime Library Exception
4 | //
5 | // See https://swift.org/LICENSE.txt for license information
6 | // See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
7 | //
8 | //===----------------------------------------------------------------------===//
9 |
10 | /// Describes a macro that is freestanding, meaning that it is used with the
11 | /// `#` syntax.
12 | public protocol FreestandingMacro: Macro {
13 | static func expandFreestandingMacro(
14 | macro: PluginMessage.MacroReference,
15 | macroRole: PluginMessage.MacroRole?,
16 | discriminator: String,
17 | expandingSyntax: PluginMessage.Syntax
18 | ) throws -> (expandedSource: String?,
19 | diagnostics: [PluginMessage.Diagnostic])
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/SwiftCompilerPlugin/Macro.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Macro.swift
3 | //
4 | //
5 | // Created by John Scott on 02/09/2023.
6 | //
7 |
8 | public protocol Macro {}
9 |
--------------------------------------------------------------------------------
/Sources/SwiftCompilerPlugin/PluginMessages.swift:
--------------------------------------------------------------------------------
1 | //===----------------------------------------------------------------------===//
2 | //
3 | // This source file is part of the Swift open source project
4 | //
5 | // Copyright (c) 2023 Apple Inc. and the Swift project authors
6 | // Licensed under Apache License v2.0 with Runtime Library Exception
7 | //
8 | // See http://swift.org/LICENSE.txt for license information
9 | // See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10 | //
11 | //===----------------------------------------------------------------------===//
12 |
13 | // NOTE: Types in this file should be self-contained and should not depend on any non-stdlib types.
14 |
15 | public enum HostToPluginMessage: Codable {
16 | /// Send capability of the host, and get capability of the plugin.
17 | case getCapability(
18 | capability: PluginMessage.HostCapability?
19 | )
20 |
21 | /// Expand a '@freestanding' macro.
22 | case expandFreestandingMacro(
23 | macro: PluginMessage.MacroReference,
24 | macroRole: PluginMessage.MacroRole? = nil,
25 | discriminator: String,
26 | syntax: PluginMessage.Syntax
27 | )
28 |
29 | /// Expand an '@attached' macro.
30 | case expandAttachedMacro(
31 | macro: PluginMessage.MacroReference,
32 | macroRole: PluginMessage.MacroRole,
33 | discriminator: String,
34 | attributeSyntax: PluginMessage.Syntax,
35 | declSyntax: PluginMessage.Syntax,
36 | parentDeclSyntax: PluginMessage.Syntax?,
37 | extendedTypeSyntax: PluginMessage.Syntax?,
38 | conformanceListSyntax: PluginMessage.Syntax?
39 | )
40 |
41 | /// Optionally implemented message to load a dynamic link library.
42 | /// 'moduleName' can be used as a hint indicating that the library
43 | /// provides the specified module.
44 | case loadPluginLibrary(
45 | libraryPath: String,
46 | moduleName: String
47 | )
48 | }
49 |
50 | public enum PluginToHostMessage: Codable {
51 | case getCapabilityResult(
52 | capability: PluginMessage.PluginCapability
53 | )
54 |
55 | /// Unified response for freestanding/attached macro expansion.
56 | case expandMacroResult(
57 | expandedSource: String?,
58 | diagnostics: [PluginMessage.Diagnostic]
59 | )
60 |
61 | // @available(*, deprecated: "use expandMacroResult() instead")
62 | case expandFreestandingMacroResult(
63 | expandedSource: String?,
64 | diagnostics: [PluginMessage.Diagnostic]
65 | )
66 |
67 | // @available(*, deprecated: "use expandMacroResult() instead")
68 | case expandAttachedMacroResult(
69 | expandedSources: [String]?,
70 | diagnostics: [PluginMessage.Diagnostic]
71 | )
72 |
73 | case loadPluginLibraryResult(
74 | loaded: Bool,
75 | diagnostics: [PluginMessage.Diagnostic]
76 | )
77 | }
78 |
79 | public enum PluginMessage {
80 | public static var PROTOCOL_VERSION_NUMBER: Int { 7 } // Pass extension protocol list
81 |
82 | public struct HostCapability: Codable {
83 | var protocolVersion: Int
84 |
85 | public init(protocolVersion: Int) {
86 | self.protocolVersion = protocolVersion
87 | }
88 | }
89 |
90 | public struct PluginCapability: Codable {
91 | public var protocolVersion: Int
92 |
93 | /// Optional features this plugin provides.
94 | /// * 'load-plugin-library': 'loadPluginLibrary' message is implemented.
95 | public var features: [String]?
96 |
97 | public init(protocolVersion: Int, features: [String]? = nil) {
98 | self.protocolVersion = protocolVersion
99 | self.features = features
100 | }
101 | }
102 |
103 | public struct MacroReference: Codable {
104 | public var moduleName: String
105 | public var typeName: String
106 |
107 | // The name of 'macro' declaration the client is using.
108 | public var name: String
109 |
110 | public init(moduleName: String, typeName: String, name: String) {
111 | self.moduleName = moduleName
112 | self.typeName = typeName
113 | self.name = name
114 | }
115 | }
116 |
117 | public enum MacroRole: String, Codable {
118 | case expression
119 | case declaration
120 | case accessor
121 | case memberAttribute
122 | case member
123 | case peer
124 | case conformance
125 | case codeItem
126 | case `extension`
127 | }
128 |
129 | public struct SourceLocation: Codable {
130 | /// A file ID consisting of the module name and file name (without full path),
131 | /// as would be generated by the macro expansion `#fileID`.
132 | public var fileID: String
133 |
134 | /// A full path name as would be generated by the macro expansion `#filePath`,
135 | /// e.g., `/home/taylor/alison.swift`.
136 | public var fileName: String
137 |
138 | /// UTF-8 offset of the location in the file.
139 | public var offset: Int
140 |
141 | public var line: Int
142 | public var column: Int
143 |
144 | public init(fileID: String, fileName: String, offset: Int, line: Int, column: Int) {
145 | self.fileID = fileID
146 | self.fileName = fileName
147 | self.offset = offset
148 | self.line = line
149 | self.column = column
150 | }
151 | }
152 |
153 | public struct Diagnostic: Codable {
154 | public enum Severity: String, Codable {
155 | case error
156 | case warning
157 | case note
158 | }
159 | public struct Position: Codable {
160 | public var fileName: String
161 | /// UTF-8 offset in the file.
162 | public var offset: Int
163 |
164 | public init(fileName: String, offset: Int) {
165 | self.fileName = fileName
166 | self.offset = offset
167 | }
168 |
169 | public static var invalid: Self {
170 | .init(fileName: "", offset: 0)
171 | }
172 | }
173 | public struct PositionRange: Codable {
174 | public var fileName: String
175 | /// UTF-8 offset of the start of the range in the file.
176 | public var startOffset: Int
177 | /// UTF-8 offset of the end of the range in the file.
178 | public var endOffset: Int
179 |
180 | public init(fileName: String, startOffset: Int, endOffset: Int) {
181 | self.fileName = fileName
182 | self.startOffset = startOffset
183 | self.endOffset = endOffset
184 | }
185 |
186 | public static var invalid: Self {
187 | .init(fileName: "", startOffset: 0, endOffset: 0)
188 | }
189 | }
190 | public struct Note: Codable {
191 | public var position: Position
192 | public var message: String
193 |
194 | public init(position: Position, message: String) {
195 | self.position = position
196 | self.message = message
197 | }
198 | }
199 | public struct FixIt: Codable {
200 | public struct Change: Codable {
201 | public var range: PositionRange
202 | public var newText: String
203 |
204 | internal init(range: PositionRange, newText: String) {
205 | self.range = range
206 | self.newText = newText
207 | }
208 | }
209 | public var message: String
210 | public var changes: [Change]
211 |
212 | internal init(message: String, changes: [Change]) {
213 | self.message = message
214 | self.changes = changes
215 | }
216 | }
217 | public var message: String
218 | public var severity: Severity
219 | public var position: Position
220 | public var highlights: [PositionRange]
221 | public var notes: [Note]
222 | public var fixIts: [FixIt]
223 |
224 | internal init(message: String, severity: Severity, position: Position, highlights: [PositionRange], notes: [Note], fixIts: [FixIt]) {
225 | self.message = message
226 | self.severity = severity
227 | self.position = position
228 | self.highlights = highlights
229 | self.notes = notes
230 | self.fixIts = fixIts
231 | }
232 | }
233 |
234 | public struct Syntax: Codable {
235 | public enum Kind: String, Codable {
236 | case declaration
237 | case statement
238 | case expression
239 | case type
240 | case pattern
241 | case attribute
242 | }
243 | public var kind: Kind
244 | public var source: String
245 | public var location: SourceLocation
246 |
247 | public init(kind: Kind, source: String, location: SourceLocation) {
248 | self.kind = kind
249 | self.source = source
250 | self.location = location
251 | }
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/Tests/SwiftCompilerPluginTests/SwiftCompilerPluginTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SwiftCompilerPlugin
3 |
4 | final class SwiftCompilerPluginTests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 |
--------------------------------------------------------------------------------