diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0023a53 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..1681c1f --- /dev/null +++ b/Package.swift @@ -0,0 +1,51 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription +import CompilerPluginSupport + +let package = Package( + name: "Mocked", + 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: "Mocked", + targets: ["Mocked"] + ), + .executable( + name: "MockedClient", + targets: ["MockedClient"] + ), + ], + dependencies: [ + .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "600.0.0-latest"), + ], + 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: "MockedMacros", + dependencies: [ + .product(name: "SwiftSyntaxMacros", 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: "Mocked", dependencies: ["MockedMacros"]), + + // A client of the library, which is able to use the macro in its own code. + .executableTarget(name: "MockedClient", dependencies: ["Mocked"]), + + // A test target used to develop the macro implementation. + .testTarget( + name: "MockedTests", + dependencies: [ + "MockedMacros", + .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), + ] + ), + ] +) diff --git a/Sources/Mocked/Mocked.swift b/Sources/Mocked/Mocked.swift new file mode 100644 index 0000000..09e15fb --- /dev/null +++ b/Sources/Mocked/Mocked.swift @@ -0,0 +1,11 @@ +// 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: "MockedMacros", type: "StringifyMacro") diff --git a/Sources/MockedClient/main.swift b/Sources/MockedClient/main.swift new file mode 100644 index 0000000..12f3c24 --- /dev/null +++ b/Sources/MockedClient/main.swift @@ -0,0 +1,8 @@ +import Mocked + +let a = 17 +let b = 25 + +let (result, code) = #stringify(a + b) + +print("The value \(result) was produced by the code \"\(code)\"") diff --git a/Sources/MockedMacros/MockedMacro.swift b/Sources/MockedMacros/MockedMacro.swift new file mode 100644 index 0000000..eadebb4 --- /dev/null +++ b/Sources/MockedMacros/MockedMacro.swift @@ -0,0 +1,33 @@ +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 MockedPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + StringifyMacro.self, + ] +} diff --git a/Tests/MockedTests/MockedTests.swift b/Tests/MockedTests/MockedTests.swift new file mode 100644 index 0000000..d7a12c7 --- /dev/null +++ b/Tests/MockedTests/MockedTests.swift @@ -0,0 +1,48 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import XCTest + +// 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. +#if canImport(MockedMacros) +import MockedMacros + +let testMacros: [String: Macro.Type] = [ + "stringify": StringifyMacro.self, +] +#endif + +final class MockedTests: XCTestCase { + func testMacro() throws { + #if canImport(MockedMacros) + assertMacroExpansion( + """ + #stringify(a + b) + """, + expandedSource: """ + (a + b, "a + b") + """, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } + + func testMacroWithStringLiteral() throws { + #if canImport(MockedMacros) + assertMacroExpansion( + #""" + #stringify("Hello, \(name)") + """#, + expandedSource: #""" + ("Hello, \(name)", #""Hello, \(name)""#) + """#, + macros: testMacros + ) + #else + throw XCTSkip("macros are only supported when running tests for the host platform") + #endif + } +}