EP 53 · Standalone · Apr 8, 2019 ·Members

Video #53: Swift Syntax Enum Properties

smart_display

Loading stream…

Video #53: Swift Syntax Enum Properties

Episode: Video #53 Date: Apr 8, 2019 Access: Members Only 🔒 URL: https://www.pointfree.co/episodes/ep53-swift-syntax-enum-properties

Episode thumbnail

Description

We’ve seen how “enum properties” help close the gap between the ergonomics of accessing data on structs and enums, but defining them by hand requires a lot of boilerplate. This week we join forces with Apple’s Swift Syntax library to generate this boilerplate automatically!

Video

Cloudflare Stream video ID: 9552aee5fde4e83cd502d48ba03c36fa Local file: video_53_swift-syntax-enum-properties.mp4 *(download with --video 53)*

Transcript

0:05

We’ve now compared struct property access to enum associated value access and seen how structs are much more ergonomic by default, but we’ve also seen that we can recover these ergonomics on enums by defining our own computed properties per case. Unfortunately it comes at the cost of manual intervention: where the compiler gives us struct properties and key paths for free, we’re responsible for defining enum properties by hand, so it’s easy for us to forget to do so, and we can easily end up in a situation where some enum cases have properties defined while others don’t.

0:48

In order to embrace the idea of enum properties and benefit from their ergonomics universally without having to remember to take the time to define them by hand, we can turn to code generation. The Swift community has a bunch of tools that help here. The standard library team uses gyb , which stands for “generate your boilerplate”. It uses Python to interpolate templates and generate a bunch of standard library boilerplate. Another popular community tool is Sourcery. Today we’re going to use a relatively new tool from Apple called SwiftSyntax , which is a Swift language wrapper around libSyntax , a library for parsing and inspecting Swift source code. Swift Package Manager

1:35

We are going to build a command line tool that will generate all of the code to give us enum properties, and we are going to use the Swift Package Manager to do it. If you primarily work in iOS code bases, then you probably haven’t used the Swift package manager too much yet, because unfortunately it doesn’t have first class support for Xcode and iOS development. However, it’s a very powerful tool, easy to get set up, and will be the best way for us to pull down the SwiftSyntax dependency and start playing with it.

2:26

To begin, we just hop over to the command line, create a directory, and then run swift package init and it will create the scaffolding of a library that can be built by Swift. $ mkdir EnumProperties $ cd EnumProperties $ swift package init Creating library package: EnumProperties Creating Package.swift Creating README.md Creating .gitignore Creating Sources/ Creating Sources/EnumProperties/EnumProperties.swift Creating Tests/ Creating Tests/LinuxMain.swift Creating Tests/EnumPropertiesTests/ Creating Tests/EnumPropertiesTests/EnumPropertiesTests.swift Creating Tests/EnumPropertiesTests/XCTestManifests.swift

2:37

When we run swift package init we get a bunch of output, which is the default scaffolding of a package:

2:42

we get a Package.swift file, which holds the main configuration of a package, including descriptions of the libraries it builds and their dependencies.

2:54

We get a Sources directory which will hold all the source code for our library

3:00

We get a Tests directory which will hold all the tests for our library

3:05

and we get a few other second files, like LinuxMain.swift and XCTestManifests.swift , which are useful for testing on Linux, but not very important right now.

3:11

The Package.swift is pretty important, because it describes all of the libraries that are included in this package (yes! a single package can have many libraries!), as well as all the dependencies needed to run the libraries: // swift-tools-version:5.0 // The swift-tools-version declares the minimum version of Swift // required to build this package. import PackageDescription let package = Package( name: "EnumProperties", products: [ // Products define the executables and libraries produced by a // package, and make them visible to other packages. .library( name: "EnumProperties", targets: ["EnumProperties"]), ], dependencies: [ // Dependencies declare other packages that this package depends // on. // .package(url: /* package url */, from: "1.0.0"), ], targets: [ // Targets are the basic building blocks of a package. A target // can define a module or a test suite. // Targets can depend on other targets in this package, and on // products in packages which this package depends on. .target( name: "EnumProperties", dependencies: []), .testTarget( name: "EnumPropertiesTests", dependencies: ["EnumProperties"]), ] )

4:02

Our package will depend on SwiftSyntax (https://github.com/apple/swift-syntax), which is a library by Apple that can parse, inspect, generate, and transform Swift source code. We’ll use it to parse and inspect our code so that we can generate those enum properties. We can add SwiftSyntax as a dependency in our Package.swift file, which we copied straight from SwiftSyntax ’s README file. dependencies: [ .package( url: "https://github.com/apple/swift-syntax.git", .exact("0.50000.0") ), ],

4:42

And we can add its SwiftSyntax module as a dependency of the EnumProperties module. .target( name: "EnumProperties", dependencies: ["SwiftSyntax"]),

5:09

Now we can run swift build to pull down our dependency and build our package. $ swift build Fetching https://github.com/apple/swift-syntax.git Completed resolution in 1.13s Cloning https://github.com/apple/swift-syntax.git Resolving https://github.com/apple/swift-syntax.git at 0.50000.0

5:24

And from here we can work on our package in any editor we want, but we want to use Xcode, so we can run swift package generate-xcodeproj to automatically generate a project file for us to work in. $ swift package generate-xcodeproj generated: ./EnumProperties.xcodeproj

5:58

Let’s open the project and get started. $ xed .

6:02

Now we’re free to work within this generated Xcode project file, which includes all of the sources and dependencies.

6:12

All ready we can just try building our project (cmd+B), and we see it builds all of SwiftSyntax.

6:17

We’re not going to go straight into writing this code generation tool. Let’s first explore SwiftSyntax using a playground. Now, we could technically create a playground and add it directly to this project, however remember that this project was created via that generate xcodeproj command line tool, and so next time we run it it will completely blow away any changes we make to it. In particular, it will remove the playground from the project.

6:52

So instead, we can back up a step and create a workspace that holds both the generated Xcode project and the playground. That allows both things to coexist peacefully. Further, by the mere fact that the playground is in the same workspace as the Xcode project, it will get access to all of the dependencies being used for the package, most importantly SwiftSyntax. This means we can explore building our library in the playground-driven style, something we’ve talked about before on this series and something we love doing.

7:14

So, to do that we simply: Create a workspace Drag our Xcode project file into it Create a new playground and add it to our workspace

7:57

Now we should be able to build things so that we have access to them in the playground. import SwiftSyntax

8:12

Already this is cool, and if some of our viewers weren’t aware at how easy it is to set up projects with Swift package manager, I hope this was illuminating. If you want a super lightweight way of creating a library, or explore playing with a 3rd party library, simply fire up a new Swift package, add a playground, and create a workspace. It’s a very powerful development tool. Swift Syntax

8:36

Great, now that we have everything set up, let’s start exploring the SwiftSyntax API so that we can understand how it might help us do some code generation.

8:45

SwiftSyntax has a SyntaxTreeParser type that is the starting point for parsing a Swift file into a structure that we can traverse over: SyntaxTreeParser

8:57

It comes with a static function, parse , that, given a URL to valid Swift source code, will return a Syntax value that we can walk over and inspect. SyntaxTreeParser.parse(<#url: URL#>)

9:05

We don’t have a URL to feed it, so let’s add a file to our playground’s Resources directory. It will contain the definitions of some enums we want to generate properties for, starting with the Validated type, which we’ve defined in previous episodes. enum Validated<Valid, Invalid> { case valid(Valid) case invalid([Invalid]) }

9:33

And we can access that URL using the main bundle. import Foundation let url = Bundle.main.url( forResource: "Validated", withExtension: "swift" )! let tree = try SyntaxTreeParser.parse(url)

9:56

Alright, we have a value! It’s a SourceFileSyntax , which is a Syntax type that describes a parsed source file. It has a bunch of properties and methods.

10:12

The one we care about is the walk method, which will allow us to walk over every token to find the enums and enum cases and generate our enum properties. tree.walk(<#visitor: SyntaxVisitor#>)

10:48

In order to walk our parsed source tree, SwiftSyntax provides a SyntaxVisitor class. It’s a bit of an object-oriented API, so we need to make a subclass in order to visit the nodes we care about. class Visitor: SyntaxVisitor { }

11:00

And once subclassed, we can pass an instance to the walk method. let visitor = Visitor() tree.walk(visitor)

11:07

This doesn’t do much yet because we haven’t hooked into the SyntaxVisitor API. Under the hood, there are a whole bunch of callback methods being invoked on SyntaxVisitor as it walks over various nodes of the parsed tree.

11:20

SyntaxVisitor defines a whole lot of visit methods that we can hook into: almost every unique Swift syntax has a corresponding visit method!

11:30

We need to visit the nodes we care about in order to generate enum properties. Let’s look at an example to remind ourselves exactly what we want to generate. // extension Validated { // var valid: Valid? { // guard case let .valid(value) = self else { return nil } // return value // } // var invalid: [Invalid?] { // guard case let .invalid(value) = self else { return nil } // return value // } // }

11:37

We want to re-open every enum we encounter and define a computed property per case. We need to know: The name of the enum, the name of each case, and the type of the associated value.

12:07

Let’s start by searching for the enum’s name. If we add enum to visit we get a bunch of visit methods in autocomplete, but most of them are prefixed with EnumCase , which probably deal with enum cases themselves, so the EnumDeclSyntax node seems to make the most sense to try first.

12:20

When this method gets called we can print the node to see what we’re working with. class Visitor: SyntaxVisitor { override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorKind { print(node) return .visitChildren } } But the API requires that we return a value of SyntaxVisitorKind . It’s just an enum with two cases that tell the visitor to visit or skip the node’s children.

12:44

Alright, that just printed the source as defined, which kinda makes sense because our source file only contains an enum declaration.

12:53

If we take a look at this node’s attributes, we can see some interesting attributes, including identifier , which is the name of the enum. class Visitor: SyntaxVisitor { override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorKind { print(node.identifier) return .visitChildren } } let visitor = Visitor() tree.walk(visitor) // Validated

13:05

This is exactly what we need to print out an extension for the given type. So what can we do to turn this into Swift code? Well, we know we want to generate an extension, so let’s add that declaration. class Visitor: SyntaxVisitor { override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorKind { print("extension \(node.identifier) {") print("}") return .visitChildren } } I’m not exactly sure how we’re going to insert the properties between those braces quite yet, but let’s keep going and see how things shape up. When we run the playground: let visitor = Visitor() tree.walk(visitor) // extension Validated { // }

13:28

We already have valid Swift code printing to the console! It took a little bit to get there though. Generating properties

14:41

Now how do we go about generating those enum properties? We need to somehow get those case names and associated types.

14:51

If we explore the other visit methods on SyntaxVisitor , there are a couple promising node types, in particular the EnumCaseElementSyntax . override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { }

15:22

Let’s print the node and see what we’re working with. We can return .skipChildren because this node should have all the information we need. We don’t need to traverse any deeper into this node. override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { print(node) return .skipChildren } And when we run the playground, it prints: extension Validated { } valid(Valid) invalid([Invalid])

15:37

Now we’re getting somewhere! The EnumCaseElementSyntax node contains all of the syntax of an enum case element, and in particular the case name and associated type, which we need.

15:54

So what can we do with this node to generate source code? Well, it also has an identifier . override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { print(node.identifier) return .skipChildren } // valid // invalid This appears to map to the case name, which is what we need to name the properties we generate.

16:12

Elements also have an associatedValue . override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { print(node.identifier) print(node.associatedValue) return .skipChildren } // valid // Optional((Valid)) // invalid // Optional(([Invalid]))

16:26

And this returns the type! It’s optional, which makes sense because enum cases don’t always have associated values.

16:46

Our Validated type has an associated value for each case, so let’s force-unwrap it for now. override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { print(node.identifier) print(node.associatedValue!) return .skipChildren } // valid // (Valid) // invalid // ([Invalid])

16:52

This output includes the parentheses that wrap an enum’s associated values, which we don’t really care about.

17:08

In order to get inside of those parentheses, we can call parameterList , which represents just the items inside. override func visit( _ node: EnumCaseElementSyntax ) -> SyntaxVisitorKind { print(node.identifier) print(node.associatedValue!.parameterList) return .skipChildren } // valid // Valid // invalid // [Invalid]

17:18

Now we have to generate the code that makes up each one of these properties.

17:37

We can start with the var declaration. print(" var \(node.identifier): \(node.associatedValue!.parameterList)? {")

18:21

And let’s not forget to close the scope. print(" }") Now when we run the playground, we can see the scaffolding of our properties. // var valid: Valid? { // } // … // var invalid: [Invalid]? { // }

18:38

Now we’re inside the scope of the property, we can start by printing those lines, too. We can start with the first line, which is a guard that tries to unwrap the associated value. print(" guard case let .\(node.identifier)(value) = self else { return nil }")

19:30

One line down, one to go, and this one’s very simple. print(" return value") And now, when we run the playground. // var valid: Valid? { // guard case let .valid(value) = self else { return nil } // return value // } // … // var invalid: [Invalid]? { // guard case let .invalid(value) = self else { return nil } // return value // } We get some valid-looking properties! If we get rid of a few debug prints, though, we can see that we’re not quite there yet: extension Validated { } var valid: Valid? { guard case let .valid(value) = self else { return nil } return value } var invalid: [Invalid]? { guard case let .invalid(value) = self else { return nil } return value } The code we’re printing immediately closes the extension and our properties are kind of dangling at the end. A finishing touch for valid code

20:00

We’ve been able to write all of the lines we needed to generate some valid enum properties by hooking into just one more Swift Syntax method, they’re just not quite in the right order yet.

20:37

First, let’s not close the extension in the method that opens it. class Visitor: SyntaxVisitor { override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorKind { print("extension \(node.identifier) {") // print("}") return .visitChildren } This could never work because the return value of .visitChildren is what lets the visitor class know to continue, and that’s the work that is printing our all of our other properties.

20:55

Luckily, there’s another API we can hook into in order to close these extensions: visitPre and visitPost are called before and after each node is visited. We can use visitPost and scope things to EnumDeclSyntax . override func visitPost(_ node: Syntax) { if node is EnumDeclSyntax { print("}") } }

21:32

And when we check the console. // extension Validated { // var valid: Valid? { // guard case let .valid(value) = self else { return nil } // return value // } // var invalid: [Invalid]? { // guard case let .invalid(value) = self else { return nil } // return value // } // } There we have it! This is exactly the code we previously wrote by hand.

21:47

Just to make sure it all works, let’s copy and paste the Validated type and our generated code into the playground. enum Validated<Valid, Invalid> { case valid(Valid) case invalid([Invalid]) } extension Validated { var valid: Valid? { guard case let .valid(value) = self else { return nil } return value } var invalid: [Invalid]? { guard case let .invalid(value) = self else { return nil } return value } }

22:02

No complaints from the compiler, so let’s take things for a spin.

22:04

Given an array of validated values: let validatedValues: [Validated<Int, String>] = [ .valid(1), .invalid(["Failed to calculate value"]), .valid(42), ]

22:07

We can use our generated properties to very succinctly pluck out all of the valid values. validatedValues .compactMap { $0.valid } // [1, 42]

22:19

And all of the invalid values. validatedValues .compactMap { $0.invalid } // [["Failed to calculate value"]] Our generated code is working! Till next time

22:34

So already we are seeing some really promising results. However, in order for this to be useful we’d have to copy and paste this code into a Swift file so that it can be compiled and be available in our application.

22:15

It’s pretty amazing, though, that in just a couple dozen lines we have the bulk of our code generation working, but it’s not quite useful yet. What we want is a real command line tool that we could incorporate into our build system to automatically generate enum properties for every enum in our project.

23:03

However, before we get to that we need to beef up this code generation a bit because there are a lot of situations that are not properly handled. For example, what if an enum case holds no associated values? What if an enum case holds multiple associated values? Or what if the enum is nested inside another type?

23:32

So next time we are going to put the finishing touches on how we generate the code for enum properties, and then after that we will whip everything into a proper Swift package that can be brought into your codebase immediately.

23:46

Until next time! Downloads Sample code 0053-swift-syntax-enum-properties Point-Free A hub for advanced Swift programming. Brought to you by Brandon Williams and Stephen Celis . Content Become a member The Point-Free Way Beta previews Gifts Videos Collections Free clips Blog More About Us Community Slack Mastodon Twitter BlueSky GitHub Contact Us Privacy Policy © 2026 Point-Free, Inc. All rights are reserved for the videos and transcripts on this site. All other content is licensed under CC BY-NC-SA 4.0 , and the underlying source code to run this site is licensed under the MIT License .