Skip to content

Commit

Permalink
Add example macros for member-attribute, member, and accessor macros
Browse files Browse the repository at this point in the history
Note that this change requires some in-flight fixes that haven't
made it to a snapshot release yet.
  • Loading branch information
DougGregor committed Feb 2, 2023
1 parent 9a49431 commit 94de859
Show file tree
Hide file tree
Showing 6 changed files with 248 additions and 2 deletions.
8 changes: 8 additions & 0 deletions MacroExamples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

/* Begin PBXBuildFile section */
BD2CDEE9298B24040015A701 /* Diagnostics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2CDEE8298B24040015A701 /* Diagnostics.swift */; };
BD2CDEEB298B34730015A701 /* WrapStoredPropertiesMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2CDEEA298B34730015A701 /* WrapStoredPropertiesMacro.swift */; };
BD2CDEED298B4A650015A701 /* DictionaryIndirectionMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD2CDEEC298B4A650015A701 /* DictionaryIndirectionMacro.swift */; };
BD752BE5294D3BEC00D00A2E /* WarningMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD752BE4294D3BEC00D00A2E /* WarningMacro.swift */; };
BD752BE7294D461B00D00A2E /* FontLiteralMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD752BE6294D461B00D00A2E /* FontLiteralMacro.swift */; };
BD841F82294CE1F600DA4D81 /* AddBlocker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD841F81294CE1F600DA4D81 /* AddBlocker.swift */; };
Expand Down Expand Up @@ -64,6 +66,8 @@

/* Begin PBXFileReference section */
BD2CDEE8298B24040015A701 /* Diagnostics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Diagnostics.swift; sourceTree = "<group>"; };
BD2CDEEA298B34730015A701 /* WrapStoredPropertiesMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapStoredPropertiesMacro.swift; sourceTree = "<group>"; };
BD2CDEEC298B4A650015A701 /* DictionaryIndirectionMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryIndirectionMacro.swift; sourceTree = "<group>"; };
BD3FE05C296611F200426C82 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
BD752BE4294D3BEC00D00A2E /* WarningMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WarningMacro.swift; sourceTree = "<group>"; };
BD752BE6294D461B00D00A2E /* FontLiteralMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontLiteralMacro.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -120,6 +124,8 @@
BD752BE4294D3BEC00D00A2E /* WarningMacro.swift */,
BD752BE6294D461B00D00A2E /* FontLiteralMacro.swift */,
BD2CDEE8298B24040015A701 /* Diagnostics.swift */,
BD2CDEEA298B34730015A701 /* WrapStoredPropertiesMacro.swift */,
BD2CDEEC298B4A650015A701 /* DictionaryIndirectionMacro.swift */,
);
path = MacroExamplesPlugin;
sourceTree = "<group>";
Expand Down Expand Up @@ -356,9 +362,11 @@
buildActionMask = 2147483647;
files = (
BD841F82294CE1F600DA4D81 /* AddBlocker.swift in Sources */,
BD2CDEEB298B34730015A701 /* WrapStoredPropertiesMacro.swift in Sources */,
BD752BE5294D3BEC00D00A2E /* WarningMacro.swift in Sources */,
BDF5AFF82947E95C00FA119B /* StringifyMacro.swift in Sources */,
BD2CDEE9298B24040015A701 /* Diagnostics.swift in Sources */,
BD2CDEED298B4A650015A701 /* DictionaryIndirectionMacro.swift in Sources */,
BD752BE7294D461B00D00A2E /* FontLiteralMacro.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
25 changes: 25 additions & 0 deletions MacroExamples/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,28 @@ testStringify()
blockAdd()
warningAndError()
testFontLiteral()

// 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
}

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)")
22 changes: 22 additions & 0 deletions MacroExamplesLib/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,25 @@ public protocol ExpressibleByFontLiteral {
/// 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


/// 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")
85 changes: 85 additions & 0 deletions MacroExamplesPlugin/DictionaryIndirectionMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import SwiftSyntax
import SwiftSyntaxMacros

public struct DictionaryStorageMacro { }

extension DictionaryStorageMacro: AccessorMacro {
public static func expansion<
Context: MacroExpansionContext,
Declaration: DeclSyntaxProtocol
>(
of node: AttributeSyntax,
providingAccessorsOf declaration: Declaration,
in context: Context
) throws -> [AccessorDeclSyntax] {
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier,
binding.accessor == nil,
let type = binding.typeAnnotation?.type
else {
return []
}

// Ignore the "_storage" variable.
if identifier.text == "_storage" {
return []
}

guard let defaultValue = binding.initializer?.value else {
throw CustomError.message("stored property must have an initializer")
}

print(varDecl.recursiveDescription)
return [
"""
get {
_storage[\(literal: identifier.text), default: \(defaultValue)] as! \(type)
}
""",
"""
set {
_storage[\(literal: identifier.text)] = newValue
}
""",
]
}
}

extension DictionaryStorageMacro: MemberMacro {
public static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let storage: DeclSyntax = "var _storage: [String: Any] = [:]"
return [
storage.with(\.leadingTrivia, [.newlines(1), .spaces(2)])
]
}
}

extension DictionaryStorageMacro: MemberAttributeMacro {
public static func expansion(
of node: AttributeSyntax, attachedTo declaration: some DeclGroupSyntax,
providingAttributesFor member: DeclSyntax,
in context: some MacroExpansionContext
) throws -> [AttributeSyntax] {
guard let property = member.as(VariableDeclSyntax.self),
property.isStoredProperty
else {
return []
}

return [
AttributeSyntax(
attributeName: SimpleTypeIdentifierSyntax(
name: .identifier("DictionaryStorage")
)
)
.with(\.leadingTrivia, [.newlines(1), .spaces(2)])
]
}
}
99 changes: 99 additions & 0 deletions MacroExamplesPlugin/WrapStoredPropertiesMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

/// Implementation of the `wrapStoredProperties` macro, which can be
/// used to apply an attribute to all of the stored properties of a type.
///
/// This macro demonstrates member-attribute macros, which allow an attribute
/// written on a type or extension to apply attributes to the members
/// declared within that type or extension.
public struct WrapStoredPropertiesMacro: MemberAttributeMacro {
public static func expansion<
Declaration: DeclGroupSyntax,
Context: MacroExpansionContext
>(
of node: AttributeSyntax,
attachedTo decl: Declaration,
providingAttributesFor member: DeclSyntax,
in context: Context
) throws -> [AttributeSyntax] {
guard let property = member.as(VariableDeclSyntax.self),
property.isStoredProperty
else {
return []
}

guard case let .argumentList(arguments) = node.argument,
let firstElement = arguments.first,
let stringLiteral = firstElement.expression
.as(StringLiteralExprSyntax.self),
stringLiteral.segments.count == 1,
case let .stringSegment(wrapperName)? = stringLiteral.segments.first else {
throw CustomError.message("macro requires a string literal containing the name of an attribute")
}

return [
AttributeSyntax(
attributeName: SimpleTypeIdentifierSyntax(
name: .identifier(wrapperName.content.text)
)
)
.with(\.leadingTrivia, [.newlines(1), .spaces(2)])
]
}
}

extension VariableDeclSyntax {
/// Determine whether this variable has the syntax of a stored property.
///
/// This syntactic check cannot account for semantic adjustments due to,
/// e.g., accessor macros or property wrappers.
var isStoredProperty: Bool {
if bindings.count != 1 {
return false
}

let binding = bindings.first!
switch binding.accessor {
case .none:
return true

case .accessors(let node):
for accessor in node.accessors {
switch accessor.accessorKind.tokenKind {
case .keyword(.willSet), .keyword(.didSet):
// Observers can occur on a stored property.
break

default:
// Other accessors make it a computed property.
return false
}
}

return true

case .getter:
return false

@unknown default:
return false
}
}
}

extension DeclGroupSyntax {
/// Enumerate the stored properties that syntactically occur in this
/// declaration.
func storedProperties() -> [VariableDeclSyntax] {
return members.members.compactMap { member in
guard let variable = member.decl.as(VariableDeclSyntax.self),
variable.isStoredProperty else {
return nil
}

return variable
}
}
}
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ Macros are an experimental feature, so you will need a custom Swift toolchain an
3. Go to the Xcode -> Toolchains menu and select the development toolchain you downloaded.
4. Make sure the `MacroExamples` scheme is selected, then build and run! If the first build fails, build again--there's something funky going on with the dependencies.

The output of the `MacroExamples` program is pretty simple: it shows the result of running the example macro(s).
The output of the `MacroExamples` program is pretty simple: it shows the result of running the example macro(s). The `main.swift` file is annotated to describe what the macros are actually doing.

## Example macros

A number of macros in this package are designed to illustrate different capabilities of the macro system:
* `#addBlocker`: demonstrates how a freestanding macro can emit compiler diagnostics based on the source code for the macro argument, by producing a warning for each use of the binary `+` operator with range highlighting and a Fix-It to replace the `+` with `-`.
* `@DictionaryStorage`: demonstrates how an attached macro can have multiple roles that compose together to move all of the stored properties of a type into a separate dictionary.

## Adding your own macro

Expand All @@ -30,7 +36,8 @@ This examples package is meant to grow to include additional macros that have in
* **Declaration**: a macro is declared in the `MacroExamplesLib` target, using the `macro` introducer. For example, the simple `stringify` macro is declared like this:

```swift
public macro stringify<T>(_ value: T) -> (T, String) = MacroExamplesPlugin.StringifyMacro
@freestanding(expression)
public macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MacroExamplesPlugin", type: "StringifyMacro")
```

The name after `macro` is the name to be used in source code, whereas the name after the `=` is the module and type name for your macro implementation. If you haven't implemented that type, or get the name wrong, you will get a compiler warning.
Expand Down

0 comments on commit 94de859

Please sign in to comment.