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