├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── StaticMemberIterable │ └── StaticMemberIterable.swift ├── StaticMemberIterableClient │ └── main.swift └── StaticMemberIterableMacros │ └── StaticMemberIterableMacro.swift └── Tests └── StaticMemberIterableTests └── StaticMemberIterableTests.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: "**" 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: Swift ${{ matrix.swift }} on ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - os: macos-13 17 | swift: "5.9" 18 | - os: ubuntu-latest 19 | swift: "5.9" 20 | 21 | steps: 22 | - uses: swift-actions/setup-swift@v1 23 | with: 24 | swift-version: ${{ matrix.swift }} 25 | - run: swift --version 26 | - uses: actions/checkout@v3 27 | 28 | - name: test 29 | run: swift test 30 | 31 | - name: build for release 32 | run: swift build -c release 33 | 34 | - name: run example 35 | run: swift run 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .swiftpm 2 | .build 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 Full Queue Developer 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "swift-syntax", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/apple/swift-syntax.git", 7 | "state" : { 8 | "revision" : "74203046135342e4a4a627476dd6caf8b28fe11b", 9 | "version" : "509.0.0" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | 3 | import PackageDescription 4 | import CompilerPluginSupport 5 | 6 | let package = Package( 7 | name: "StaticMemberIterable", 8 | platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, making them visible to other packages. 11 | .library( 12 | name: "StaticMemberIterable", 13 | targets: ["StaticMemberIterable"] 14 | ), 15 | .executable( 16 | name: "StaticMemberIterableClient", 17 | targets: ["StaticMemberIterableClient"] 18 | ), 19 | ], 20 | dependencies: [ 21 | .package(url: "https://github.com/apple/swift-syntax.git", from: "509.0.0"), 22 | ], 23 | targets: [ 24 | // Targets are the basic building blocks of a package, defining a module or a test suite. 25 | // Targets can depend on other targets in this package and products from dependencies. 26 | // Macro implementation that performs the source transformation of a macro. 27 | .macro( 28 | name: "StaticMemberIterableMacros", 29 | dependencies: [ 30 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 31 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax") 32 | ] 33 | ), 34 | 35 | // Library that exposes a macro as part of its API, which is used in client programs. 36 | .target(name: "StaticMemberIterable", dependencies: ["StaticMemberIterableMacros"]), 37 | 38 | // A client of the library, which is able to use the macro in its own code. 39 | .executableTarget(name: "StaticMemberIterableClient", dependencies: ["StaticMemberIterable"]), 40 | 41 | // A test target used to develop the macro implementation. 42 | .testTarget( 43 | name: "StaticMemberIterableTests", 44 | dependencies: [ 45 | "StaticMemberIterableMacros", 46 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 47 | ] 48 | ), 49 | ] 50 | ) 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StaticMemberIterable 2 | 3 | Confidently cover all static members. Like `CaseIterable`, this macro creates an array of all the static members of a type. This is useful when a type has a few examples as static members. 4 | 5 | Here, we have a `Chili` type so that we can discuss the heat level and names of various chilis. So far, we have two: `jalapeño` and `habenero`. For testing or for displaying in the UI, we want to confidently list both the jalapeño & the habenero. With `@StaticMemberIterable`, we can finally do si. 6 | 7 | ``` 8 | @StaticMemberIterable 9 | struct Chili { 10 | let name: String 11 | let heatLevel: Int 12 | 13 | static let jalapeño = Chili(name: "jalapeño", heatLevel: 2) 14 | static let habenero = Chili(name: "habenero", heatLevel: 5) 15 | } 16 | ``` 17 | 18 | expands to 19 | 20 | ``` 21 | struct Chili { 22 | let name: String 23 | let heatLevel: Int 24 | 25 | static let jalapeño = Chili(name: "jalapeño", heatLevel: 2) 26 | static let habenero = Chili(name: "habenero", heatLevel: 5) 27 | 28 | static let allStaticMembers = [jalapeño, habenero] 29 | } 30 | ``` 31 | 32 | ## Installation 33 | 34 | In `Package.swift`, add the package to your dependencies. 35 | ``` 36 | .package(url: "https://github.com/FullQueueDeveloper/StaticMemberIterable.git", from: "1.0.0"), 37 | ``` 38 | 39 | And add `"StaticMemberIterable"` to the list of your target's dependencies. 40 | 41 | When prompted by Xcode, trust the macro. 42 | 43 | 44 | ## Swift macros? 45 | 46 | Introduced at WWDC '23, requiring Swift 5.9 47 | 48 | ## License 49 | 50 | [BSD-3-Clause](https://opensource.org/license/bsd-3-clause/) 51 | -------------------------------------------------------------------------------- /Sources/StaticMemberIterable/StaticMemberIterable.swift: -------------------------------------------------------------------------------- 1 | // The Swift Programming Language 2 | // https://docs.swift.org/swift-book 3 | 4 | @attached(member, names: named(allStaticMembers)) 5 | public macro StaticMemberIterable() = #externalMacro(module: "StaticMemberIterableMacros", type: "StaticMemberIterableMacro") 6 | -------------------------------------------------------------------------------- /Sources/StaticMemberIterableClient/main.swift: -------------------------------------------------------------------------------- 1 | import StaticMemberIterable 2 | 3 | @StaticMemberIterable 4 | struct Chili { 5 | let name: String 6 | let heatLevel: Int 7 | 8 | static let jalapeño = Chili(name: "jalapeño", heatLevel: 2) 9 | static let habenero = Chili(name: "habenero", heatLevel: 5) 10 | } 11 | 12 | print(Chili.allStaticMembers) 13 | 14 | struct MyRecord { 15 | // ... 16 | @StaticMemberIterable 17 | enum Fixtures { 18 | static let a = MyRecord() 19 | static let b = MyRecord() 20 | } 21 | } 22 | 23 | print(MyRecord.Fixtures.allStaticMembers) 24 | 25 | // Generates a compiler warning when there aren't any static members. 26 | @StaticMemberIterable 27 | struct Fruit { 28 | let name: String 29 | } 30 | 31 | -------------------------------------------------------------------------------- /Sources/StaticMemberIterableMacros/StaticMemberIterableMacro.swift: -------------------------------------------------------------------------------- 1 | import SwiftCompilerPlugin 2 | import SwiftSyntax 3 | import SwiftSyntaxBuilder 4 | import SwiftSyntaxMacros 5 | import SwiftDiagnostics 6 | 7 | public struct StaticMemberIterableMacro: MemberMacro { 8 | public static func expansion( 9 | of node: SwiftSyntax.AttributeSyntax, 10 | providingMembersOf declaration: Declaration, 11 | in context: Context) throws -> [SwiftSyntax.DeclSyntax] where Declaration : SwiftSyntax.DeclGroupSyntax, Context : SwiftSyntaxMacros.MacroExpansionContext { 12 | 13 | let staticMemberNames: [String] = declaration.memberBlock.members 14 | .flatMap { (memberDeclListItemSyntax: MemberBlockItemSyntax) in 15 | memberDeclListItemSyntax 16 | .children(viewMode: .fixedUp) 17 | .compactMap({ $0.as(VariableDeclSyntax.self) }) 18 | } 19 | .compactMap { (variableDeclSyntax: VariableDeclSyntax) in 20 | guard variableDeclSyntax.hasStaticModifier else { 21 | return nil 22 | } 23 | return variableDeclSyntax.bindings 24 | .compactMap { (patternBindingSyntax: PatternBindingSyntax) in 25 | patternBindingSyntax.pattern.as(IdentifierPatternSyntax.self)?.identifier.text 26 | } 27 | .first 28 | } 29 | 30 | guard !staticMemberNames.isEmpty else { 31 | context.diagnose(Diagnostic(node: node.root, message: NoStaticMembersWarning())) 32 | return [] 33 | } 34 | return ["static let allStaticMembers = [\(raw: staticMemberNames.joined(separator: ", "))]"] 35 | } 36 | 37 | public struct NotATypeError: DiagnosticMessage { 38 | 39 | public var message: String { "`StaticMemberIterable` works on a `class`, `enum`, or `struct`" } 40 | 41 | public var diagnosticID: SwiftDiagnostics.MessageID { .init(domain: "StaticMemberIterableMacro", id: "NotATypeMembersError")} 42 | 43 | public var severity: SwiftDiagnostics.DiagnosticSeverity { .error } 44 | } 45 | 46 | public struct NoStaticMembersWarning: DiagnosticMessage { 47 | 48 | public var message: String { 49 | "'@StaticMemberIterable' does not generate an empty list when there are no static members" 50 | } 51 | 52 | public var diagnosticID: SwiftDiagnostics.MessageID { .init(domain: "StaticMemberIterableMacro", id: "NoStaticMembersWarning")} 53 | 54 | public var severity: SwiftDiagnostics.DiagnosticSeverity { .warning } 55 | } 56 | } 57 | 58 | private extension VariableDeclSyntax { 59 | var hasStaticModifier: Bool { 60 | self.modifiers.children(viewMode: .fixedUp) 61 | .compactMap { syntax in 62 | syntax.as(DeclModifierSyntax.self)? 63 | .children(viewMode: .fixedUp) 64 | .contains { syntax in 65 | switch syntax.as(TokenSyntax.self)?.tokenKind { 66 | case .keyword(.static): 67 | return true 68 | default: 69 | return false 70 | } 71 | } 72 | } 73 | .contains(true) 74 | } 75 | } 76 | 77 | @main 78 | struct StaticMemberIterablePlugin: CompilerPlugin { 79 | let providingMacros: [Macro.Type] = [ 80 | StaticMemberIterableMacro.self, 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /Tests/StaticMemberIterableTests/StaticMemberIterableTests.swift: -------------------------------------------------------------------------------- 1 | import SwiftSyntaxMacros 2 | import SwiftSyntaxMacrosTestSupport 3 | import XCTest 4 | import StaticMemberIterableMacros 5 | 6 | let testMacros: [String: Macro.Type] = [ 7 | "StaticMemberIterable": StaticMemberIterableMacro.self, 8 | ] 9 | 10 | final class StaticMemberIterableTests: XCTestCase { 11 | func testEnumWithOnlyStaticMembers() { 12 | assertMacroExpansion( 13 | """ 14 | struct MyRecord { 15 | @StaticMemberIterable 16 | enum Fixtures { 17 | static let a = MyRecord() 18 | static let b = MyRecord() 19 | } 20 | } 21 | """, 22 | expandedSource: 23 | """ 24 | struct MyRecord { 25 | enum Fixtures { 26 | static let a = MyRecord() 27 | static let b = MyRecord() 28 | 29 | static let allStaticMembers = [a, b] 30 | } 31 | } 32 | """, 33 | macros: testMacros 34 | ) 35 | } 36 | 37 | func testEnumWithCasesAndStaticMembers() { 38 | assertMacroExpansion( 39 | """ 40 | @StaticMemberIterable 41 | enum Flavors { 42 | case red, green 43 | case blue 44 | static let original = Flavors.blue 45 | static let newest = Flavors.green 46 | } 47 | """, 48 | expandedSource: 49 | """ 50 | 51 | enum Flavors { 52 | case red, green 53 | case blue 54 | static let original = Flavors.blue 55 | static let newest = Flavors.green 56 | 57 | static let allStaticMembers = [original, newest] 58 | } 59 | """, 60 | macros: testMacros 61 | ) 62 | } 63 | 64 | func testStructWithStaticMembers() { 65 | assertMacroExpansion( 66 | """ 67 | @StaticMemberIterable 68 | struct Chili { 69 | let name: String 70 | let heatLevel: Int 71 | static let jalapeño = Chili(name: "jalapeño", heatLevel: 2) 72 | static let habenero = Chili(name: "habenero", heatLevel: 5) 73 | } 74 | """, 75 | expandedSource: 76 | """ 77 | 78 | struct Chili { 79 | let name: String 80 | let heatLevel: Int 81 | static let jalapeño = Chili(name: "jalapeño", heatLevel: 2) 82 | static let habenero = Chili(name: "habenero", heatLevel: 5) 83 | 84 | static let allStaticMembers = [jalapeño, habenero] 85 | } 86 | """, 87 | macros: testMacros 88 | ) 89 | } 90 | 91 | func testStructWithNoStaticMembers() { 92 | assertMacroExpansion( 93 | """ 94 | @StaticMemberIterable 95 | struct Fruit { 96 | let name: String 97 | } 98 | """, 99 | expandedSource: 100 | """ 101 | 102 | struct Fruit { 103 | let name: String 104 | } 105 | """, 106 | diagnostics: [DiagnosticSpec(message: "'@StaticMemberIterable' does not generate an empty list when there are no static members", line: 1, column: 1, severity: .warning)], 107 | macros: testMacros 108 | ) 109 | } 110 | 111 | func testClassWithNoStaticMembers() { 112 | assertMacroExpansion( 113 | """ 114 | @StaticMemberIterable 115 | class Fruit { 116 | let name: String 117 | } 118 | """, 119 | expandedSource: 120 | """ 121 | 122 | class Fruit { 123 | let name: String 124 | } 125 | """, 126 | diagnostics: [DiagnosticSpec(message: "'@StaticMemberIterable' does not generate an empty list when there are no static members", line: 1, column: 1, severity: .warning)], 127 | macros: testMacros 128 | ) 129 | } 130 | 131 | func testEnumWithNoStaticMembers() { 132 | assertMacroExpansion( 133 | """ 134 | @StaticMemberIterable 135 | enum Fruit { 136 | case mango 137 | } 138 | """, 139 | expandedSource: 140 | """ 141 | 142 | enum Fruit { 143 | case mango 144 | } 145 | """, 146 | diagnostics: [DiagnosticSpec(message: "'@StaticMemberIterable' does not generate an empty list when there are no static members", line: 1, column: 1, severity: .warning)], 147 | macros: testMacros 148 | ) 149 | } 150 | } 151 | --------------------------------------------------------------------------------