Add macro examples

This commit is contained in:
Nick Lockwood
2023-05-16 18:47:23 +01:00
parent f72c8d46a3
commit 5c88fb1b05
3 changed files with 532 additions and 0 deletions
+196
View File
@@ -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)
+151
View File
@@ -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")
+185
View File
@@ -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
}
}