Skip to content

Commit d982a14

Browse files
committed
Add OptionSet macro example and improve MacroToolkit to work well for it (new attribute, expression, and struct wrapps)
1 parent 54e7c80 commit d982a14

File tree

5 files changed

+346
-9
lines changed

5 files changed

+346
-9
lines changed

Sources/MacroToolkit/MacroToolkit.swift

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,10 @@ public struct Type: SyntaxExpressibleByStringInterpolation {
290290
_syntax = syntax
291291
}
292292

293+
public init(_ syntax: any TypeSyntaxProtocol) {
294+
_syntax = TypeSyntax(syntax)
295+
}
296+
293297
public init(stringInterpolation: SyntaxStringInterpolation) {
294298
_syntax = TypeSyntax(stringInterpolation: stringInterpolation)
295299
}
@@ -448,6 +452,104 @@ extension Collection where Element == AttributeListElement {
448452
}
449453
}
450454

455+
public struct Expression {
456+
public var _syntax: ExprSyntax
457+
458+
public init(_ syntax: ExprSyntax) {
459+
_syntax = syntax
460+
}
461+
462+
public init(_ syntax: any ExprSyntaxProtocol) {
463+
_syntax = ExprSyntax(syntax)
464+
}
465+
466+
/// Gets the contents of the expression if it's a string literal with no interpolation.
467+
public var asSimpleStringLiteral: String? {
468+
guard
469+
let literal = _syntax.as(StringLiteralExprSyntax.self),
470+
literal.segments.count == 1,
471+
case let .stringSegment(segment)? = literal.segments.first
472+
else {
473+
return nil
474+
}
475+
return segment.content.text
476+
}
477+
}
478+
479+
public struct MacroAttribute {
480+
public var _syntax: AttributeSyntax
481+
public var _argumentListSyntax: TupleExprElementListSyntax
482+
483+
public init?(_ syntax: AttributeSyntax) {
484+
guard case let .argumentList(arguments) = syntax.argument else {
485+
return nil
486+
}
487+
_syntax = syntax
488+
_argumentListSyntax = arguments
489+
}
490+
491+
public func argument(labeled label: String) -> Expression? {
492+
(_argumentListSyntax.first { element in
493+
return element.label?.text == label
494+
}?.expression).map(Expression.init)
495+
}
496+
497+
public var arguments: [Expression] {
498+
Array(_argumentListSyntax).map { argument in
499+
Expression(argument.expression)
500+
}
501+
}
502+
503+
public var name: Type {
504+
Type(_syntax.attributeName)
505+
}
506+
}
507+
508+
public struct Struct {
509+
public var _syntax: StructDeclSyntax
510+
511+
public init(_ syntax: StructDeclSyntax) {
512+
_syntax = syntax
513+
}
514+
515+
public init?(_ syntax: any DeclGroupSyntax) {
516+
guard let syntax = syntax.as(StructDeclSyntax.self) else {
517+
return nil
518+
}
519+
_syntax = syntax
520+
}
521+
522+
// TODO: Add members property to all declgroupsyntax decls through protocol default impl
523+
public var members: [Decl] {
524+
_syntax.memberBlock.members.map(\.decl).map(Decl.init)
525+
}
526+
527+
public var inheritedTypes: [Type] {
528+
_syntax.inheritanceClause?.inheritedTypeCollection.map(\.typeName).map(Type.init) ?? []
529+
}
530+
}
531+
532+
public struct Decl {
533+
public var _syntax: DeclSyntax
534+
535+
public init(_ syntax: DeclSyntax) {
536+
_syntax = syntax
537+
}
538+
539+
public init(_ syntax: any DeclSyntaxProtocol) {
540+
_syntax = DeclSyntax(syntax)
541+
}
542+
543+
// TODO: Add conversions for all possible member types
544+
public var asEnum: Enum? {
545+
_syntax.as(EnumDeclSyntax.self).map(Enum.init)
546+
}
547+
548+
public var asStruct: Struct? {
549+
_syntax.as(StructDeclSyntax.self).map(Struct.init)
550+
}
551+
}
552+
451553
extension FunctionDeclSyntax {
452554
/// Gets the signature's effect specifiers, or returns a default effect specifiers
453555
/// syntax (without any specifiers).
@@ -552,6 +654,12 @@ extension CodeBlockSyntax {
552654
}
553655
}
554656

657+
extension DeclGroupSyntax {
658+
public var isPublic: Bool {
659+
modifiers?.contains { $0.name.tokenKind == .keyword(.public) } == true
660+
}
661+
}
662+
555663
// TODO: Figure out a destructuring implementation that uses variadic generics (tricky without same type requirements)
556664
public func destructure<Element>(_ elements: some Sequence<Element>) -> ()? {
557665
let array = Array(elements)
@@ -561,7 +669,9 @@ public func destructure<Element>(_ elements: some Sequence<Element>) -> ()? {
561669
return ()
562670
}
563671

564-
public func destructure<Element>(_ elements: some Sequence<Element>) -> (Element)? {
672+
/// Named differently to allow type inference to still work correctly (single element tuples
673+
/// are weird in Swift).
674+
public func destructureSingle<Element>(_ elements: some Sequence<Element>) -> (Element)? {
565675
let array = Array(elements)
566676
guard array.count == 1 else {
567677
return nil
@@ -607,8 +717,10 @@ public func destructure(_ type: NominalType) -> (String, ())? {
607717
}
608718
}
609719

610-
public func destructure(_ type: NominalType) -> (String, (Type))? {
611-
destructure(type.genericArguments ?? []).map { arguments in
720+
/// Named differently to allow type inference to still work correctly (single element tuples
721+
/// are weird in Swift).
722+
public func destructureSingle(_ type: NominalType) -> (String, (Type))? {
723+
destructureSingle(type.genericArguments ?? []).map { arguments in
612724
(type.name, arguments)
613725
}
614726
}
@@ -643,8 +755,10 @@ public func destructure(_ type: FunctionType) -> ((), Type)? {
643755
}
644756
}
645757

646-
public func destructure(_ type: FunctionType) -> ((Type), Type)? {
647-
destructure(type.parameters).map { parameters in
758+
/// Named differently to allow type inference to still work correctly (single element tuples
759+
/// are weird in Swift).
760+
public func destructureSingle(_ type: FunctionType) -> ((Type), Type)? {
761+
destructureSingle(type.parameters).map { parameters in
648762
(parameters, type.returnType)
649763
}
650764
}
@@ -687,13 +801,15 @@ public func destructure(_ type: Type) -> DestructuredType<()>? {
687801
}
688802
}
689803

690-
public func destructure(_ type: Type) -> DestructuredType<(Type)>? {
804+
/// Named differently to allow type inference to still work correctly (single element tuples
805+
/// are weird in Swift).
806+
public func destructureSingle(_ type: Type) -> DestructuredType<(Type)>? {
691807
if let type = type.asNominalType {
692-
return destructure(type).map { destructured in
808+
return destructureSingle(type).map { destructured in
693809
.nominal(name: destructured.0, genericArguments: destructured.1)
694810
}
695811
} else if let type = type.asFunctionType {
696-
return destructure(type).map { destructured in
812+
return destructureSingle(type).map { destructured in
697813
.function(parameterTypes: destructured.0, returnType: destructured.1)
698814
}
699815
} else {

Sources/MacroToolkitExample/MacroToolkitExample.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ public macro AddCompletionHandler() =
1313
@freestanding(expression)
1414
public macro addBlocker<T>(_ value: T) -> T =
1515
#externalMacro(module: "MacroToolkitExamplePlugin", type: "AddBlocker")
16+
17+
@attached(member, names: arbitrary)
18+
@attached(conformance)
19+
public macro MyOptionSet<RawType>() =
20+
#externalMacro(module: "MacroToolkitExamplePlugin", type: "OptionSetMacro")

Sources/MacroToolkitExamplePlugin/AddBlocker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public struct AddBlocker: ExpressionMacro {
4747
of node: some FreestandingMacroExpansionSyntax,
4848
in context: some MacroExpansionContext
4949
) throws -> ExprSyntax {
50-
guard let (argument): (TupleExprElementSyntax) = destructure(node.argumentList) else {
50+
guard let (argument) = destructureSingle(node.argumentList) else {
5151
throw MacroError("#addBlocker only expects one argument")
5252
}
5353

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import MacroToolkit
2+
import SwiftDiagnostics
3+
import SwiftSyntax
4+
import SwiftSyntaxBuilder
5+
import SwiftSyntaxMacros
6+
7+
// Modified from: https://github.com/DougGregor/swift-macro-examples/blob/f61ac7cdca8dc3557e53f86e7e03df1353908d3e/MacroExamplesPlugin/OptionSetMacro.swift
8+
9+
enum OptionSetMacroDiagnostic {
10+
case requiresStruct
11+
case requiresStringLiteral(String)
12+
case requiresOptionsEnum(String)
13+
case requiresOptionsEnumRawType
14+
case invalidAttribute
15+
}
16+
17+
extension OptionSetMacroDiagnostic: DiagnosticMessage {
18+
func diagnose(at node: some SyntaxProtocol) -> Diagnostic {
19+
Diagnostic(node: Syntax(node), message: self)
20+
}
21+
22+
var message: String {
23+
switch self {
24+
case .requiresStruct:
25+
return "'OptionSet' macro can only be applied to a struct"
26+
27+
case .requiresStringLiteral(let name):
28+
return "'OptionSet' macro argument \(name) must be a string literal"
29+
30+
case .requiresOptionsEnum(let name):
31+
return "'OptionSet' macro requires nested options enum '\(name)'"
32+
33+
case .requiresOptionsEnumRawType:
34+
return "'OptionSet' macro requires a raw type"
35+
36+
case .invalidAttribute:
37+
return "'OptionSet' macro attribute is invalid"
38+
}
39+
}
40+
41+
var severity: DiagnosticSeverity { .error }
42+
43+
var diagnosticID: MessageID {
44+
MessageID(domain: "Swift", id: "OptionSet.\(self)")
45+
}
46+
}
47+
48+
public struct OptionSetMacro {
49+
/// Decodes the arguments to the macro expansion.
50+
/// - Returns: the important arguments used by the various roles of this
51+
/// macro inhabits, or nil if an error occurred.
52+
static func decodeExpansion(
53+
of attribute: AttributeSyntax,
54+
attachedTo decl: some DeclGroupSyntax,
55+
in context: some MacroExpansionContext
56+
) -> (Struct, Enum, Type)? {
57+
guard let attribute = MacroAttribute(attribute) else {
58+
context.diagnose(OptionSetMacroDiagnostic.invalidAttribute.diagnose(at: attribute))
59+
return nil
60+
}
61+
62+
// Determine the name of the options enum
63+
let optionsEnumName: String
64+
if let argument = attribute.argument(labeled: "optionsName") {
65+
guard let stringLiteral = argument.asSimpleStringLiteral else {
66+
context.diagnose(
67+
OptionSetMacroDiagnostic
68+
.requiresStringLiteral("optionsName")
69+
.diagnose(at: argument._syntax)
70+
)
71+
return nil
72+
}
73+
74+
optionsEnumName = stringLiteral
75+
} else {
76+
optionsEnumName = "Options"
77+
}
78+
79+
// Only apply to structs
80+
guard let structDecl = Struct(decl) else {
81+
context.diagnose(OptionSetMacroDiagnostic.requiresStruct.diagnose(at: decl))
82+
return nil
83+
}
84+
85+
// Find the option enum within the struct
86+
guard
87+
let optionsEnum = structDecl.members.compactMap(\.asEnum)
88+
.first(where: { $0.identifier == optionsEnumName })
89+
else {
90+
context.diagnose(
91+
OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)
92+
)
93+
return nil
94+
}
95+
96+
// Retrieve the raw type from the attribute
97+
// TODO: Improve destructuring on single-element arrays
98+
guard case let .nominal(_, (rawType)) = destructureSingle(attribute.name) else {
99+
context.diagnose(
100+
OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute._syntax))
101+
return nil
102+
}
103+
104+
return (structDecl, optionsEnum, rawType)
105+
}
106+
}
107+
108+
extension OptionSetMacro: ConformanceMacro {
109+
public static func expansion(
110+
of attribute: AttributeSyntax,
111+
providingConformancesOf decl: some DeclGroupSyntax,
112+
in context: some MacroExpansionContext
113+
) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] {
114+
// Decode the expansion arguments. If there is an explicit conformance to
115+
// OptionSet already, don't add one.
116+
guard
117+
let (structDecl, _, _) = decodeExpansion(of: attribute, attachedTo: decl, in: context),
118+
!structDecl.inheritedTypes.contains(where: { type in
119+
type.description == "OptionSet"
120+
})
121+
else {
122+
return []
123+
}
124+
125+
return [("OptionSet", nil)]
126+
}
127+
}
128+
129+
extension OptionSetMacro: MemberMacro {
130+
public static func expansion(
131+
of attribute: AttributeSyntax,
132+
providingMembersOf decl: some DeclGroupSyntax,
133+
in context: some MacroExpansionContext
134+
) throws -> [DeclSyntax] {
135+
// Decode the expansion arguments.
136+
guard
137+
let (_, optionsEnum, rawType) = decodeExpansion(
138+
of: attribute,
139+
attachedTo: decl,
140+
in: context
141+
)
142+
else {
143+
return []
144+
}
145+
146+
let cases = optionsEnum.cases
147+
148+
// TODO: This seems wrong, surely other modifiers would also make sense to passthrough?
149+
let access = decl.isPublic ? "public " : ""
150+
151+
let staticVars = cases.map { (case_) -> DeclSyntax in
152+
"""
153+
\(raw: access)static let \(raw: case_.identifier): Self =
154+
Self(rawValue: 1 << \(raw: optionsEnum.identifier).\(raw: case_.identifier).rawValue)
155+
"""
156+
}
157+
158+
return [
159+
"\(raw: access)typealias RawValue = \(rawType._syntax)",
160+
"\(raw: access)var rawValue: RawValue",
161+
"\(raw: access)init() { self.rawValue = 0 }",
162+
"\(raw: access)init(rawValue: RawValue) { self.rawValue = rawValue }",
163+
] + staticVars
164+
}
165+
}

0 commit comments

Comments
 (0)