├── Sources ├── MacrosTalkClient │ ├── types.swift │ └── main.swift ├── MacrosTalk │ └── MacrosTalk.swift └── MacrosTalkMacros │ ├── MacrosTalkMacros.swift │ └── CasedMacro.swift ├── .gitignore ├── .swiftpm ├── xcode │ ├── package.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ ├── MacrosTalk.xcscheme │ │ ├── MacrosTalkClient.xcscheme │ │ └── MacrosTalk-Package.xcscheme └── MacrosTalk.xctestplan ├── Package.resolved ├── Package.swift └── Tests └── SwiftHeroesTests └── SwiftHeroesTests.swift /Sources/MacrosTalkClient/types.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | struct User {} 4 | struct Team {} 5 | struct Company {} 6 | -------------------------------------------------------------------------------- /.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 | .vscode -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/MacrosTalkClient/main.swift: -------------------------------------------------------------------------------- 1 | import MacrosTalk 2 | 3 | let a = 4 4 | let b = 2 5 | 6 | let (result, code) = #stringify(a + b) 7 | 8 | print("The value \(result) was produced by the code \"\(code)\"") 9 | 10 | @Cased 11 | enum Membership { 12 | case nonMember 13 | case user(User, Team) 14 | case team(Team) 15 | case company(Company) 16 | } 17 | 18 | let myUser = Membership.user(.init(), .init()) 19 | let myTeam = Membership.team(.init()) 20 | 21 | -------------------------------------------------------------------------------- /.swiftpm/MacrosTalk.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "2E493D95-2C54-4416-9FB2-2047F6443008", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:", 18 | "identifier" : "MacrosTalkTests", 19 | "name" : "MacrosTalkTests" 20 | } 21 | } 22 | ], 23 | "version" : 1 24 | } 25 | -------------------------------------------------------------------------------- /Sources/MacrosTalk/MacrosTalk.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 | /// #stringify(x + y) 8 | /// 9 | /// produces a tuple `(x + y, "x + y")`. 10 | @freestanding(expression) 11 | public macro stringify(_ value: T) -> (T, String) = #externalMacro(module: "MacrosTalkMacros", type: "StringifyMacro") 12 | 13 | @attached(member, names: prefixed(is), arbitrary) 14 | public macro Cased() = #externalMacro( 15 | module: "MacrosTalkMacros", 16 | type: "CasedMacro" 17 | ) 18 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "a64ef6da48e13e3f1aad6d3eb3439e9e26ce6334673737d0defe06584c8139e5", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-macro-testing", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/pointfreeco/swift-macro-testing", 8 | "state" : { 9 | "revision" : "20c1a8f3b624fb5d1503eadcaa84743050c350f4", 10 | "version" : "0.5.2" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-snapshot-testing", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/pointfreeco/swift-snapshot-testing", 17 | "state" : { 18 | "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", 19 | "version" : "1.17.6" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-syntax", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/apple/swift-syntax.git", 26 | "state" : { 27 | "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", 28 | "version" : "509.1.1" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /Sources/MacrosTalkMacros/MacrosTalkMacros.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | /// Implementation of the `stringify` macro, which takes an expression 7 | /// of any type and produces a tuple containing the value of that expression 8 | /// and the source code that produced the value. For example 9 | /// 10 | /// #stringify(x + y) 11 | /// 12 | /// will expand to 13 | /// 14 | /// (x + y, "x + y") 15 | public struct StringifyMacro: ExpressionMacro { 16 | public static func expansion( 17 | of node: some FreestandingMacroExpansionSyntax, 18 | in context: some MacroExpansionContext 19 | ) -> ExprSyntax { 20 | guard let argument = node.argumentList.first?.expression else { 21 | fatalError("compiler bug: the macro does not have any arguments") 22 | } 23 | 24 | return "(\(argument), \(literal: argument.description))" 25 | } 26 | } 27 | 28 | @main 29 | struct MacrosTalkPlugin: CompilerPlugin { 30 | let providingMacros: [Macro.Type] = [ 31 | StringifyMacro.self, 32 | CasedMacro.self 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 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: "MacrosTalk", 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: "MacrosTalk", 14 | targets: ["MacrosTalk"] 15 | ), 16 | .executable( 17 | name: "MacrosTalkClient", 18 | targets: ["MacrosTalkClient"] 19 | ), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), 23 | .package(url: "https://github.com/pointfreeco/swift-macro-testing", from: "0.4.0") 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: "MacrosTalkMacros", 31 | dependencies: [ 32 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 33 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 34 | ] 35 | ), 36 | 37 | // Library that exposes a macro as part of its API, which is used in client programs. 38 | .target(name: "MacrosTalk", dependencies: ["MacrosTalkMacros"]), 39 | 40 | // A client of the library, which is able to use the macro in its own code. 41 | .executableTarget(name: "MacrosTalkClient", dependencies: ["MacrosTalk"]), 42 | 43 | // A test target used to develop the macro implementation. 44 | .testTarget( 45 | name: "MacrosTalkTests", 46 | dependencies: [ 47 | "MacrosTalkMacros", 48 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 49 | .product(name: "MacroTesting", package: "swift-macro-testing") 50 | ] 51 | ), 52 | ] 53 | ) 54 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MacrosTalk.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 35 | 36 | 37 | 38 | 48 | 49 | 55 | 56 | 62 | 63 | 64 | 65 | 67 | 68 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MacrosTalkClient.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Tests/SwiftHeroesTests/SwiftHeroesTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntax 2 | import SwiftSyntaxBuilder 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import Testing 6 | import MacroTesting 7 | 8 | #if canImport(MacrosTalkMacros) 9 | import MacrosTalkMacros 10 | #endif 11 | 12 | @Suite 13 | struct CasedTests { 14 | @Test("@Cased on struct") 15 | func testCasedStruct() throws { 16 | assertMacro(["Cased": CasedMacro.self]) { 17 | """ 18 | @Cased 19 | struct NotAnEnum { 20 | } 21 | """ 22 | } diagnostics: { 23 | """ 24 | @Cased 25 | ┬───── 26 | ╰─ 🛑 @Cased can only be attached to enums 27 | struct NotAnEnum { 28 | } 29 | """ 30 | } 31 | } 32 | 33 | @Test("@Cased on enum") 34 | func testCased() async throws { 35 | assertMacro(["Cased": CasedMacro.self]) { 36 | """ 37 | @Cased 38 | enum Membership { 39 | case nonMember 40 | case user(User, Team) 41 | case team(Team) 42 | case company(Company) 43 | } 44 | """ 45 | } expansion: { 46 | """ 47 | enum Membership { 48 | case nonMember 49 | case user(User, Team) 50 | case team(Team) 51 | case company(Company) 52 | 53 | var isNonMember: Bool { 54 | if case .nonMember = self { 55 | return true 56 | } 57 | return false 58 | } 59 | 60 | var isUser: Bool { 61 | if case .user = self { 62 | return true 63 | } 64 | return false 65 | } 66 | 67 | var user: (User, Team)? { 68 | guard case let .user(e0, e1) = self else { 69 | return nil 70 | } 71 | 72 | return (e0, e1) 73 | } 74 | 75 | var isTeam: Bool { 76 | if case .team = self { 77 | return true 78 | } 79 | return false 80 | } 81 | 82 | var team: Team? { 83 | guard case let .team(element) = self else { 84 | return nil 85 | } 86 | 87 | return element 88 | } 89 | 90 | var isCompany: Bool { 91 | if case .company = self { 92 | return true 93 | } 94 | return false 95 | } 96 | 97 | var company: Company? { 98 | guard case let .company(element) = self else { 99 | return nil 100 | } 101 | 102 | return element 103 | } 104 | } 105 | """ 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Sources/MacrosTalkMacros/CasedMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | 6 | public struct CasedMacro: MemberMacro { 7 | public static func expansion( 8 | of node: AttributeSyntax, 9 | providingMembersOf declaration: some DeclGroupSyntax, 10 | conformingTo protocols: [TypeSyntax], 11 | in context: some MacroExpansionContext 12 | ) throws -> [DeclSyntax] { 13 | guard let enumDecl = declaration.as(EnumDeclSyntax.self) else { 14 | throw Error.notAnEnum 15 | } 16 | 17 | let caseDecls = enumDecl.memberBlock.members 18 | .compactMap { 19 | $0.decl.as(EnumCaseDeclSyntax.self)? 20 | .elements 21 | .first 22 | } 23 | 24 | return caseDecls.flatMap { caseDecl in 25 | let name = caseDecl.name 26 | let textName = name.text 27 | let camelName = textName[textName.startIndex].uppercased() + textName.dropFirst() 28 | 29 | var output: [DeclSyntax] = [ 30 | """ 31 | var is\(raw: camelName): Bool { 32 | if case .\(name) = self { return true } 33 | return false 34 | } 35 | """ 36 | ] 37 | 38 | let associatedTypes = caseDecl.associatedTypes 39 | 40 | switch associatedTypes.count { 41 | case 1: 42 | let type = associatedTypes[0] 43 | output.append( 44 | """ 45 | var \(name): \(type)? { 46 | guard case let .\(name)(element) = self else { 47 | return nil 48 | } 49 | 50 | return element 51 | } 52 | """ 53 | ) 54 | case 2...: 55 | let tuple = associatedTypes.map(\.text).joined(separator: ", ") 56 | let args = associatedTypes.enumerated() 57 | .map { idx, _ in "e\(idx)" } 58 | .joined(separator: ", ") 59 | 60 | output.append( 61 | """ 62 | var \(name): (\(raw: tuple))? { 63 | guard case let .\(name)(\(raw: args)) = self else { 64 | return nil 65 | } 66 | 67 | return (\(raw: args)) 68 | } 69 | """ 70 | ) 71 | default: 72 | break 73 | } 74 | 75 | return output 76 | } 77 | } 78 | 79 | enum Error: Swift.Error, CustomStringConvertible { 80 | case notAnEnum 81 | 82 | var description: String { 83 | switch self { 84 | case .notAnEnum: 85 | "@Cased can only be attached to enums" 86 | } 87 | } 88 | } 89 | } 90 | 91 | private extension EnumCaseElementSyntax { 92 | var associatedTypes: [TokenSyntax] { 93 | parameterClause?.parameters 94 | .compactMap { $0.type.as(IdentifierTypeSyntax.self)?.name } ?? [] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /.swiftpm/xcode/xcshareddata/xcschemes/MacrosTalk-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 30 | 36 | 37 | 38 | 39 | 40 | 46 | 47 | 49 | 55 | 56 | 57 | 58 | 59 | 69 | 71 | 77 | 78 | 79 | 80 | 86 | 87 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | --------------------------------------------------------------------------------