Streaming is available in most browsers,
and in the Developer app.
-
Expand on Swift macros
Discover how Swift macros can help you reduce boilerplate in your codebase and adopt complex features more easily. Learn how macros can analyze code, emit rich compiler errors to guide developers towards correct usage, and generate new code that is automatically incorporated back into your project. We'll also take you through important concepts like macro roles, compiler plugins, and syntax trees.
Chapters
- 0:00 - Introduction
- 0:51 - Why macros?
- 2:13 - Design philosophy
- 4:48 - Translation model
- 6:18 - Macro roles
- 17:48 - Macro implementation
- 33:36 - Writing correct macros
- 38:42 - Wrap up
Resources
Related Videos
WWDC23
-
Download
♪ ♪ Becca: Hi, I'm Becca from the Swift team. Today we'll be talking about Swift macros, an exciting new feature that lets you customize the Swift language for your needs. We'll start by talking about what macros are for.
Then, we'll talk about some of the principles we kept in mind when we designed Swift macros. And then we'll cover how Swift macros work and the specific ways they can interact with the other code in your project.
After that, we'll talk about how to implement macros and finish by discussing how to make sure your macros work correctly.
So let's get started by talking about why Swift supports macros. Swift loves to let users write expressive code and APIs. That's why it provides features like derived conformances and result builders, which help users to avoid writing repetitive boilerplate. These features all work basically the same way. For example, when you conform to Codable without providing implementations for its members, Swift automatically expands the conformance into a set of members that it inserts into the program. I've shown the expansion for the conformance in the gray box. Creating this code for you lets you use Codable without having to know exactly how it works and makes it so you don't have to decide if adding Codable support is worth writing a screenful of code. Swift has many features that work in this way. You write some simple syntax, and the compiler expands it into a more complicated piece of code automatically. But what if the existing features can't do what you want? Well, you could add a feature to the Swift compiler, since it's open source. But that literally involves me, personally, getting on a video conference and discussing your feature with other Swift project leaders, so it's not exactly a process that scales. That's why we're introducing macros. They let you add your own language features to Swift, eliminating tedium and boilerplate, in a way you can distribute in a Swift package without modifying the compiler. Some of you haven't used macros in other languages. But if you have, you may have mixed feelings about them. That's partly because a lot of Swift developers are familiar with Objective-C or other languages that use the C preprocessor, and they know about the limitations and pitfalls of C macros. But Swift macros are very different in ways that avoid many of those issues. We designed them with four goals in mind. The first goal is that it should be pretty obvious when you're using a macro. There are two kinds of macros: Freestanding macros stand in place of something else in your code. They always start with a pound (#) sign. And attached macros are used as attributes on declarations in your code. They always start with an at (@) sign. Swift already uses the pound (#) and at (@) signs to indicate special compiler behavior. Macros just make that extensible. And if you don't see #’s or @’s, you can be confident that there aren't any macros involved. The second goal is that both the code passed into a macro and the code sent back out of it should be complete and checked for mistakes. You can't pass "1 +” to a macro because arguments have to be complete expressions. Nor can you pass an argument with the wrong type because macro arguments and results are type-checked, just like function arguments. And a macro's implementation can validate its inputs and emit compiler warnings or errors if something's wrong, so it's easier to be certain that you're using a macro correctly. The third goal is that macro expansions should be incorporated into the program in predictable, additive ways. A macro can only add to the visible code in your program. It can't remove it or change it. So even if you have no idea what "someUnknownMacro" does, you can still be sure that it doesn't delete the call to "finishDoingThingy" or move it into a new function. That makes it a lot easier to read code that uses macros. And the final goal is that macros should not be impenetrable magic. Macros just add more code to your program, and that's something you can see right in Xcode.
You can right-click on a macro's use site and ask to see what it expands into. You can set breakpoints in the expansion or step into it with the debugger. When the code inside a macro expansion doesn't compile, you'll see both where the error is in the expansion, and where that expansion goes in your source code. And all of these tools work even if the macro is provided by a closed-source library. Macro authors can even write unit tests for their macros to make sure they work as expected; something we highly encourage they do.
We think these goals make Swift macros easy for developers to understand and maintain. So now that we understand what Swift macros are trying to achieve, let's talk about how they do it. Before we get lost in the details, let's just get the basic concept down. When Swift sees you call a macro in your code, like the "stringify" macro from the Xcode macro package template, it extracts that use from the code and sends it to a special compiler plug-in that contains the implementation for that macro. The plug-in runs as a separate process in a secure sandbox, and it contains custom Swift code written by the macro's author. It processes the macro use and returns an "expansion," a new fragment of code created by the macro. The Swift compiler then adds that expansion to your program and compiles your code and the expansion together. So when you run the program, it works just as though you wrote the expansion yourself instead of calling the macro. Now, there's an important point I glossed over here. How did Swift know that the "stringify" macro exists? And the answer is, it comes from a macro declaration. A macro declaration provides the API for a macro. You can write the declaration right in your own module, or you can import it from a library or framework. It specifies the macro's name and signature, the number of parameters it takes, their labels and types, and the type of the result if the macro has one, just like a function declaration. And it also has one or more attributes that specify the macro's roles. It's impossible to write a macro without thinking about what its roles are. So let's talk about what a role is and how you can use different roles to write different kinds of macros. A role is a set of rules for a macro. It governs where and how you apply the macro, what kind of code it expands into, and where that expansion is inserted into your code. Ultimately, it's macro roles that are responsible for achieving our goal of inserting expansions in predictable, additive ways.
There are two roles that create freestanding macros: Expression and declaration. And there are five roles that create attached macros: Peer, accessor, member attribute, member, and conformance. Let's take a look at these roles and when you might use them. We'll start with the "freestanding expression" role. If the term "expression" doesn't ring a bell, an expression is what we call a unit of code that executes and produces a result. In this "let" statement, the arithmetic after the equal sign is an expression. But expressions have a recursive structure-- they're often made up of smaller expressions. So "x + width" alone is also an expression. And so is just the word "width." A "freestanding expression" macro then is a macro that expands into an expression. How would you use one? Imagine you need to force-unwrap an optional. Swift provides a force-unwrap operator, but some teams feel that it's a little too easy to throw in a force-unwrap without thinking about its safety, so their style guides tell developers to write something more complex that indicates why the value should never be nil. But most of these alternatives, such as using a "guard let" and then calling "preconditionFailure" in the "else" branch, are a little too much ceremony. Let's design a macro that strikes a better balance between these extremes. We want this macro to compute and return a value, so we make it a "freestanding(expression)” macro. We give it the name "unwrap" and a generic type where the value passed in is optional, but the value returned is non-optional. And we also pass in a string that's part of the message printed if the unwrap fails. So we end up with a macro that we call just like a function, but it expands into an expression that contains a "guard let" wrapped in a closure. The error message even includes the variable name, something that would be impossible with a normal function. Now that we've seen the freestanding expression role, let's look at the freestanding declaration role. It expands into one or more declarations, like functions, variables, or types. What could you use it for? Imagine you're writing some kind of statistical analysis that needs a 2D array type. You want all of the rows in the array to have the same number of columns, so you don't want an array-of-arrays.
Instead, you want to store the elements in a flat, one-dimensional array, and then compute a one-dimensional index from the two-dimensional indices passed in by the developer. To do that, you might write a type like this one. The "makeIndex" function takes the two integers needed for a 2D index, and then does a little arithmetic to turn them into a 1D index. But then you find that, in another part of the program, you need a three-dimensional array. It's almost exactly the same as the 2D array. There's just a few more indices, and the calculation is a little more complex. And then you need a 4D array and then a 5D array, and soon you're swimming in array types that are almost identical, but not quite close enough to use generics, or protocol extensions, or subclasses, or any of the other features Swift offers for this kind of thing. Fortunately, each of these structs is a declaration, so we can use a declaration macro to create them. So let's declare a freestanding declaration macro with the name "makeArrayND," since it's going to create an N-dimensional array type. We'll pass the number of dimensions as an Int parameter, and we won't declare a result type because this macro will add a declaration to our program, not compute a result that's used by other code. Now we can call the macro four times with two, three, four, and five dimensions, and each of those calls will expand into an entire multi-dimensional array type with the right number of arguments and the right calculation for that size. So far, we've only looked at freestanding macros. Now let's move on to roles for attached macros. Attached macros are, as the name suggests, attached to a specific declaration. That means they have more information to work from. Freestanding macros are only given the arguments they're passed, but attached macros can also access the declaration they're attached to. They often inspect that declaration and pull out names, types, and other information from inside them. We'll start with the attached peer role. A peer macro can be attached to any declaration, not only variables, and functions, and types, but even things like import and operator declarations, and can insert new declarations alongside it. So if you use it on a method or property, you'll end up creating members of the type, but if you use it on a top-level function or type, you'll end up creating new top-level declarations. That makes them incredibly flexible. Here's one way you might use them. Suppose you're writing a library that uses Swift concurrency, but you know that some of your clients are still using older concurrency techniques, so you want to give them versions of your APIs that use completion handlers. It's not difficult to write these methods. You just remove the "async" keyword, add a completion handler parameter, move the result type into the parameter list, and call the async version in a detached task. But you're doing this a lot, and you don't want to have to write it by hand. That's a great job for an attached peer macro. We'll declare one called "AddCompletionHandler" and give it a parameter for the completion handler's argument label, and then attach that macro to the async version of the method. The macro will create a completion handler-based signature equivalent to the original, write the method body, and even attach a documentation comment with extra text for the completion handler. Pretty cool. Next, let's look at the attached accessor role. These can be attached to variables and subscripts, and they can install accessors into them, like "get," "set," "willSet," or "didSet". So how might that be useful? Suppose you have a bunch of types that basically wrap around dictionaries and let you access their contents with properties. So for instance, this "Person" struct lets you access the "name," "height," and "birth_date" fields, but if there's other information in the dictionary besides those three fields, it'll just be preserved and ignored by your program. These three properties need computed getters and setters, but writing them by hand is tedious, and we can't use property wrappers because property wrappers can't access other stored properties on the type they're used with. So let's write an attached accessor macro that can help with this. We'll call it "DictionaryStorage." We'll give it a "key" parameter because the dictionary spells "birth_date" with an underscore, but you can also just leave the key out, and it'll default to nil, which will make the macro use the property's name as the key. So now, instead of writing that big accessor block, you can just put “@DictionaryStorage" before each property, and the macro will generate the accessors for you. That's a nice improvement, but there's still some boilerplate here: The identical "DictionaryStorage" attributes. They're less boilerplate, but they're still boilerplate. Some built-in attributes let you deal with this kind of situation by applying them to the entire type or extension. The "attached member attribute" role can make your macros behave like that too. The macro gets attached to a type or extension, and it can add attributes to the members of whatever it's attached to. Let's see how it's done. We're going to do something a little different here. Rather than declaring a new macro, we'll add another role attribute to the "DictionaryStorage" macro, alongside the "attached accessor" role it already has. This is a really useful technique for creating macros. You're allowed to compose any combination of roles except the two freestanding roles because there are places where Swift wouldn't know which one to use. Swift will expand all of the roles that make sense wherever you applied them, but at least one of the roles has to work there. So if you attach "DictionaryStorage" to a type, Swift will expand the "member attribute" role. If you attach it to a property, Swift will expand the "accessor" role. But if you attach it to a function, you'll get a compilation error because "DictionaryStorage" doesn't have any roles that could attach to a function.
With this second role added to "DictionaryStorage," instead of attaching it separately to every property, you can just attach it to the whole type. The macro will have logic to skip certain members, like the initializer, the "dictionary" property, and properties like "birth_date" that already have a "DictionaryStorage" attribute. But it'll add a "DictionaryStorage" attribute to any other stored property, and then those attributes will expand into the accessors we already saw. That's a nice improvement, but there's still more boilerplate we could eliminate: The initializer and stored property. These are required by the "DictionaryRepresentable" protocol, and the property is used by the accessors, but they're exactly the same in any type that uses DictionaryStorage. Let's make the DictionaryStorage macro add them automatically, so we don't have to write them by hand. We can do that using the "attached member" role. Like member attribute macros, you can apply these macros to types and extensions, but instead of adding attributes to existing members, they add totally new members. So you can add methods, properties, initializers, and so on. You can even add stored properties to classes and structs, or cases to enums. Once again, we'll add a new "attached member" role to the DictionaryStorage macro, composing it with the other two. This new role will add an initializer and a property called "dictionary." You might be wondering, when two different macros get applied to the same code, which one gets expanded first? Then answer is, it doesn't really matter. Each one will see the original version of the declaration without expansions provided by the others. So you don't need to worry about ordering. You'll see the same thing no matter when the compiler expands your macro. With the attached member role added, we don't even have to write those two members anymore. Simply using DictionaryStorage on the type will automatically add them for us. And then the other role will add the DictionaryStorage attributes on the properties, and those attributes will expand into accessors, and so on.
But there's still one last bit of boilerplate to eliminate: The conformance to the DictionaryRepresentable protocol.
The "attached conformance" role is perfect for this. It can add a conformance to a type or extension. We'll add one last "attached conformance" role to the "DictionaryStorage" macro, composing it with the other three. This new role will add a conformance to "DictionaryRepresentation." So now we don't have to write the conformance manually. The DictionaryStorage attribute we already added for the accessors and generated members will now automatically add the conformance too, along with all the other stuff it was already doing. It's been a long time since we saw our starting point, so just to remind you, we took a big, unruly type full of repetitive code and moved most of that code into several roles of a super-powerful macro so that what's left concisely specifies only what's special about this particular type. Imagine if you had 10 or 20 types that could use DictionaryStorage. How much easier would it be to work with all of them? We've spent a bunch of time now talking about declarations and roles, but so far, the code they expand into has just seemed to magically appear. Let's fill in that gap now and talk about how you implement your macro. When I've showed you macro declarations so far, I've left out something very important: the implementation. It's after an equal sign, and it's always another macro. Sometimes it's another macro you've written, just with the parameters rearranged or with extra parameters specified as literals.
But usually, you'll use an external macro. An external macro is one that's implemented by a compiler plug-in. You might remember that I talked about compiler plug-ins earlier. I said that when the compiler sees a macro being used, it starts a plug-in in a separate process and asks it to expand the macro. "#externalMacro" is what defines that relationship. It specifies the plug-in the compiler should launch and the name of a type inside that plug-in. So when Swift expands this macro, it will launch a plug-in called "MyLibMacros" and ask a type called "StringifyMacro" to expand it. So the macro declaration goes in your normal library alongside your other APIs, but the macro implementation goes in a separate compiler plug-in module. And "#externalMacro" creates the link between the declaration and the type implementing it. What does a macro implementation look like? Well, let's take a look at how DictionaryStorage might be implemented. If you recall, our "DictionaryStorage" macro had an "attached member" role that added a stored property and an initializer to the type. Here's a simple implementation of that role. We'll walk through it one step at a time and learn how it works. Right at the top, we start by importing a library called SwiftSyntax. SwiftSyntax is a package maintained by the Swift project that helps you parse, inspect, manipulate, and generate Swift source code. Swift contributors keep SwiftSyntax up to date as the language evolves, so it supports every feature the Swift compiler does. SwiftSyntax represents source code as a special tree structure. For example, the "Person" struct in this code sample is represented as an instance of a type called "StructDeclSyntax." But that instance has properties, and each of those properties represents some portion of the struct declaration. The list of attributes is in the "attributes" property. The actual keyword "struct" is in the "structKeyword" property. The struct's name is in the "identifier" property. And the body with the curly braces and the struct's members is in the "memberBlock" property. There are also properties like "modifiers" that represent things that some struct declarations have. But this one doesn't. These are nil.
Some of the syntax nodes in these properties are called "tokens." These represent a specific piece of text in the source file, like a name, or a keyword, or a bit of punctuation, and they just contain that text and any surrounding trivia, like spaces and comments.
If you drill deep enough into the syntax tree, you'll find a token node that covers every byte of the source file. But some of those nodes, like the "AttributeListSyntax" node in the "attributes" property and the "MemberDeclBlockSyntax" node in the "memberBlock" property, are not tokens. These have child nodes in their own properties. For example, if we look inside the "memberBlock" property, we'll find a token for the opening curly brace, a "MemberDeclListSyntax" node for the list of members, and a token for the closing curly brace. And if you keep exploring the contents of that "MemberDeclListSyntax" node, you'll eventually find a node for each of the properties, and so on. Working with SwiftSyntax is a huge topic all its own, so rather than make this video twice as long, I'm going to refer you to two other resources. One is the companion "Write Swift Macros" session, which includes practical tips for figuring out how a particular piece of source code is represented as a syntax tree. The other is the SwiftSyntax package's documentation. You can find it online, or if you use Xcode's Build Documentation command in your macro package, SwiftSyntax docs will appear in the Developer Documentation window. In addition to the main SwiftSyntax library, we also import two other modules. One is "SwiftSyntaxMacros", which provides protocols and types necessary for writing macros. The other is called "SwiftSyntaxBuilder". This library provides convenience APIs for constructing syntax trees to represent newly-generated code. You can write a macro without using it, but it's incredibly handy, and we highly recommend you take advantage of it. Now that we've imported these libraries, we'll start actually writing the "DictionaryStorageMacro" type that our plug-in is supposed to provide. Notice that it conforms to a protocol called "MemberMacro." Each role has a corresponding protocol, and the implementation has to conform to the protocol for each role the macro provides. The "DictionaryStorage" macro has four of these roles, so the "DictionaryStorageMacro" type will need to conform to the four corresponding protocols. But to keep things simple, we're just worrying about the "MemberMacro" conformance for now. Moving on to the body of this type, we see a method called "expansion of, providingMembersOf, in." This method is required by the MemberMacro protocol, and it's what the Swift compiler calls to expand the member role when the macro is used. We're not using the arguments yet, but we'll talk about them later. For now, notice that it's a static method. All of the expansion methods are static, so Swift doesn't actually create an instance of the DictionaryStorageMacro type. It just uses it as a container for the methods. Each of the expansion methods returns SwiftSyntax nodes that are inserted into the source code. A member macro expands into a list of declarations to add as members to the type, so the expansion method for a member macro returns an array of "DeclSyntax" nodes. If we look inside the body, we see that array being created. It has the initializer and the stored property we want this macro to add. Now, the "var dictionary" bit here looks like it's an ordinary string, but it's actually not. This string literal is being written where a DeclSyntax is expected, so Swift actually treats it as a fragment of source code and asks the Swift parser to turn it into a DeclSyntax node. This is one of those conveniences that the SwiftSyntaxBuilder library provides. It's a good thing we imported it earlier. So with that and with conformances to the protocols for the other three roles, we'll have a working implementation of our DictionaryStorage macro. But although this macro will now work when you use it correctly, what happens if you use it wrong? For instance, what if you try to apply it to an enum instead of to a struct? Well, the "attached member" role will try to add a stored "dictionary" property. But an enum can't have stored properties, so Swift will produce an error: "Enums must not contain stored properties." It's great that Swift will stop this code from compiling, but the error message is a little confusing, isn't it? It's not really clear why the DictionaryStorage macro tried to create a stored property or what you should have done differently. I said earlier that one of Swift's goals was to allow macros to detect mistakes in their inputs and emit custom errors. So let's modify our macro's implementation to produce a much clearer error message for this: “@DictionaryStorage can only be applied to a struct." That will give developers a better idea of what they did wrong. The key to doing this will be the parameters to the expansion method, which we've ignored so far. The exact arguments are slightly different for different roles, but for a member macro, there are three. The first is called "attribute", and its type is AttributeSyntax. This is the actual DictionaryStorage attribute the developer wrote to use the macro. The second argument is called "declaration" and is a type that conforms to "DeclGroupSyntax." DeclGroupSyntax is a protocol that the nodes for structs, enums, classes, actors, protocols, and extensions all conform to. So this parameter gives us the declaration that the developer attached the attribute to. And the final parameter is called "context" and is of a type that conforms to "MacroExpansionContext". The context object is used when the macro implementation wants to communicate with the compiler. It can do a few different things, including emitting errors and warnings. We'll use all three of these parameters to emit our error. Let's see how it's done. First, we need to detect the problem. We'll do that by checking the type of the "declaration" parameter. Each kind of declaration has a different type, so if it's a struct, its type will be "StructDeclSyntax", if it's an enum, it'll be "EnumDeclSyntax", and so on. So we'll write a guard-else that calls the "declaration" parameter's "is" method and passes "StructDeclSyntax". If the declaration isn't a struct, we'll end up in the "else" block. For now, we'll return an empty array, so the macro doesn't add any code to the project, but what we really want to do is emit an error.
Now, the easy way to do it is to just throw an ordinary Swift error, but that doesn't give you very much control over the output. So instead, I'll show you the more complicated way that lets you create more sophisticated errors. The first step is to create an instance of a type called "Diagnostic". This is a bit of compiler jargon. Just as a doctor looking at an X-ray of your broken leg diagnoses a fracture, a compiler or macro looking at a syntax tree of your broken code diagnoses an error or warning. So we call the instance representing the error a “Diagnostic." A diagnostic contains at least two pieces of information. The first is the syntax node that the error occurred at, so the compiler knows which line to mark as incorrect. Here, we want to point to the DictionaryStorage attribute the user wrote, which, happily, is provided by the "attribute" parameter the method was passed. The second is the actual message you want the compiler to produce. You provide this by creating a custom type and then passing an instance of it. Let's take a quick look at it.
The "MyLibDiagnostic" type defines all of the diagnostics this module can produce. We've chosen to use an enum and provide a case for each diagnostic, but you could use another kind of type if you wanted. This type works sort of like a throwable Swift error. It conforms to the "DiagnosticMessage" protocol, and it has a bunch of properties that provide information about the diagnostic. One of the most important is the "severity" property. It specifies whether the diagnostic is an error or a warning.
Then there's the "message" property, which produces the actual error message, and the "diagnosticID" property. You should use the plug-in's module name for the domain and some kind of unique string for the ID. I've chosen to use string raw values for this enum, but that's just a convenience. So with the message in hand, you can create the diagnostic. Then you tell the context to diagnose it, and you're done.
That's a pretty basic diagnostic, but if you want, you can get a lot fancier with them. For example, you can add Fix-Its to a diagnostic that are automatically applied by the Xcode Fix button. You can also add highlights and attach notes pointing to other locations in the code. So you can really provide a first-class error experience for your developers. But once you've made sure your macro is being applied correctly, you still need to actually create the expansion. SwiftSyntax gives you several different tools to do that. Syntax nodes are immutable, but they have lots of APIs that either create new nodes or return modified versions of existing nodes. The SwiftSyntaxBuilder library adds SwiftUI-style syntax builders where some of the child nodes are specified by a trailing closure. For example, the multidimensional array macro can use a syntax builder to generate whatever number of parameters is appropriate for the type it's creating. And the string literal feature we used to make the DictionaryStorage property and initializer also supports interpolations.
All of these features are useful in different situations, and you'll probably find yourself combining several in particularly complicated macros. But the string literal feature is especially good at producing syntax trees for large amounts of code, and there's a bit to learn about its interpolation features. So let's look at how you might use those to generate some code. Earlier, we talked about the "unwrap" macro. It takes an optional value and a message string and expands into a "guard let" wrapped in a closure. The general shape of this code is always going to be the same, but a lot of the contents are customized for the specific use site. Let's focus in on the "guard let" statement and see how we could write a function to generate just that statement. To start, we'll just take that exact code sample we just saw and put it in a helper method called "makeGuardStatement" that returns a Statement Syntax node. Then we'll slowly add interpolations to replace all the stuff that needs to be different depending on where it's used. The first thing we'll do is add the right message string. The message string is an arbitrary expression, so we'll pass it in as an ExprSyntax node and then interpolate it in. An ordinary interpolation like this can add a syntax node to the code, but it can't add a plain String. That's a safety feature to keep you from inserting invalid code by accident. The guard-let condition is similar, except that it's just a variable name, so it's a token, not an expression. No matter, we add a TokenSyntax parameter and interpolate it in, just as we interpolated the expression. There's a trickier case when you add the expression being unwrapped to the error message. One of the features of our macro is that when it fails, it prints out the code you were trying to unwrap. That means we need to create a string literal that contains a stringified version of a syntax node.
Let's start by pulling the prefix out of the Statement Syntax literal and into a variable that's just a plain string. We'll interpolate that string in, but we'll use a special interpolation that starts with "literal:". When you do this, SwiftSyntax will add the contents of the string as a string literal. This also works for making literals from other kinds of information computed by the macro, numbers, Booleans, arrays, dictionaries, and even optionals. Now that we're building up the string in a variable, we can change it to have the right code in the message. Just add a parameter for the original expression, and interpolate its "description" property into the string. You don't need to do anything special to escape it. The "literal:" interpolation will automatically detect if the string contains special characters and add escapes or switch to a raw literal to make sure the code is valid. So the "literal:" interpolation makes it super easy to do the right thing. The last thing to deal with are the file and line numbers. These are a little tricky because the compiler doesn't tell the macro the source location it's expanding into. However, the macro expansion context has an API you can use to generate special syntax nodes that the compiler will turn into literals with source location info. So let's see how that's done. We'll add another argument for the macro expansion context, and then we'll use its "location of" method. This returns an object that can produce syntax nodes for the location of whatever node you provide. It will return nil if the node is one that your macro created, rather than one that the compiler passed in to you, but we know that "originalWrapped" is one of the arguments that the user wrote, so its location will never be nil, and we can safely force-unwrap the result. Now all you have to do is interpolate the syntax nodes for the file and line number, and you're done. We're now generating the right "guard" statement. So far, we've discussed how to make macros work at all. But let's move on and talk about how to make them work well. And we'll start by talking about name collisions. When we looked at the "unwrap" macro before, we looked at an example where we unwrapped a simple variable name.
But if we try to unwrap a more complicated expression, the macro has to expand differently. It generates code which captures the expression's result into a variable called "wrappedValue," and then unwraps that.
But what happens if you try to use a variable called "wrappedValue" in the message? When the compiler goes looking for "wrappedValue", it'll end up finding the closer one, so it'll use that instead of the one you actually meant.
You could try to fix this by picking a name that you think your users probably won't use by accident, but wouldn't it be better to make this impossible? That's what the "makeUniqueName" method on the Macro Expansion Context does. It returns a variable name that's guaranteed to not be used in user code or in any other macro expansion, so you can be sure that the message string won't accidentally refer to it. Some of you might be wondering, why doesn't Swift automatically stop that from happening? Some languages have so-called "hygienic" macro systems, where the names inside a macro are distinct from the names outside, so they can't conflict with each other.
Swift isn't like that because we've found that a lot of macros need to use names from outside themselves. Think of the DictionaryStorage macro, which uses a "dictionary" property on the type. If "dictionary" inside a macro meant something different from "dictionary" outside, it'd be pretty hard to make that work.
And sometimes, you even want to introduce a whole new name that non-macro code can access. Peer macros, member macros, and declaration macros basically exist entirely to do this. But when they do, they need to declare the names they're adding, so the compiler knows about them. And they do that inside their role attribute.
You might not have noticed it before, but we've actually seen these declarations all along. The "member" role on the DictionaryStorage macro had a "names:" parameter that specified the names "dictionary" and "init". And in fact, most of the macros we've looked at in this session have at least one role with a "names" argument.
There are five name specifiers you can use: "Overloaded" means that the macro adds declarations with the exact same base name as whatever the macro is attached to. "Prefixed" means that the macro adds declarations with the same base name, except with the specified prefix added. "Suffixed" is the same thing, except with a suffix instead of a prefix. "Named" means that the macro adds declarations with a specific, fixed base name. And "arbitrary" means that the macro adds declarations with some other name that can't be described using any of these rules. It's really common to use "arbitrary." For example, our multidimensional array macro declares a type with a name that's computed from one of its parameters, so it needs to specify "arbitrary." But when you can use one of the other specifiers, please do so. It'll make both the compiler and other tools like code completion faster. Now, at this point in the session, I'm guessing you're all raring to write your first macro. And you might have a great idea of a way to start: Just write a macro that inserts the date and time when it was expanded. Great idea, right? Wrong. It turns out, you must not write this macro. Let me explain why. Macros need to use only the information the compiler provides to them. The compiler assumes that macro implementations are pure functions, and that, if the data it provided hasn't changed, then the expansion can't change either.
If you circumvent that, you might see inconsistent behavior.
Now, the macro system is designed to prevent some kinds of behavior that could violate this rule. Compiler plug-ins run in a sandbox that stops macro implementations from reading files on disk or accessing the network.
But the sandbox doesn't block every bad action. You could use APIs to get information like the date or random numbers, or you could save information from one expansion in a global variable and use it in another expansion. But if you do these things, your macro might misbehave. So don't. Last, but absolutely not least, let's talk about testing. Your macro plug-in is just an ordinary Swift module, which means that you can, and definitely should, write normal unit tests for it.
Test-driven development is an incredibly effective approach to developing Swift macros. The "assertMacroExpansion" helper from SwiftSyntaxMacrosTestSupport will check that a macro produces the right expansion. Just give it an example of the macro and the code it should expand into, and it'll make sure they match. So we've learned a lot about Swift macros today. Macros let you reduce boilerplate by designing new language features that "expand" a small use site into a more complicated piece of code. You declare a macro alongside other APIs, typically in a library, but you actually implement it in a separate plug-in that runs Swift code in a secure sandbox. A macro's roles express where you can use it and how its expansion is integrated into the rest of the program. And you can, and definitely should, write unit tests for your macros to make sure they work as expected. If you haven't watched it already, the "Write Swift Macros" session should be your next stop. It will show you how to work with Xcode's macro development tools and macro package template, how to inspect SwiftSyntax trees and pull information out of them, and how to build a macro development workflow around your unit tests. So thanks for watching, and happy coding. ♪ ♪
-
-
0:44 - The #unwrap expression macro, with a more complicated argument
let image = #unwrap(request.downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
0:50 - Existing features using expansions (1)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
1:11 - Existing features using expansions (2)
struct Smoothie: Codable { var id, title, description: String var measuredIngredients: [MeasuredIngredient] // Begin expansion for Codable private enum CodingKeys: String, CodingKey { case id, title, description, measuredIngredients } init(from decoder: Decoder) throws { … } func encode(to encoder Encoder) throws { … } // End expansion for Codable static let berryBlue = Smoothie(id: "berry-blue", title: "Berry Blue") { """ Filling and refreshing, this smoothie \ will fill you with joy! """ Ingredient.orange .measured(with: .cups).scaled(by: 1.5) Ingredient.blueberry .measured(with: .cups) Ingredient.avocado .measured(with: .cups).scaled(by: 0.2) } }
-
3:16 - Macros inputs are complete, type-checked, and validated
#unwrap(1 + ) // error: expected expression after operator @AddCompletionHandler(parameterName: 42) // error: cannot convert argument of type 'Int' to expected type 'String' func sendRequest() async throws -> Response @DictionaryStorage class Options { … } // error: '@DictionaryStorage' can only be applied to a 'struct'
-
3:45 - Macro expansions are inserted in predictable ways
func doThingy() { startDoingThingy() #someUnknownMacro() finishDoingThingy() }
-
4:51 - How macros work, featuring #stringify
func printAdd(_ a: Int, _ b: Int) { let (result, str) = #stringify(a + b) // Begin expansion for "#stringify" (a + b, "a + b") // End expansion for "#stringify" print("\(str) = \(result)") } printAdd(1, 2) // prints "a + b = 3"
-
5:43 - Macro declaration for #stringify
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String)
-
7:11 - What’s an expression?
let numPixels = (x + width) * (y + height) // ^~~~~~~~~~~~~~~~~~~~~~~~~~ This is an expression // ^~~~~~~~~ But so is this // ^~~~~ And this
-
7:34 - The #unwrap expression macro: motivation
// Some teams are nervous about this: let image = downloadedImage! // Alternatives are super wordy: guard let image = downloadedImage else { preconditionFailure("Unexpectedly found nil: downloadedImage was already checked") }
-
8:03 - The #unwrap expression macro: macro declaration
/// Force-unwraps the optional value passed to `expr`. /// - Parameter message: Failure message, followed by `expr` in single quotes @freestanding(expression) macro unwrap<Wrapped>(_ expr: Wrapped?, message: String) -> Wrapped
-
8:21 - The #unwrap expression macro: usage
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
9:09 - The #makeArrayND declaration macro: motivation
public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance } public struct Array3D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1, width2: Int public func makeIndex(_ i0: Int, _ i1: Int, _ i2: Int) -> Index { Index(storageIndex: (i0 * width1 + i1) * width2 + i2) } public subscript (_ i0: Int, _ i1: Int, _ i2: Int) -> Element { get { self[makeIndex(i0, i1, i2)] } set { self[makeIndex(i0, i1, i2)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } // Note: Omitted additional members needed for 'Collection' conformance }
-
10:03 - The #makeArrayND declaration macro: macro declaration
/// Declares an `n`-dimensional array type named `Array<n>D`. /// - Parameter n: The number of dimensions in the array. @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int)
-
10:15 - The #makeArrayND declaration macro: usage
#makeArrayND(n: 2) // Begin expansion for "#makeArrayND" public struct Array2D<Element>: Collection { public struct Index: Hashable, Comparable { var storageIndex: Int } var storage: [Element] var width1: Int public func makeIndex(_ i0: Int, _ i1: Int) -> Index { Index(storageIndex: i0 * width1 + i1) } public subscript (_ i0: Int, _ i1: Int) -> Element { get { self[makeIndex(i0, i1)] } set { self[makeIndex(i0, i1)] = newValue } } public subscript (_ i: Index) -> Element { get { storage[i.storageIndex] } set { storage[i.storageIndex] = newValue } } } // End expansion for "#makeArrayND" #makeArrayND(n: 3) #makeArrayND(n: 4) #makeArrayND(n: 5)
-
11:23 - The @AddCompletionHandler peer macro: motivation
/// Fetch the avatar for the user with `username`. func fetchAvatar(_ username: String) async -> Image? { ... } func fetchAvatar(_ username: String, onCompletion: @escaping (Image?) -> Void) { Task.detached { onCompletion(await fetchAvatar(username)) } }
-
11:51 - The @AddCompletionHandler peer macro: macro declaration
/// Overload an `async` function to add a variant that takes a completion handler closure as /// a parameter. @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler")
-
11:59 - The @AddCompletionHandler peer macro: usage
/// Fetch the avatar for the user with `username`. @AddCompletionHandler(parameterName: "onCompletion") func fetchAvatar(_ username: String) async -> Image? { ... } // Begin expansion for "@AddCompletionHandler" /// Fetch the avatar for the user with `username`. /// Equivalent to ``fetchAvatar(username:)`` with /// a completion handler. func fetchAvatar( _ username: String, onCompletion: @escaping (Image?) -> Void ) { Task.detached { onCompletion(await fetchAvatar(username)) } } // End expansion for "@AddCompletionHandler"
-
12:36 - The @DictionaryStorage accessor macro: motivation
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } }
-
13:04 - The @DictionaryStorage accessor macro: declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
13:20 - The @DictionaryStorage accessor macro: usage
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] @DictionaryStorage var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
13:56 - The @DictionaryStorage member attribute macro: macro declaration
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
14:46 - The @DictionaryStorage member attribute macro: usage
@DictionaryStorage struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
15:52 - The @DictionaryStorage member macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
16:26 - The @DictionaryStorage member macro: usage
// The @DictionaryStorage member macro @DictionaryStorage struct Person: DictionaryRepresentable { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
16:59 - The @DictionaryStorage conformance macro: macro definition
/// Adds accessors to get and set the value of the specified property in a dictionary /// property called `storage`. @attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil)
-
17:09 - The @DictionaryStorage conformance macro: usage
struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
17:28 - @DictionaryStorage starting point
struct Person: DictionaryRepresentable { init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } var height: Measurement<UnitLength> { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } var birthDate: Date? { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } }
-
17:32 - @DictionaryStorage ending point
@DictionaryStorage struct Person // Begin expansion for "@DictionaryStorage" : DictionaryRepresentable // End expansion for "@DictionaryStorage" { // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var name: String // Begin expansion for "@DictionaryStorage" { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } // End expansion for "@DictionaryStorage" // Begin expansion for "@DictionaryStorage" @DictionaryStorage // End expansion for "@DictionaryStorage" var height: Measurement<UnitLength> // Begin expansion for "@DictionaryStorage" { get { dictionary["height"]! as! Measurement<UnitLength> } set { dictionary["height"] = newValue } } // End expansion for "@DictionaryStorage" @DictionaryStorage(key: "birth_date") var birthDate: Date? // Begin expansion for "@DictionaryStorage" { get { dictionary["birth_date"] as! Date? } set { dictionary["birth_date"] = newValue as Any? } } // End expansion for "@DictionaryStorage" }
-
17:35 - @DictionaryStorage ending point (without expansions)
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
18:01 - Macro implementations
/// Creates a tuple containing both the result of `expr` and its source code represented as a /// `String`. @freestanding(expression) macro stringify<T>(_ expr: T) -> (T, String) = #externalMacro( module: "MyLibMacros", type: "StringifyMacro" )
-
19:18 - Implementing @DictionaryStorage’s @attached(member) role (1)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } }
-
19:52 - Code used to demonstrate SwiftSyntax trees
@DictionaryStorage struct Person { var name: String var height: Measurement<UnitLength> @DictionaryStorage(key: "birth_date") var birthDate: Date? }
-
22:00 - Implementing @DictionaryStorage’s @attached(member) role (2)
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } }
-
24:29 - A type that @DictionaryStorage isn’t compatible with
@DictionaryStorage enum Gender { case other(String) case female case male // Begin expansion for "@DictionaryStorage" init(dictionary: [String: Any]) { self.dictionary = dictionary } var dictionary: [String: Any] // End expansion for "@DictionaryStorage" }
-
25:17 - Expansion method with error checking
import SwiftSyntax import SwiftSyntaxMacros import SwiftSyntaxBuilder struct DictionaryStorageMacro: MemberMacro { static func expansion( of attribute: AttributeSyntax, providingMembersOf declaration: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { guard declaration.is(StructDeclSyntax.self) else { let structError = Diagnostic( node: attribute, message: MyLibDiagnostic.notAStruct ) context.diagnose(structError) return [] } return [ "init(dictionary: [String: Any]) { self.dictionary = dictionary }", "var dictionary: [String: Any]" ] } } enum MyLibDiagnostic: String, DiagnosticMessage { case notAStruct var severity: DiagnosticSeverity { return .error } var message: String { switch self { case .notAStruct: return "'@DictionaryStorage' can only be applied to a 'struct'" } } var diagnosticID: MessageID { MessageID(domain: "MyLibMacros", id: rawValue) } }
-
29:32 - Parameter list for `ArrayND.makeIndex`
FunctionParameterListSyntax { for dimension in 0 ..< numDimensions { FunctionParameterSyntax( firstName: .wildcardToken(), secondName: .identifier("i\(dimension)"), type: TypeSyntax("Int") ) } }
-
30:17 - The #unwrap expression macro: revisited
let image = #unwrap(downloadedImage, message: "was already checked") // Begin expansion for "#unwrap" { [downloadedImage] in guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } return downloadedImage }() // End expansion for "#unwrap"
-
30:38 - Implementing the #unwrap expression macro: start
static func makeGuardStmt() -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + "was already checked", file: "main/ImageLoader.swift", line: 42 ) } """ }
-
30:57 - Implementing the #unwrap expression macro: the message string
static func makeGuardStmt(message: ExprSyntax) -> StmtSyntax { return """ guard let downloadedImage else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:21 - Implementing the #unwrap expression macro: the variable name
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { return """ guard let \(wrapped) else { preconditionFailure( "Unexpectedly found nil: ‘downloadedImage’ " + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
31:44 - Implementing the #unwrap expression macro: interpolating a string as a literal
static func makeGuardStmt(wrapped: TokenSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘downloadedImage’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
32:11 - Implementing the #unwrap expression macro: adding an expression as a string
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: "main/ImageLoader.swift", line: 42 ) } """ }
-
33:00 - Implementing the #unwrap expression macro: inserting the file and line numbers
static func makeGuardStmt(wrapped: TokenSyntax, originalWrapped: ExprSyntax, message: ExprSyntax, in context: some MacroExpansionContext) -> StmtSyntax { let messagePrefix = "Unexpectedly found nil: ‘\(originalWrapped.description)’ " let originalLoc = context.location(of: originalWrapped)! return """ guard let \(wrapped) else { preconditionFailure( \(literal: messagePrefix) + \(message), file: \(originalLoc.file), line: \(originalLoc.line) ) } """ }
-
34:05 - The #unwrap expression macro, with a name conflict
let wrappedValue = "🎁" let image = #unwrap(request.downloadedImage, message: "was \(wrappedValue)") // Begin expansion for "#unwrap" { [wrappedValue = request.downloadedImage] in guard let wrappedValue else { preconditionFailure( "Unexpectedly found nil: ‘request.downloadedImage’ " + "was \(wrappedValue)", file: "main/ImageLoader.swift", line: 42 ) } return wrappedValue }() // End expansion for "#unwrap"
-
34:30 - The MacroExpansion.makeUniqueName() method
let captureVar = context.makeUniqueName() return """ { [\(captureVar) = \(originalWrapped)] in \(makeGuardStmt(wrapped: captureVar, …)) \(makeReturnStmt(wrapped: captureVar)) } """
-
35:44 - Declaring a macro’s names
@attached(conformance) @attached(member, names: named(dictionary), named(init(dictionary:))) @attached(memberAttribute) @attached(accessor) macro DictionaryStorage(key: String? = nil) @attached(peer, names: overloaded) macro AddCompletionHandler(parameterName: String = "completionHandler") @freestanding(declaration, names: arbitrary) macro makeArrayND(n: Int)
-
38:28 - Macros are testable
import MyLibMacros import XCTest import SwiftSyntaxMacrosTestSupport final class MyLibTests: XCTestCase { func testMacro() { assertMacroExpansion( """ @DictionaryStorage var name: String """, expandedSource: """ var name: String { get { dictionary["name"]! as! String } set { dictionary["name"] = newValue } } """, macros: ["DictionaryStorage": DictionaryStorageMacro.self]) } }
-
-
Looking for something specific? Enter a topic above and jump straight to the good stuff.
An error occurred when submitting your query. Please check your Internet connection and try again.