Skip to content

Commit

Permalink
Add Macros project
Browse files Browse the repository at this point in the history
  • Loading branch information
Pablo Guardiola committed Nov 11, 2024
1 parent 4d2986e commit e75bdba
Show file tree
Hide file tree
Showing 20 changed files with 722 additions and 0 deletions.
Binary file not shown.
Binary file not shown.
8 changes: 8 additions & 0 deletions Articles/EducaSwiftHexMacroPackage/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
15 changes: 15 additions & 0 deletions Articles/EducaSwiftHexMacroPackage/Package.resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"originHash" : "0220b7139e3a149ad907bfb9408312c7d6d65cc7a606e0728e52834fab3778d5",
"pins" : [
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-syntax.git",
"state" : {
"revision" : "0687f71944021d616d34d922343dcef086855920",
"version" : "600.0.1"
}
}
],
"version" : 3
}
51 changes: 51 additions & 0 deletions Articles/EducaSwiftHexMacroPackage/Package.swift
Original file line number Diff line number Diff line change
@@ -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: "EducaSwiftHexMacroPackage",
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: "EducaSwiftHexMacroPackage",
targets: ["EducaSwiftHexMacroPackage"]
),
.executable(
name: "EducaSwiftHexMacroPackageClient",
targets: ["EducaSwiftHexMacroPackageClient"]
),
],
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: "EducaSwiftHexMacroPackageMacros",
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: "EducaSwiftHexMacroPackage", dependencies: ["EducaSwiftHexMacroPackageMacros"]),

// A client of the library, which is able to use the macro in its own code.
.executableTarget(name: "EducaSwiftHexMacroPackageClient", dependencies: ["EducaSwiftHexMacroPackage"]),

// A test target used to develop the macro implementation.
.testTarget(
name: "EducaSwiftHexMacroPackageTests",
dependencies: [
"EducaSwiftHexMacroPackageMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import SwiftUI

// The Swift Programming Language
// https://docs.swift.org/swift-book

/// A macro that produces a Color from an hex value. For example,
///
/// #hexColor("FF00FF") // or FFFF00FF to include alpha value
///
/// produces a `Color`.
@freestanding(expression)
public macro hexColor(_ hex: String) -> Color = #externalMacro(module: "EducaSwiftHexMacroPackageMacros", type: "HexColorMacro")
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import EducaSwiftHexMacroPackage
import SwiftUI

let color = #hexColor("FFFF00FF")

print("The value FFFF00FF was produced by the code \"\(color)\"")
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros
import SwiftUI

public enum HexColorMacroError: Error {

case inputNotValid
case lengthNotValid

var description: String {
switch self {
case .inputNotValid:
return "Input is not valid."
case .lengthNotValid:
return "Length not valid."
}
}
}

/// Implementation of the `hexColor` macro, which takes an String
/// and produces a Color with rgba values. For example
///
/// #hexColor("FFFF00FF") // a-r-g-b
///
/// will expand to
///
/// Color(
/// .sRGB,
/// red: 1.0,
/// green: 0.0,
/// blue: 1.0,
/// opacity: 1.0
/// )
public struct HexColorMacro: ExpressionMacro {
public static func expansion(
of node: some FreestandingMacroExpansionSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
guard let argument = node.arguments.first?.expression,
let segments = argument.as(StringLiteralExprSyntax.self)?.segments,
segments.count == 1,
case .stringSegment(let literalSegment)? = segments.first
else {
throw HexColorMacroError.inputNotValid
}

guard literalSegment.content.text.count == 6 || literalSegment.content.text.count == 8 else {
throw HexColorMacroError.lengthNotValid
}

let hex = literalSegment.content.text.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (1, 1, 1, 0)
}

return """
Color(
.sRGB,
red: \(raw: Double(r) / 255),
green: \(raw: Double(g) / 255),
blue: \(raw: Double(b) / 255),
opacity: \(raw: Double(a) / 255)
)
"""
}
}

@main
struct EducaSwiftHexMacroPackagePlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
HexColorMacro.self,
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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(EducaSwiftHexMacroPackageMacros)
import EducaSwiftHexMacroPackageMacros

let testMacros: [String: Macro.Type] = [
"hexColor": HexColorMacro.self,
]
#endif

final class EducaSwiftHexMacroPackageTests: XCTestCase {
func testMacro() throws {
#if canImport(EducaSwiftHexMacroPackageMacros)
assertMacroExpansion(
"""
#hexColor("FFFF00FF")
""",
expandedSource: """
Color(
.sRGB,
red: 1.0,
green: 1.0,
blue: 1.0,
opacity: 1.0
)
""",
macros: testMacros
)
#else
throw XCTSkip("macros are only supported when running tests for the host platform")
#endif
}
}
Loading

0 comments on commit e75bdba

Please sign in to comment.