mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
Add macro examples
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// From https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamples/main.swift
|
||||
// swiftformat:options --indent 2
|
||||
|
||||
import Foundation
|
||||
import MacroExamplesLib
|
||||
|
||||
let x = 1
|
||||
let y = 2
|
||||
let z = 3
|
||||
|
||||
// "Stringify" macro turns the expression into a string.
|
||||
print(#stringify(x + y))
|
||||
|
||||
// "AddBlocker" complains about addition operations. We emit a warning
|
||||
// so it doesn't block compilation.
|
||||
print(#addBlocker(x * y + z))
|
||||
|
||||
#myWarning("remember to pass a string literal here")
|
||||
|
||||
// Uncomment to get an error out of the macro.
|
||||
// let text = "oops"
|
||||
// #myWarning(text)
|
||||
|
||||
struct Font: ExpressibleByFontLiteral {
|
||||
init(fontLiteralName _: String, size _: Int, weight _: MacroExamplesLib.FontWeight) {}
|
||||
}
|
||||
|
||||
let _: Font = #fontLiteral(name: "Comic Sans", size: 14, weight: .thin)
|
||||
|
||||
// "#URL" macro provides compile time checked URL construction. If the URL is
|
||||
// malformed an error is emitted. Otherwise a non-optional URL is expanded.
|
||||
print(#URL("https://swift.org/"))
|
||||
|
||||
let domain = "domain.com"
|
||||
// print(#URL("https://\(domain)/api/path")) // error: #URL requires a static string literal
|
||||
// print(#URL("https://not a url.com")) // error: Malformed url
|
||||
|
||||
// Use the "wrapStoredProperties" macro to deprecate all of the stored
|
||||
// properties.
|
||||
@wrapStoredProperties(#"available(*, deprecated, message: "hands off my data")"#)
|
||||
struct OldStorage {
|
||||
var x: Int
|
||||
}
|
||||
|
||||
// The deprecation warning below comes from the deprecation attribute
|
||||
// introduced by @wrapStoredProperties on OldStorage.
|
||||
_ = OldStorage(x: 5).x
|
||||
|
||||
// Move the storage from each of the stored properties into a dictionary
|
||||
// called `_storage`, turning the stored properties into computed properties.
|
||||
@DictionaryStorage
|
||||
struct Point {
|
||||
var x: Int = 1
|
||||
var y: Int = 2
|
||||
}
|
||||
|
||||
@CaseDetection
|
||||
enum Pet {
|
||||
case dog
|
||||
case cat(curious: Bool)
|
||||
case parrot
|
||||
case snake
|
||||
}
|
||||
|
||||
let pet: Pet = .cat(curious: true)
|
||||
print("Pet is dog: \(pet.isDog)")
|
||||
print("Pet is cat: \(pet.isCat)")
|
||||
|
||||
var point = Point()
|
||||
print("Point storage begins as an empty dictionary: \(point)")
|
||||
print("Default value for point.x: \(point.x)")
|
||||
point.y = 17
|
||||
print("Point storage contains only the value we set: \(point)")
|
||||
|
||||
// MARK: - ObservableMacro
|
||||
|
||||
struct Treat {}
|
||||
|
||||
@Observable
|
||||
final class Dog {
|
||||
var name: String?
|
||||
var treat: Treat?
|
||||
|
||||
var isHappy: Bool = true
|
||||
|
||||
init() {}
|
||||
|
||||
func bark() {
|
||||
print("bork bork")
|
||||
}
|
||||
}
|
||||
|
||||
let dog = Dog()
|
||||
print(dog.name ?? "")
|
||||
dog.name = "George"
|
||||
dog.treat = Treat()
|
||||
print(dog.name ?? "")
|
||||
dog.bark()
|
||||
|
||||
// MARK: NewType
|
||||
|
||||
@NewType(String.self)
|
||||
struct Hostname:
|
||||
NewTypeProtocol,
|
||||
Hashable,
|
||||
CustomStringConvertible
|
||||
{}
|
||||
|
||||
@NewType(String.self)
|
||||
struct Password:
|
||||
NewTypeProtocol,
|
||||
Hashable,
|
||||
CustomStringConvertible
|
||||
{
|
||||
var description: String { "(redacted)" }
|
||||
}
|
||||
|
||||
let hostname = Hostname("localhost")
|
||||
print("hostname: description=\(hostname) hashValue=\(hostname.hashValue)")
|
||||
|
||||
let password = Password("squeamish ossifrage")
|
||||
print("password: description=\(password) hashValue=\(password.hashValue)")
|
||||
|
||||
struct MyStruct {
|
||||
@AddCompletionHandler
|
||||
func f(a _: Int, for b: String, _: Double) async -> String {
|
||||
b
|
||||
}
|
||||
|
||||
@AddAsync
|
||||
func c(a: Int, for b: String, _ value: Double, completionBlock: @escaping (Result<String, Error>) -> Void) {
|
||||
completionBlock(.success("a: \(a), b: \(b), value: \(value)"))
|
||||
}
|
||||
|
||||
@AddAsync
|
||||
func d(a _: Int, for _: String, _: Double, completionBlock: @escaping (Bool) -> Void) {
|
||||
completionBlock(true)
|
||||
}
|
||||
}
|
||||
|
||||
@CustomCodable
|
||||
struct CustomCodableString: Codable {
|
||||
@CodableKey(name: "OtherName")
|
||||
var propertyWithOtherName: String
|
||||
|
||||
var propertyWithSameName: Bool
|
||||
|
||||
func randomFunction() {}
|
||||
}
|
||||
|
||||
Task {
|
||||
let myStruct = MyStruct()
|
||||
let a = try? await myStruct.c(a: 5, for: "Test", 20)
|
||||
|
||||
await myStruct.d(a: 10, for: "value", 40)
|
||||
}
|
||||
|
||||
MyStruct().f(a: 1, for: "hello", 3.14159) { result in
|
||||
print("Eventually received \(result + "!")")
|
||||
}
|
||||
|
||||
let json = """
|
||||
{
|
||||
"OtherName": "Name",
|
||||
"propertyWithSameName": true
|
||||
}
|
||||
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let product = try jsonDecoder.decode(CustomCodableString.self, from: json)
|
||||
print(product.propertyWithOtherName)
|
||||
|
||||
@MyOptionSet<UInt8>
|
||||
enum ShippingOptions {
|
||||
private enum Options: Int {
|
||||
case nextDay
|
||||
case secondDay
|
||||
case priority
|
||||
case standard
|
||||
}
|
||||
|
||||
static let express: ShippingOptions = [.nextDay, .secondDay]
|
||||
static let all: ShippingOptions = [.express, .priority, .standard]
|
||||
}
|
||||
|
||||
// `@MetaEnum` adds a nested enum called `Meta` with the same cases, but no
|
||||
// associated values/payloads. Handy for e.g. describing a schema.
|
||||
@MetaEnum enum Value {
|
||||
case integer(Int)
|
||||
case text(String)
|
||||
case boolean(Bool)
|
||||
case null
|
||||
}
|
||||
|
||||
print(Value.Meta(.integer(42)) == .integer)
|
||||
@@ -0,0 +1,151 @@
|
||||
// From https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamplesLib/Macros.swift
|
||||
// swiftformat:options --indent 2
|
||||
|
||||
import Foundation
|
||||
|
||||
/// "Stringify" the provided value and produce a tuple that includes both the
|
||||
/// original value as well as the source code that generated it.
|
||||
@freestanding(expression) public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MacroExamplesPlugin", type: "StringifyMacro")
|
||||
|
||||
/// Macro that produces a warning on "+" operators within the expression, and
|
||||
/// suggests changing them to "-".
|
||||
@freestanding(expression) public macro addBlocker<T>(_ value: T) -> T = #externalMacro(module: "MacroExamplesPlugin", type: "AddBlocker")
|
||||
|
||||
/// Macro that produces a warning, as a replacement for the built-in
|
||||
/// #warning("...").
|
||||
@freestanding(expression) public macro myWarning(_ message: String) = #externalMacro(module: "MacroExamplesPlugin", type: "WarningMacro")
|
||||
|
||||
public enum FontWeight {
|
||||
case thin
|
||||
case normal
|
||||
case medium
|
||||
case semiBold
|
||||
case bold
|
||||
}
|
||||
|
||||
public protocol ExpressibleByFontLiteral {
|
||||
init(fontLiteralName: String, size: Int, weight: FontWeight)
|
||||
}
|
||||
|
||||
/// Font literal similar to, e.g., #colorLiteral.
|
||||
@freestanding(expression) public macro fontLiteral<T>(name: String, size: Int, weight: FontWeight) -> T = #externalMacro(module: "MacroExamplesPlugin", type: "FontLiteralMacro")
|
||||
where T: ExpressibleByFontLiteral
|
||||
|
||||
/// Check if provided string literal is a valid URL and produce a non-optional
|
||||
/// URL value. Emit error otherwise.
|
||||
@freestanding(expression) public macro URL(_ stringLiteral: String) -> URL = #externalMacro(module: "MacroExamplesPlugin", type: "URLMacro")
|
||||
|
||||
/// Apply the specified attribute to each of the stored properties within the
|
||||
/// type or member to which the macro is attached. The string can be
|
||||
/// any attribute (without the `@`).
|
||||
@attached(memberAttribute)
|
||||
public macro wrapStoredProperties(_ attributeName: String) = #externalMacro(module: "MacroExamplesPlugin", type: "WrapStoredPropertiesMacro")
|
||||
|
||||
/// Wrap up the stored properties of the given type in a dictionary,
|
||||
/// turning them into computed properties.
|
||||
///
|
||||
/// This macro composes three different kinds of macro expansion:
|
||||
/// * Member-attribute macro expansion, to put itself on all stored properties
|
||||
/// of the type it is attached to.
|
||||
/// * Member macro expansion, to add a `_storage` property with the actual
|
||||
/// dictionary.
|
||||
/// * Accessor macro expansion, to turn the stored properties into computed
|
||||
/// properties that look for values in the `_storage` property.
|
||||
@attached(accessor)
|
||||
@attached(member, names: named(_storage))
|
||||
@attached(memberAttribute)
|
||||
public macro DictionaryStorage() = #externalMacro(module: "MacroExamplesPlugin", type: "DictionaryStorageMacro")
|
||||
|
||||
public protocol Observable {}
|
||||
|
||||
public protocol Observer<Subject> {
|
||||
associatedtype Subject: Observable
|
||||
}
|
||||
|
||||
public struct ObservationRegistrar<Subject: Observable> {
|
||||
public init() {}
|
||||
|
||||
public func addObserver(_: some Observer<Subject>) {}
|
||||
|
||||
public func removeObserver(_: some Observer<Subject>) {}
|
||||
|
||||
public func beginAccess<Value>(_ keyPath: KeyPath<Subject, Value>) {
|
||||
print("beginning access for \(keyPath)")
|
||||
}
|
||||
|
||||
public func beginAccess() {
|
||||
print("beginning access in \(Subject.self)")
|
||||
}
|
||||
|
||||
public func endAccess() {
|
||||
print("ending access in \(Subject.self)")
|
||||
}
|
||||
|
||||
public func register<Value>(observable _: Subject, willSet: KeyPath<Subject, Value>, to _: Value) {
|
||||
print("registering willSet event for \(willSet)")
|
||||
}
|
||||
|
||||
public func register<Value>(observable _: Subject, didSet: KeyPath<Subject, Value>) {
|
||||
print("registering didSet event for \(didSet)")
|
||||
}
|
||||
}
|
||||
|
||||
@attached(member, names: named(Storage), named(_storage), named(_registrar), named(addObserver), named(removeObserver), named(withTransaction))
|
||||
@attached(memberAttribute)
|
||||
@attached(conformance)
|
||||
public macro Observable() = #externalMacro(module: "MacroExamplesPlugin", type: "ObservableMacro")
|
||||
|
||||
@attached(accessor)
|
||||
public macro ObservableProperty() = #externalMacro(module: "MacroExamplesPlugin", type: "ObservablePropertyMacro")
|
||||
|
||||
/// Adds a "completionHandler" variant of an async function, which creates a new
|
||||
/// task , calls thh original async function, and delivers its result to the completion
|
||||
/// handler.
|
||||
@attached(peer, names: overloaded)
|
||||
public macro AddCompletionHandler() =
|
||||
#externalMacro(module: "MacroExamplesPlugin", type: "AddCompletionHandlerMacro")
|
||||
|
||||
@attached(peer, names: overloaded)
|
||||
public macro AddAsync() =
|
||||
#externalMacro(module: "MacroExamplesPlugin", type: "AddAsyncMacro")
|
||||
|
||||
/// Add computed properties named `is<Case>` for each case element in the enum.
|
||||
@attached(member, names: arbitrary)
|
||||
public macro CaseDetection() = #externalMacro(module: "MacroExamplesPlugin", type: "CaseDetectionMacro")
|
||||
|
||||
@attached(member, names: named(Meta))
|
||||
public macro MetaEnum() = #externalMacro(module: "MacroExamplesPlugin", type: "MetaEnumMacro")
|
||||
|
||||
@attached(member)
|
||||
public macro CodableKey(name: String) = #externalMacro(module: "MacroExamplesPlugin", type: "CodableKey")
|
||||
|
||||
@attached(member, names: named(CodingKeys))
|
||||
public macro CustomCodable() = #externalMacro(module: "MacroExamplesPlugin", type: "CustomCodable")
|
||||
|
||||
/// Create an option set from a struct that contains a nested `Options` enum.
|
||||
///
|
||||
/// Attach this macro to a struct that contains a nested `Options` enum
|
||||
/// with an integer raw value. The struct will be transformed to conform to
|
||||
/// `OptionSet` by
|
||||
/// 1. Introducing a `rawValue` stored property to track which options are set,
|
||||
/// along with the necessary `RawType` typealias and initializers to satisfy
|
||||
/// the `OptionSet` protocol.
|
||||
/// 2. Introducing static properties for each of the cases within the `Options`
|
||||
/// enum, of the type of the struct.
|
||||
///
|
||||
/// The `Options` enum must have a raw value, where its case elements
|
||||
/// each indicate a different option in the resulting option set. For example,
|
||||
/// the struct and its nested `Options` enum could look like this:
|
||||
///
|
||||
/// @MyOptionSet
|
||||
/// struct ShippingOptions {
|
||||
/// private enum Options: Int {
|
||||
/// case nextDay
|
||||
/// case secondDay
|
||||
/// case priority
|
||||
/// case standard
|
||||
/// }
|
||||
/// }
|
||||
@attached(member, names: arbitrary)
|
||||
@attached(conformance)
|
||||
public macro MyOptionSet<RawType>() = #externalMacro(module: "MacroExamplesPlugin", type: "OptionSetMacro")
|
||||
@@ -0,0 +1,185 @@
|
||||
// From https://github.com/DougGregor/swift-macro-examples/blob/main/MacroExamplesPlugin/OptionSetMacro.swift
|
||||
// swiftformat:options --indent 2
|
||||
|
||||
import SwiftDiagnostics
|
||||
import SwiftSyntax
|
||||
import SwiftSyntaxBuilder
|
||||
import SwiftSyntaxMacros
|
||||
|
||||
enum OptionSetMacroDiagnostic {
|
||||
case requiresStruct
|
||||
case requiresStringLiteral(String)
|
||||
case requiresOptionsEnum(String)
|
||||
case requiresOptionsEnumRawType
|
||||
}
|
||||
|
||||
extension OptionSetMacroDiagnostic: DiagnosticMessage {
|
||||
func diagnose(at node: some SyntaxProtocol) -> Diagnostic {
|
||||
Diagnostic(node: Syntax(node), message: self)
|
||||
}
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .requiresStruct:
|
||||
return "'OptionSet' macro can only be applied to a struct"
|
||||
|
||||
case let .requiresStringLiteral(name):
|
||||
return "'OptionSet' macro argument \(name) must be a string literal"
|
||||
|
||||
case let .requiresOptionsEnum(name):
|
||||
return "'OptionSet' macro requires nested options enum '\(name)'"
|
||||
|
||||
case .requiresOptionsEnumRawType:
|
||||
return "'OptionSet' macro requires a raw type"
|
||||
}
|
||||
}
|
||||
|
||||
var severity: DiagnosticSeverity { .error }
|
||||
|
||||
var diagnosticID: MessageID {
|
||||
MessageID(domain: "Swift", id: "OptionSet.\(self)")
|
||||
}
|
||||
}
|
||||
|
||||
/// The label used for the OptionSet macro argument that provides the name of
|
||||
/// the nested options enum.
|
||||
private let optionsEnumNameArgumentLabel = "optionsName"
|
||||
|
||||
/// The default name used for the nested "Options" enum. This should
|
||||
/// eventually be overridable.
|
||||
private let defaultOptionsEnumName = "Options"
|
||||
|
||||
extension TupleExprElementListSyntax {
|
||||
/// Retrieve the first element with the given label.
|
||||
func first(labeled name: String) -> Element? {
|
||||
first { element in
|
||||
if let label = element.label, label.text == name {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum OptionSetMacro {
|
||||
/// Decodes the arguments to the macro expansion.
|
||||
///
|
||||
/// - Returns: the important arguments used by the various roles of this
|
||||
/// macro inhabits, or nil if an error occurred.
|
||||
static func decodeExpansion(
|
||||
of attribute: AttributeSyntax,
|
||||
attachedTo decl: some DeclGroupSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) -> (StructDeclSyntax, EnumDeclSyntax, TypeSyntax)? {
|
||||
// Determine the name of the options enum.
|
||||
let optionsEnumName: String
|
||||
if case let .argumentList(arguments) = attribute.argument,
|
||||
let optionEnumNameArg = arguments.first(labeled: optionsEnumNameArgumentLabel)
|
||||
{
|
||||
// We have a options name; make sure it is a string literal.
|
||||
guard let stringLiteral = optionEnumNameArg.expression.as(StringLiteralExprSyntax.self),
|
||||
stringLiteral.segments.count == 1,
|
||||
case let .stringSegment(optionsEnumNameString)? = stringLiteral.segments.first
|
||||
else {
|
||||
context.diagnose(OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose(at: optionEnumNameArg.expression))
|
||||
return nil
|
||||
}
|
||||
|
||||
optionsEnumName = optionsEnumNameString.content.text
|
||||
} else {
|
||||
optionsEnumName = defaultOptionsEnumName
|
||||
}
|
||||
|
||||
// Only apply to structs.
|
||||
guard let structDecl = decl.as(StructDeclSyntax.self) else {
|
||||
context.diagnose(OptionSetMacroDiagnostic.requiresStruct.diagnose(at: decl))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find the option enum within the struct.
|
||||
guard let optionsEnum = decl.memberBlock.members.compactMap({ member in
|
||||
if let enumDecl = member.decl.as(EnumDeclSyntax.self),
|
||||
enumDecl.identifier.text == optionsEnumName
|
||||
{
|
||||
return enumDecl
|
||||
}
|
||||
|
||||
return nil
|
||||
}).first else {
|
||||
context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Retrieve the raw type from the attribute.
|
||||
guard let genericArgs = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.genericArgumentClause,
|
||||
let rawType = genericArgs.arguments.first?.argumentType
|
||||
else {
|
||||
context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute))
|
||||
return nil
|
||||
}
|
||||
|
||||
return (structDecl, optionsEnum, rawType)
|
||||
}
|
||||
}
|
||||
|
||||
extension OptionSetMacro: ConformanceMacro {
|
||||
public static func expansion(
|
||||
of attribute: AttributeSyntax,
|
||||
providingConformancesOf decl: some DeclGroupSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
|
||||
// Decode the expansion arguments.
|
||||
guard let (structDecl, _, _) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else {
|
||||
return []
|
||||
}
|
||||
|
||||
// If there is an explicit conformance to OptionSet already, don't add one.
|
||||
if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypeCollection,
|
||||
inheritedTypes.contains(where: { inherited in inherited.typeName.trimmedDescription == "OptionSet" })
|
||||
{
|
||||
return []
|
||||
}
|
||||
|
||||
return [("OptionSet", nil)]
|
||||
}
|
||||
}
|
||||
|
||||
extension OptionSetMacro: MemberMacro {
|
||||
public static func expansion(
|
||||
of attribute: AttributeSyntax,
|
||||
providingMembersOf decl: some DeclGroupSyntax,
|
||||
in context: some MacroExpansionContext
|
||||
) throws -> [DeclSyntax] {
|
||||
// Decode the expansion arguments.
|
||||
guard let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else {
|
||||
return []
|
||||
}
|
||||
|
||||
// Find all of the case elements.
|
||||
let caseElements = optionsEnum.memberBlock.members.flatMap { member in
|
||||
guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else {
|
||||
return [EnumCaseElementSyntax]()
|
||||
}
|
||||
|
||||
return Array(caseDecl.elements)
|
||||
}
|
||||
|
||||
// Dig out the access control keyword we need.
|
||||
let access = decl.modifiers?.first(where: \.isNeededAccessLevelModifier)
|
||||
|
||||
let staticVars = caseElements.map { element -> DeclSyntax in
|
||||
"""
|
||||
\(access) static let \(element.identifier): Self =
|
||||
Self(rawValue: 1 << \(optionsEnum.identifier).\(element.identifier).rawValue)
|
||||
"""
|
||||
}
|
||||
|
||||
return [
|
||||
"\(access)typealias RawValue = \(rawType)",
|
||||
"\(access)var rawValue: RawValue",
|
||||
"\(access)init() { self.rawValue = 0 }",
|
||||
"\(access)init(rawValue: RawValue) { self.rawValue = rawValue }",
|
||||
] + staticVars
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user