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