Last time we set out to automate all the boilerplate involved in creating what we call “enum properties”, which are computed properties on enums that give us easy access to the data held inside the enum’s cases. We’re doing this because Swift makes it very easy to access data in structs, but does not give us many tools for doing the same with enums.
To achieve this we started creating a package with the Swift Package Manager. It has a dependency on SwiftSyntax, which is Apple’s library for parsing Swift source code into a tree of tokens that we can traverse and inspect. We ended up with a playground that is capable of parsing the code in a Swift file, finding all the enums in it, and creating computed properties for each of the cases in those enums.
That’s awesome, but there are still a lot of subtle edge cases that we are not accounting for, and so we can’t really point this tool at a real world code base and expect it to work.
So today we are going handle more of those edge cases so that we will be in a good position to extract everything out of the playground and make a real command line tool out of it!
Let’s take a look at what we’ve written so far.
import SwiftSyntax
import Foundation
let url = Bundle.main.url(
forResource: "Enums", withExtension: "swift"
)!
let tree = try SyntaxTreeParser.parse(url)
class Visitor: SyntaxVisitor {
override func visit(_ node: EnumDeclSyntax) -> SyntaxVisitorKind {
print("extension \(node.identifier) {")
return .visitChildren
}
override func visitPost(_ node: Syntax) {
if node is EnumDeclSyntax {
print("}")
}
}
override func visit(
_ node: EnumCaseElementSyntax
) -> SyntaxVisitorKind {
print(" var \(node.identifier): (\(node.associatedValue!.parameterList))? {")
print(" guard case let .\(node.identifier)(value) = self else { return nil }")
print(" return value")
print(" }")
}
}
let visitor = Visitor()
tree.walk(visitor)
// 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
// }
// }
Now this tool is very useful but currently only works for enums where every case has a single associated value. In the case of multiple associated values or no associated values, we would be generating invalid code. So today we’re going to address a bunch of these shortcomings to end up with a tool that we can run on code bases with many different kinds of enums in various shapes that should always generate correct, compiling code.