diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6ca1d3e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + push: + branches: + - '**' + tags: + - "*.*.*" + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + tests: + name: Tests + if: github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/') + uses: oversizedev/GithubWorkflows/.github/workflows/test.yml@main + secrets: inherit + + build-oversize-macro: + name: Build OversizeMacro + needs: + - tests + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm.yml@main + with: + package: OversizeMacro + destination: platform=macOS,arch=arm64 + secrets: inherit + + build-oversize-macro-client: + name: Build OversizeMacroClient + needs: + - tests + uses: oversizedev/GithubWorkflows/.github/workflows/build-swiftpm.yml@main + with: + package: OversizeMacroClient + destination: platform=macOS,arch=arm64 + secrets: inherit + + bump: + name: Bump version + needs: + - build-oversize-macro + - build-oversize-macro-client + if: github.ref == 'refs/heads/main' + uses: oversizedev/GithubWorkflows/.github/workflows/bump.yml@main + secrets: inherit + + release: + name: Create Release + if: github.ref != 'refs/heads/main' && startsWith(github.ref, 'refs/tags/') + uses: oversizedev/GithubWorkflows/.github/workflows/release.yml@main + secrets: inherit diff --git a/.gitignore b/.gitignore index 0023a53..88b0786 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,6 @@ /Packages xcuserdata/ DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata .netrc +Package.resolved +/.swiftpm \ No newline at end of file diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..66c25c3 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,3 @@ +--swiftversion 6.1 +--disable preferKeyPath +--ifdef no-indent \ No newline at end of file diff --git a/Package.resolved b/Package.resolved deleted file mode 100644 index 76592a2..0000000 --- a/Package.resolved +++ /dev/null @@ -1,15 +0,0 @@ -{ - "originHash" : "d15b1252bd3c95d94505e32501f21e201e161c7f947ac532b6ce78549391bca4", - "pins" : [ - { - "identity" : "swift-syntax", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-syntax.git", - "state" : { - "revision" : "0687f71944021d616d34d922343dcef086855920", - "version" : "600.0.1" - } - } - ], - "version" : 3 -} diff --git a/Package.swift b/Package.swift index 8dc57b1..1164e1e 100644 --- a/Package.swift +++ b/Package.swift @@ -1,14 +1,13 @@ // swift-tools-version: 6.1 // The swift-tools-version declares the minimum version of Swift required to build this package. -import PackageDescription import CompilerPluginSupport +import PackageDescription let package = Package( name: "OversizeMacro", platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6), .macCatalyst(.v13)], products: [ - // Products define the executables and libraries a package produces, making them visible to other packages. .library( name: "OversizeMacro", targets: ["OversizeMacro"] @@ -19,25 +18,25 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0"), + .package(url: "https://github.com/swiftlang/swift-syntax.git", .upToNextMajor(from: "601.0.0")), ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. - // Macro implementation that performs the source transformation of a macro. .macro( name: "OversizeMacroMacros", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), - .product(name: "SwiftCompilerPlugin", package: "swift-syntax") + .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), ] ), - - // Library that exposes a macro as part of its API, which is used in client programs. .target(name: "OversizeMacro", dependencies: ["OversizeMacroMacros"]), - - // A client of the library, which is able to use the macro in its own code. .executableTarget(name: "OversizeMacroClient", dependencies: ["OversizeMacro"]), - + .testTarget( + name: "OversizeMacroTests", + dependencies: [ + "OversizeMacro", + "OversizeMacroMacros", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), ] ) diff --git a/Sources/OversizeMacro/OversizeMacro.swift b/Sources/OversizeMacro/OversizeMacro.swift index 9cddaa8..6a43001 100644 --- a/Sources/OversizeMacro/OversizeMacro.swift +++ b/Sources/OversizeMacro/OversizeMacro.swift @@ -1,11 +1,2 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book - -/// A macro that produces both a value and a string containing the -/// source code that generated the value. For example, -/// -/// #stringify(x + y) -/// -/// produces a tuple `(x + y, "x + y")`. -@freestanding(expression) -public macro stringify(_ value: T) -> (T, String) = #externalMacro(module: "OversizeMacroMacros", type: "StringifyMacro") +@attached(member, names: arbitrary) +public macro AutoRoutable() = #externalMacro(module: "OversizeMacroMacros", type: "AutoRoutableMacro") diff --git a/Sources/OversizeMacroClient/main.swift b/Sources/OversizeMacroClient/main.swift index 14a4e25..fc44e39 100644 --- a/Sources/OversizeMacroClient/main.swift +++ b/Sources/OversizeMacroClient/main.swift @@ -1,8 +1,10 @@ import OversizeMacro -let a = 17 -let b = 25 +@AutoRoutable +enum Screens { + case meta + case instagram + case twitter +} -let (result, code) = #stringify(a + b) - -print("The value \(result) was produced by the code \"\(code)\"") +print(Screens.meta.id) diff --git a/Sources/OversizeMacroMacros/AutoRoutableMacro.swift b/Sources/OversizeMacroMacros/AutoRoutableMacro.swift new file mode 100644 index 0000000..a5b4355 --- /dev/null +++ b/Sources/OversizeMacroMacros/AutoRoutableMacro.swift @@ -0,0 +1,40 @@ +import SwiftSyntax +import SwiftSyntaxMacros + +public struct AutoRoutableMacro: MemberMacro { + public static func expansion( + of node: AttributeSyntax, + providingMembersOf declaration: Declaration, + conformingTo _: [TypeSyntax], + in context: Context + ) throws -> [DeclSyntax] { + try expansion(of: node, providingMembersOf: declaration, in: context) + } + + public static func expansion( + of _: AttributeSyntax, + providingMembersOf declaration: Declaration, + in _: Context + ) throws -> [DeclSyntax] { + let caseNames: [String] = declaration.memberBlock.members + .compactMap { $0.decl.as(EnumCaseDeclSyntax.self) } + .flatMap { $0.elements.map { $0.name.text } } + + let cases = caseNames.map { name in + """ + case .\(name): + return "\(name)" + """ + }.joined(separator: "\n") + + return [ + """ + var id: String { + switch self { + \(raw: cases) + } + } + """, + ] + } +} diff --git a/Sources/OversizeMacroMacros/OversizeMacroMacro.swift b/Sources/OversizeMacroMacros/OversizeMacroMacro.swift deleted file mode 100644 index a3a37ac..0000000 --- a/Sources/OversizeMacroMacros/OversizeMacroMacro.swift +++ /dev/null @@ -1,33 +0,0 @@ -import SwiftCompilerPlugin -import SwiftSyntax -import SwiftSyntaxBuilder -import SwiftSyntaxMacros - -/// Implementation of the `stringify` macro, which takes an expression -/// of any type and produces a tuple containing the value of that expression -/// and the source code that produced the value. For example -/// -/// #stringify(x + y) -/// -/// will expand to -/// -/// (x + y, "x + y") -public struct StringifyMacro: ExpressionMacro { - public static func expansion( - of node: some FreestandingMacroExpansionSyntax, - in context: some MacroExpansionContext - ) -> ExprSyntax { - guard let argument = node.arguments.first?.expression else { - fatalError("compiler bug: the macro does not have any arguments") - } - - return "(\(argument), \(literal: argument.description))" - } -} - -@main -struct OversizeMacroPlugin: CompilerPlugin { - let providingMacros: [Macro.Type] = [ - StringifyMacro.self, - ] -} diff --git a/Sources/OversizeMacroMacros/OversizeMacroPlugin.swift b/Sources/OversizeMacroMacros/OversizeMacroPlugin.swift new file mode 100644 index 0000000..b449dde --- /dev/null +++ b/Sources/OversizeMacroMacros/OversizeMacroPlugin.swift @@ -0,0 +1,9 @@ +import SwiftCompilerPlugin +import SwiftSyntaxMacros + +@main +struct OversizeMacroPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + AutoRoutableMacro.self, + ] +} diff --git a/Tests/OversizeMacroTests/AutoRoutableTests.swift b/Tests/OversizeMacroTests/AutoRoutableTests.swift new file mode 100644 index 0000000..e65e363 --- /dev/null +++ b/Tests/OversizeMacroTests/AutoRoutableTests.swift @@ -0,0 +1,103 @@ +import OversizeMacro +import OversizeMacroMacros +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +// MARK: - Macro Expansion Tests + +final class AutoRoutableMacroExpansionTests: XCTestCase { + private let testMacros: [String: any Macro.Type] = [ + "AutoRoutable": AutoRoutableMacro.self, + ] + + func testExpansion_basic() throws { + assertMacroExpansion( + """ + @AutoRoutable + enum Screens { + case main + case settings + case about + case event(id: String) + } + """, + expandedSource: + """ + enum Screens { + case main + case settings + case about + case event(id: String) + + var id: String { + switch self { + case .main: + return "main" + case .settings: + return "settings" + case .about: + return "about" + case .event: + return "event" + } + } + } + """, + macros: testMacros + ) + } + + func testExpansion_multipleElementsInOneLine() throws { + assertMacroExpansion( + """ + @AutoRoutable + enum Screens { + case main + case settings, about + case event(id: String) + } + """, + expandedSource: + """ + enum Screens { + case main + case settings, about + case event(id: String) + + var id: String { + switch self { + case .main: + return "main" + case .settings: + return "settings" + case .about: + return "about" + case .event: + return "event" + } + } + } + """, + macros: testMacros + ) + } +} + +// MARK: - End-to-End Tests + +final class AutoRoutableMacroEndToEndTests: XCTestCase { + @AutoRoutable + enum Screens { + case main + case settings, about + case event(id: String) + } + + func testRuntime_propertyValues() { + XCTAssertEqual(Screens.main.id, "main") + XCTAssertEqual(Screens.settings.id, "settings") + XCTAssertEqual(Screens.about.id, "about") + XCTAssertEqual(Screens.event(id: "0").id, "event") + } +}