From 9e2ea945c9a7cdf4a7023b483088c47951222fac Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Tue, 7 Mar 2023 00:12:07 -0800 Subject: [PATCH 1/2] Alternative implementation of option set macro This implementation uses static vars without initializers to describe each option, rather than cases of a nested enum, and was suggested by Joe Groff. --- MacroExamples.xcodeproj/project.pbxproj | 8 +- MacroExamples/main.swift | 11 +- MacroExamplesLib/Macros.swift | 15 +- MacroExamplesPlugin/OptionSetItemMacro.swift | 28 +++ MacroExamplesPlugin/OptionSetMacro.swift | 178 ++++++++++++------ .../MacroExamplesPluginTest.swift | 56 ++++++ .../NewTypePluginTests.swift | 4 - 7 files changed, 224 insertions(+), 76 deletions(-) create mode 100644 MacroExamplesPlugin/OptionSetItemMacro.swift diff --git a/MacroExamples.xcodeproj/project.pbxproj b/MacroExamples.xcodeproj/project.pbxproj index 6985767..854e162 100644 --- a/MacroExamples.xcodeproj/project.pbxproj +++ b/MacroExamples.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ BD841F82294CE1F600DA4D81 /* AddBlocker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD841F81294CE1F600DA4D81 /* AddBlocker.swift */; }; BD8A3130294947BD00E83EB9 /* Macros.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8A312F294947BD00E83EB9 /* Macros.swift */; }; BD8A31312949480600E83EB9 /* libMacroExamplesLib.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = BD8A3126294947A100E83EB9 /* libMacroExamplesLib.dylib */; }; + BDC559D629B857DF00F26DFF /* OptionSetItemMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC559D529B857DF00F26DFF /* OptionSetItemMacro.swift */; }; BDF5AFE42947E5B000FA119B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5AFE32947E5B000FA119B /* main.swift */; }; BDF5AFF82947E95C00FA119B /* StringifyMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5AFF72947E95C00FA119B /* StringifyMacro.swift */; }; BDFB14B52948484000708DA6 /* MacroExamplesPluginTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFB14B42948484000708DA6 /* MacroExamplesPluginTest.swift */; }; @@ -97,6 +98,7 @@ BD841F81294CE1F600DA4D81 /* AddBlocker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBlocker.swift; sourceTree = ""; }; BD8A3126294947A100E83EB9 /* libMacroExamplesLib.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libMacroExamplesLib.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; BD8A312F294947BD00E83EB9 /* Macros.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macros.swift; sourceTree = ""; }; + BDC559D529B857DF00F26DFF /* OptionSetItemMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionSetItemMacro.swift; sourceTree = ""; }; BDF5AFE02947E5B000FA119B /* MacroExamples */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = MacroExamples; sourceTree = BUILT_PRODUCTS_DIR; }; BDF5AFE32947E5B000FA119B /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; BDF5AFEE2947E61100FA119B /* libMacroExamplesPlugin.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libMacroExamplesPlugin.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -159,6 +161,7 @@ 1D682A95299E3313006F9F78 /* CustomCodable.swift */, BD48319229AFF87200F3123A /* OptionSetMacro.swift */, 88E54A5129B5475400252D99 /* MetaEnumMacro.swift */, + BDC559D529B857DF00F26DFF /* OptionSetItemMacro.swift */, ); path = MacroExamplesPlugin; sourceTree = ""; @@ -404,6 +407,7 @@ 1D682A94299E2FFB006F9F78 /* CodableKey.swift in Sources */, 371A6719299C241F00E74A8A /* CaseDetectionMacro.swift in Sources */, BD752BE5294D3BEC00D00A2E /* WarningMacro.swift in Sources */, + BDC559D629B857DF00F26DFF /* OptionSetItemMacro.swift in Sources */, 3175781D298DBC8700D79290 /* NewTypeMacro.swift in Sources */, BDF5AFF82947E95C00FA119B /* StringifyMacro.swift in Sources */, EC21BDEB298D9F9900D585C6 /* ObservableMacro.swift in Sources */, @@ -611,7 +615,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - OTHER_SWIFT_FLAGS = "-Xfrontend -enable-experimental-feature -Xfrontend Macros -Xfrontend -load-plugin-library -Xfrontend ${BUILD_DIR}/${CONFIGURATION}/libMacroExamplesPlugin.dylib -Xfrontend -dump-macro-expansions"; + OTHER_SWIFT_FLAGS = "-Xfrontend -load-plugin-library -Xfrontend ${BUILD_DIR}/${CONFIGURATION}/libMacroExamplesPlugin.dylib -diagnostic-style swift"; PRODUCT_NAME = "$(TARGET_NAME)"; "SWIFT_OPTIMIZATION_LEVEL[arch=*]" = "-Onone"; SWIFT_VERSION = 5.0; @@ -622,7 +626,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; - OTHER_SWIFT_FLAGS = "-Xfrontend -enable-experimental-feature -Xfrontend Macros -Xfrontend -load-plugin-library -Xfrontend ${BUILD_DIR}/${CONFIGURATION}/libMacroExamplesPlugin.dylib -Xfrontend -dump-macro-expansions"; + OTHER_SWIFT_FLAGS = "-Xfrontend -load-plugin-library -Xfrontend ${BUILD_DIR}/${CONFIGURATION}/libMacroExamplesPlugin.dylib -diagnostic-style swift"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; }; diff --git a/MacroExamples/main.swift b/MacroExamples/main.swift index 6fa51d5..49ad5f1 100644 --- a/MacroExamples/main.swift +++ b/MacroExamples/main.swift @@ -167,15 +167,12 @@ let jsonDecoder = JSONDecoder() let product = try jsonDecoder.decode(CustomCodableString.self, from: json) print(product.propertyWithOtherName) - @MyOptionSet struct ShippingOptions { - private enum Options: Int { - case nextDay - case secondDay - case priority - case standard - } + static var nextDay: ShippingOptions + static var secondDay: ShippingOptions + static var priority: ShippingOptions + static var standard: ShippingOptions static let express: ShippingOptions = [.nextDay, .secondDay] static let all: ShippingOptions = [.express, .priority, .standard] diff --git a/MacroExamplesLib/Macros.swift b/MacroExamplesLib/Macros.swift index 1d649de..8017c29 100644 --- a/MacroExamplesLib/Macros.swift +++ b/MacroExamplesLib/Macros.swift @@ -83,7 +83,6 @@ public struct ObservationRegistrar { } @attached(member, names: named(Storage), named(_storage), named(_registrar), named(addObserver), named(removeObserver), named(withTransaction)) -@attached(memberAttribute) public macro Observable() = #externalMacro(module: "MacroExamplesPlugin", type: "ObservableMacro") @attached(accessor) @@ -106,7 +105,7 @@ public macro MetaEnum() = #externalMacro(module: "MacroExamplesPlugin", type: "M @attached(member) public macro CodableKey(name: String) = #externalMacro(module: "MacroExamplesPlugin", type: "CodableKey") -@attached(member, names: named(CodingKeys)) +@attached(member, names: arbitrary) public macro CustomCodable() = #externalMacro(module: "MacroExamplesPlugin", type: "CustomCodable") /// Create an option set from a struct that contains a nested `Options` enum. @@ -116,7 +115,8 @@ public macro CustomCodable() = #externalMacro(module: "MacroExamplesPlugin", typ /// `OptionSet` by /// 1. Introducing a `rawValue` stored property to track which options are set, /// along with the necessary `RawType` typealias and initializers to satisfy -/// the `OptionSet` protocol. +/// the `OptionSet` protocol. The raw type is specified after `@OptionSet`, +/// e.g., `@OptionSet`. /// 2. Introducing static properties for each of the cases within the `Options` /// enum, of the type of the struct. /// @@ -124,7 +124,7 @@ public macro CustomCodable() = #externalMacro(module: "MacroExamplesPlugin", typ /// each indicate a different option in the resulting option set. For example, /// the struct and its nested `Options` enum could look like this: /// -/// @MyOptionSet +/// @MyOptionSet /// struct ShippingOptions { /// private enum Options: Int { /// case nextDay @@ -133,6 +133,11 @@ public macro CustomCodable() = #externalMacro(module: "MacroExamplesPlugin", typ /// case standard /// } /// } -@attached(member, names: arbitrary) +@attached(member, names: named(RawValue), named(rawValue), named(`init`)) @attached(conformance) +@attached(memberAttribute) public macro MyOptionSet() = #externalMacro(module: "MacroExamplesPlugin", type: "OptionSetMacro") + + +@attached(accessor) +public macro OptionSetItem(bit: Int) = #externalMacro(module: "MacroExamplesPlugin", type: "OptionSetItemMacro") diff --git a/MacroExamplesPlugin/OptionSetItemMacro.swift b/MacroExamplesPlugin/OptionSetItemMacro.swift new file mode 100644 index 0000000..b9f8251 --- /dev/null +++ b/MacroExamplesPlugin/OptionSetItemMacro.swift @@ -0,0 +1,28 @@ +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +public struct OptionSetItemMacro { } + +extension OptionSetItemMacro: AccessorMacro { + public static func expansion( + of attribute: AttributeSyntax, + providingAccessorsOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [AccessorDeclSyntax] { + guard case let .argumentList(arguments) = attribute.argument, + let bitArg = arguments.first(labeled: "bit"), + let bit = bitArg.expression.as(IntegerLiteralExprSyntax.self)?.digits else { + return [] + } + + return [ + """ + + get { + Self(rawValue: 1 << \(bit)) + } + """ + ] + } +} diff --git a/MacroExamplesPlugin/OptionSetMacro.swift b/MacroExamplesPlugin/OptionSetMacro.swift index 7a7bf74..9f6dcc3 100644 --- a/MacroExamplesPlugin/OptionSetMacro.swift +++ b/MacroExamplesPlugin/OptionSetMacro.swift @@ -8,6 +8,7 @@ enum OptionSetMacroDiagnostic { case requiresStringLiteral(String) case requiresOptionsEnum(String) case requiresOptionsEnumRawType + case itemInMacroExpansion } extension OptionSetMacroDiagnostic: DiagnosticMessage { @@ -28,6 +29,9 @@ extension OptionSetMacroDiagnostic: DiagnosticMessage { case .requiresOptionsEnumRawType: return "'OptionSet' macro requires a raw type" + + case .itemInMacroExpansion: + return "'OptionSet' item cannot occur as a result of macro expansion" } } @@ -38,15 +42,6 @@ extension OptionSetMacroDiagnostic: DiagnosticMessage { } } - -/// The label used for the OptionSet macro argument that provides the name of -/// the nested options enum. -private let optionsEnumNameArgumentLabel = "optionsName" - -/// The default name used for the nested "Options" enum. This should -/// eventually be overridable. -private let defaultOptionsEnumName = "Options" - extension TupleExprElementListSyntax { /// Retrieve the first element with the given label. func first(labeled name: String) -> Element? { @@ -69,52 +64,135 @@ public struct OptionSetMacro { of attribute: AttributeSyntax, attachedTo decl: some DeclGroupSyntax, in context: some MacroExpansionContext - ) -> (StructDeclSyntax, EnumDeclSyntax, TypeSyntax)? { - // Determine the name of the options enum. - let optionsEnumName: String - if case let .argumentList(arguments) = attribute.argument, - let optionEnumNameArg = arguments.first(labeled: optionsEnumNameArgumentLabel) { - // We have a options name; make sure it is a string literal. - guard let stringLiteral = optionEnumNameArg.expression.as(StringLiteralExprSyntax.self), - stringLiteral.segments.count == 1, - case let .stringSegment(optionsEnumNameString)? = stringLiteral.segments.first else { - context.diagnose(OptionSetMacroDiagnostic.requiresStringLiteral(optionsEnumNameArgumentLabel).diagnose(at: optionEnumNameArg.expression)) - return nil - } - - optionsEnumName = optionsEnumNameString.content.text - } else { - optionsEnumName = defaultOptionsEnumName - } - + ) -> (StructDeclSyntax, TypeSyntax)? { // Only apply to structs. guard let structDecl = decl.as(StructDeclSyntax.self) else { context.diagnose(OptionSetMacroDiagnostic.requiresStruct.diagnose(at: decl)) return nil } - // Find the option enum within the struct. - guard let optionsEnum = decl.members.members.compactMap({ member in - if let enumDecl = member.decl.as(EnumDeclSyntax.self), - enumDecl.identifier.text == optionsEnumName { - return enumDecl + // Retrieve the raw type from the attribute. + guard let genericArgs = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.genericArgumentClause, + let rawType = genericArgs.arguments.first?.argumentType else { + context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute)) + return nil + } + + + return (structDecl, rawType) + } +} + +extension VariableDeclSyntax { + /// Determine whether this variable has the syntax of a stored property. + /// + /// This syntactic check cannot account for semantic adjustments due to, + /// e.g., accessor macros or property wrappers. + func getOptionSetItemCandidateName(structName: TokenSyntax) -> TokenSyntax? { + if bindings.count != 1 { + return nil + } + + // Make sure this is a static variable. + guard let _ = modifiers?.first(where: { + return $0.name.tokenKind == .keyword(.static) + }) else { + return nil + } + + // If there is an initializer, do nothing. + let binding = bindings.first! + if binding.initializer != nil { + return nil + } + + // Make sure there are no non-observing getters. + switch binding.accessor { + case .none: + break + + case .accessors(let node): + for accessor in node.accessors { + switch accessor.accessorKind.tokenKind { + case .keyword(.willSet), .keyword(.didSet): + // Observers can occur on a stored property. + break + + default: + // Other accessors make it a computed property. + return nil + } } + break + case .getter: return nil - }).first else { - context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).diagnose(at: decl)) + + @unknown default: return nil } - // Retrieve the raw type from the attribute. - guard let genericArgs = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.genericArgumentClause, - let rawType = genericArgs.arguments.first?.argumentType else { - context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute)) + // Make sure the type is either Self or the struct. + guard let type = binding.typeAnnotation?.type, + type.trimmed.description == "Self" || + type.trimmed.description == structName.text else { + return nil + } + + guard let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else { return nil } + return identifier + } +} + +extension OptionSetMacro: MemberAttributeMacro { + public static func expansion( + of attribute: AttributeSyntax, + attachedTo declaration: some DeclGroupSyntax, + providingAttributesFor member: some DeclSyntaxProtocol, + in context: some MacroExpansionContext + ) throws -> [AttributeSyntax] { + // Decode the expansion arguments. + guard let (structDecl, _) = decodeExpansion(of: attribute, attachedTo: declaration, in: context) else { + return [] + } + + // Make sure this is an option set item candidate. + guard let property = member.as(VariableDeclSyntax.self), + let propertyName = property.getOptionSetItemCandidateName(structName: structDecl.identifier) + else { + return [] + } + + // Count how many item candidates occurred before this one. + var bit: Int = 0 + var found: Bool = false + for otherMember in declaration.members.members { + // Only consider the option set item candidates. + guard let otherProperty = otherMember.decl.as(VariableDeclSyntax.self), + let otherPropertyName = otherProperty.getOptionSetItemCandidateName(structName: structDecl.identifier) else { + continue + } + + if propertyName.text == otherPropertyName.text { + found = true + break + } + + bit += 1 + } + + // If we did not found our member in the list, fail. This could happen + // if the item came from another macro expansion. + if !found { + context.diagnose( + OptionSetMacroDiagnostic.itemInMacroExpansion.diagnose(at: property)) + return [] + } - return (structDecl, optionsEnum, rawType) + return ["@OptionSetItem(bit: \(literal: bit))"] } } @@ -125,7 +203,7 @@ extension OptionSetMacro: ConformanceMacro { in context: some MacroExpansionContext ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { // Decode the expansion arguments. - guard let (structDecl, _, _) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { + guard let (structDecl, _) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { return [] } @@ -146,34 +224,18 @@ extension OptionSetMacro: MemberMacro { in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // Decode the expansion arguments. - guard let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { + guard let (_, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { return [] } - // Find all of the case elements. - let caseElements = optionsEnum.members.members.flatMap { member in - guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { - return Array() - } - - return Array(caseDecl.elements) - } - // Dig out the access control keyword we need. let access = decl.modifiers?.first(where: \.isNeededAccessLevelModifier) - let staticVars = caseElements.map { (element) -> DeclSyntax in - """ - \(access) static let \(element.identifier): Self = - Self(rawValue: 1 << \(optionsEnum.identifier).\(element.identifier).rawValue) - """ - } - return [ "\(access)typealias RawValue = \(rawType)", "\(access)var rawValue: RawValue", "\(access)init() { self.rawValue = 0 }", "\(access)init(rawValue: RawValue) { self.rawValue = rawValue }", - ] + staticVars + ] } } diff --git a/MacroExamplesPluginTest/MacroExamplesPluginTest.swift b/MacroExamplesPluginTest/MacroExamplesPluginTest.swift index fb0e574..05d3abe 100644 --- a/MacroExamplesPluginTest/MacroExamplesPluginTest.swift +++ b/MacroExamplesPluginTest/MacroExamplesPluginTest.swift @@ -6,6 +6,8 @@ import XCTest var testMacros: [String: Macro.Type] = [ "stringify" : StringifyMacro.self, + "OptionSet" : OptionSetMacro.self, + "OptionSetItem" : OptionSetItemMacro.self, ] final class MacroExamplesPluginTests: XCTestCase { @@ -27,4 +29,58 @@ final class MacroExamplesPluginTests: XCTestCase { """# ) } + + func testOptionSet() { + let sf: SourceFileSyntax = + """ + @OptionSet + struct ShippingOptions { + static var nextDay: ShippingOptions + static var secondDay: ShippingOptions + static var priority: ShippingOptions + static var standard: ShippingOptions + + static let express: ShippingOptions = [.nextDay, .secondDay] + static let all: ShippingOptions = [.express, .priority, .standard] + + } + """ + let context = BasicMacroExpansionContext.init( + sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")] + ) + let transformedSF = sf.expand(macros: testMacros, in: context) + XCTAssertEqual( + transformedSF.description, + #""" + + struct ShippingOptions { + static var nextDay: ShippingOptions { + get { + Self(rawValue: 1 << 0) + } + } + static var secondDay: ShippingOptions { + get { + Self(rawValue: 1 << 1) + } + } + static var priority: ShippingOptions { + get { + Self(rawValue: 1 << 2) + } + } + static var standard: ShippingOptions { + get { + Self(rawValue: 1 << 3) + } + } + + static let express: ShippingOptions = [.nextDay, .secondDay] + static let all: ShippingOptions = [.express, .priority, .standard]typealias RawValue = UInt8var rawValue: RawValueinit() { self.rawValue = 0 }init(rawValue: RawValue) { self.rawValue = rawValue } + + } + """# + ) + + } } diff --git a/MacroExamplesPluginTest/NewTypePluginTests.swift b/MacroExamplesPluginTest/NewTypePluginTests.swift index 7134478..d00e49c 100644 --- a/MacroExamplesPluginTest/NewTypePluginTests.swift +++ b/MacroExamplesPluginTest/NewTypePluginTests.swift @@ -17,16 +17,12 @@ final class NewTypePluginTests: XCTestCase { } """# - // print(sf.recursiveDescription) - let context = BasicMacroExpansionContext( sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")] ) let transformed = sf.expand(macros: testMacros, in: context) - // print(transformed.recursiveDescription) - XCTAssertEqual( transformed.description, #""" From ecf2e919576aaf14a72bb2d41df43c0e2a9be02f Mon Sep 17 00:00:00 2001 From: Doug Gregor Date: Wed, 15 Mar 2023 17:25:07 -0700 Subject: [PATCH 2/2] [Option set] Make the macro support several different forms of option sets --- MacroExamples.xcodeproj/project.pbxproj | 12 +- MacroExamples/main.swift | 24 ++ MacroExamplesLib/Macros.swift | 34 --- MacroExamplesLib/OptionSetMacros.swift | 96 +++++++ ...SetItemMacro.swift => BitfieldMacro.swift} | 4 +- MacroExamplesPlugin/OptionSetMacro.swift | 241 ++++++++++++++++-- .../MacroExamplesPluginTest.swift | 97 ++++++- 7 files changed, 447 insertions(+), 61 deletions(-) create mode 100644 MacroExamplesLib/OptionSetMacros.swift rename MacroExamplesPlugin/{OptionSetItemMacro.swift => BitfieldMacro.swift} (88%) diff --git a/MacroExamples.xcodeproj/project.pbxproj b/MacroExamples.xcodeproj/project.pbxproj index 854e162..5c98344 100644 --- a/MacroExamples.xcodeproj/project.pbxproj +++ b/MacroExamples.xcodeproj/project.pbxproj @@ -26,7 +26,8 @@ BD841F82294CE1F600DA4D81 /* AddBlocker.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD841F81294CE1F600DA4D81 /* AddBlocker.swift */; }; BD8A3130294947BD00E83EB9 /* Macros.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8A312F294947BD00E83EB9 /* Macros.swift */; }; BD8A31312949480600E83EB9 /* libMacroExamplesLib.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = BD8A3126294947A100E83EB9 /* libMacroExamplesLib.dylib */; }; - BDC559D629B857DF00F26DFF /* OptionSetItemMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC559D529B857DF00F26DFF /* OptionSetItemMacro.swift */; }; + BDB8C31129C26857000B4E7E /* OptionSetMacros.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDB8C31029C26857000B4E7E /* OptionSetMacros.swift */; }; + BDC559D629B857DF00F26DFF /* BitfieldMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDC559D529B857DF00F26DFF /* BitfieldMacro.swift */; }; BDF5AFE42947E5B000FA119B /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5AFE32947E5B000FA119B /* main.swift */; }; BDF5AFF82947E95C00FA119B /* StringifyMacro.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDF5AFF72947E95C00FA119B /* StringifyMacro.swift */; }; BDFB14B52948484000708DA6 /* MacroExamplesPluginTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFB14B42948484000708DA6 /* MacroExamplesPluginTest.swift */; }; @@ -98,7 +99,8 @@ BD841F81294CE1F600DA4D81 /* AddBlocker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddBlocker.swift; sourceTree = ""; }; BD8A3126294947A100E83EB9 /* libMacroExamplesLib.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libMacroExamplesLib.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; BD8A312F294947BD00E83EB9 /* Macros.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Macros.swift; sourceTree = ""; }; - BDC559D529B857DF00F26DFF /* OptionSetItemMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionSetItemMacro.swift; sourceTree = ""; }; + BDB8C31029C26857000B4E7E /* OptionSetMacros.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionSetMacros.swift; sourceTree = ""; }; + BDC559D529B857DF00F26DFF /* BitfieldMacro.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitfieldMacro.swift; sourceTree = ""; }; BDF5AFE02947E5B000FA119B /* MacroExamples */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = MacroExamples; sourceTree = BUILT_PRODUCTS_DIR; }; BDF5AFE32947E5B000FA119B /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; BDF5AFEE2947E61100FA119B /* libMacroExamplesPlugin.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = libMacroExamplesPlugin.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -161,7 +163,7 @@ 1D682A95299E3313006F9F78 /* CustomCodable.swift */, BD48319229AFF87200F3123A /* OptionSetMacro.swift */, 88E54A5129B5475400252D99 /* MetaEnumMacro.swift */, - BDC559D529B857DF00F26DFF /* OptionSetItemMacro.swift */, + BDC559D529B857DF00F26DFF /* BitfieldMacro.swift */, ); path = MacroExamplesPlugin; sourceTree = ""; @@ -170,6 +172,7 @@ isa = PBXGroup; children = ( BD8A312F294947BD00E83EB9 /* Macros.swift */, + BDB8C31029C26857000B4E7E /* OptionSetMacros.swift */, 31757820298DC4AF00D79290 /* NewType.swift */, ); path = MacroExamplesLib; @@ -385,6 +388,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + BDB8C31129C26857000B4E7E /* OptionSetMacros.swift in Sources */, BD8A3130294947BD00E83EB9 /* Macros.swift in Sources */, 31757821298DC4AF00D79290 /* NewType.swift in Sources */, ); @@ -407,7 +411,7 @@ 1D682A94299E2FFB006F9F78 /* CodableKey.swift in Sources */, 371A6719299C241F00E74A8A /* CaseDetectionMacro.swift in Sources */, BD752BE5294D3BEC00D00A2E /* WarningMacro.swift in Sources */, - BDC559D629B857DF00F26DFF /* OptionSetItemMacro.swift in Sources */, + BDC559D629B857DF00F26DFF /* BitfieldMacro.swift in Sources */, 3175781D298DBC8700D79290 /* NewTypeMacro.swift in Sources */, BDF5AFF82947E95C00FA119B /* StringifyMacro.swift in Sources */, EC21BDEB298D9F9900D585C6 /* ObservableMacro.swift in Sources */, diff --git a/MacroExamples/main.swift b/MacroExamples/main.swift index 49ad5f1..9dd127e 100644 --- a/MacroExamples/main.swift +++ b/MacroExamples/main.swift @@ -178,6 +178,30 @@ struct ShippingOptions { static let all: ShippingOptions = [.express, .priority, .standard] } +@MyOptionSet +struct ShippingOptionsNestedOptionsEnum { + private enum Options: UInt8 { + case nextDay, secondDay + case priority, standard + } + + static let express: ShippingOptionsNestedOptionsEnum = [.nextDay, .secondDay] + static let all: ShippingOptionsNestedOptionsEnum = [.express, .priority, .standard] +} + +@MyOptionSet +enum ShippingOptionsNestedOptionSet { + case nextDay, secondDay + case priority, standard +} + +#if false +// FIXME: Currently triggers a compilation error +extension ShippingOptionsNestedOptionSet.Set { + static let express: Self = [.nextDay, .secondDay] + static let all: Self = [.express, .priority, .standard] +} +#endif // `@MetaEnum` adds a nested enum called `Meta` with the same cases, but no // associated values/payloads. Handy for e.g. describing a schema. diff --git a/MacroExamplesLib/Macros.swift b/MacroExamplesLib/Macros.swift index 8017c29..4632d7a 100644 --- a/MacroExamplesLib/Macros.swift +++ b/MacroExamplesLib/Macros.swift @@ -107,37 +107,3 @@ public macro CodableKey(name: String) = #externalMacro(module: "MacroExamplesPlu @attached(member, names: arbitrary) public macro CustomCodable() = #externalMacro(module: "MacroExamplesPlugin", type: "CustomCodable") - -/// Create an option set from a struct that contains a nested `Options` enum. -/// -/// Attach this macro to a struct that contains a nested `Options` enum -/// with an integer raw value. The struct will be transformed to conform to -/// `OptionSet` by -/// 1. Introducing a `rawValue` stored property to track which options are set, -/// along with the necessary `RawType` typealias and initializers to satisfy -/// the `OptionSet` protocol. The raw type is specified after `@OptionSet`, -/// e.g., `@OptionSet`. -/// 2. Introducing static properties for each of the cases within the `Options` -/// enum, of the type of the struct. -/// -/// The `Options` enum must have a raw value, where its case elements -/// each indicate a different option in the resulting option set. For example, -/// the struct and its nested `Options` enum could look like this: -/// -/// @MyOptionSet -/// struct ShippingOptions { -/// private enum Options: Int { -/// case nextDay -/// case secondDay -/// case priority -/// case standard -/// } -/// } -@attached(member, names: named(RawValue), named(rawValue), named(`init`)) -@attached(conformance) -@attached(memberAttribute) -public macro MyOptionSet() = #externalMacro(module: "MacroExamplesPlugin", type: "OptionSetMacro") - - -@attached(accessor) -public macro OptionSetItem(bit: Int) = #externalMacro(module: "MacroExamplesPlugin", type: "OptionSetItemMacro") diff --git a/MacroExamplesLib/OptionSetMacros.swift b/MacroExamplesLib/OptionSetMacros.swift new file mode 100644 index 0000000..8802b72 --- /dev/null +++ b/MacroExamplesLib/OptionSetMacros.swift @@ -0,0 +1,96 @@ +/// Specifies the form of option set to which the option set macro is applied. +public enum OptionSetForm { + /// Options are described via static variables within the struct, allow + /// of which are expected to have the same type as `Self`. + /// the option set macros. + /// + /// @OptionSet + /// struct ShippingOptions { + /// static var nextDay: ShippingOptions + /// static var secondDay: ShippingOptions + /// static var priority: ShippingOptions + /// static var standard: ShippingOptions + /// } + /// + /// Any static variable that has an initializer already will be ignored. + case staticVariables + + /// Options are described via the cases of a nested enum with the given + /// name. + /// + /// @OptionSet + /// struct ShippingOptions { + /// private enum Options { + /// case nextDay + /// case secondDay + /// case priority + /// case standard + /// } + /// } + /// + /// The cases will be used to indicate bit positions in the resulting + /// raw value, and the `OptionSet` macro will introduced static variables + /// of the type of the struct itself (similar to those that have are + /// written explicitly for the `staticVariables` form). + case nestedOptionsEnum(String = "Options") + + /// Options are described via cases on the enum to which the option set + /// macro is applied. + /// + /// @OptionSet + /// enum ShippingOptions { + /// case nextDay + /// case secondDay + /// case priority + /// case standard + /// } + /// + /// As with `nestedEnum`, the cases provide the bit numbers for the + /// corresponding options in the raw value. With this kind, a nested + /// struct with the given name will be created that is itself an option + /// set, e.g., + /// + /// struct Set: OptionSet { + /// var rawValue: UInt8 + /// static var nextDay: Set = Set(rawValue: 1 << 0) + /// static var secondDay: Set = Set(rawValue: 1 << 1) + /// static var priority: Set = Set(rawValue: 1 << 2) + /// static var standard: Set = Set(rawValue: 1 << 3) + /// } + case nestedOptionSet(String = "Set") +} + +/// Create an bit-packed option set from a type that sketches the option names. +/// +/// TODO: Update this +/// +/// Attach this macro to a struct that contains a nested `Options` enum +/// with an integer raw value. The struct will be transformed to conform to +/// `OptionSet` by +/// 1. Introducing a `rawValue` stored property to track which options are set, +/// along with the necessary `RawType` typealias and initializers to satisfy +/// the `OptionSet` protocol. The raw type is specified after `@OptionSet`, +/// e.g., `@OptionSet`. +/// 2. Introducing static properties for each of the cases within the `Options` +/// enum, of the type of the struct. +/// +/// The `Options` enum must have a raw value, where its case elements +/// each indicate a different option in the resulting option set. For example, +/// the struct and its nested `Options` enum could look like this: +/// +/// @MyOptionSet +/// struct ShippingOptions { +/// private enum Options: Int { +/// case nextDay +/// case secondDay +/// case priority +/// case standard +/// } +/// } +@attached(member, names: named(RawValue), named(rawValue), named(`init`), arbitrary) +@attached(conformance) +@attached(memberAttribute) +public macro MyOptionSet() = #externalMacro(module: "MacroExamplesPlugin", type: "OptionSetMacro") + +@attached(accessor) +public macro Bitfield(bit: Int) = #externalMacro(module: "MacroExamplesPlugin", type: "BitfieldMacro") diff --git a/MacroExamplesPlugin/OptionSetItemMacro.swift b/MacroExamplesPlugin/BitfieldMacro.swift similarity index 88% rename from MacroExamplesPlugin/OptionSetItemMacro.swift rename to MacroExamplesPlugin/BitfieldMacro.swift index b9f8251..16bd3ad 100644 --- a/MacroExamplesPlugin/OptionSetItemMacro.swift +++ b/MacroExamplesPlugin/BitfieldMacro.swift @@ -2,9 +2,9 @@ import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros -public struct OptionSetItemMacro { } +public struct BitfieldMacro { } -extension OptionSetItemMacro: AccessorMacro { +extension BitfieldMacro: AccessorMacro { public static func expansion( of attribute: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, diff --git a/MacroExamplesPlugin/OptionSetMacro.swift b/MacroExamplesPlugin/OptionSetMacro.swift index 9f6dcc3..3f80d9d 100644 --- a/MacroExamplesPlugin/OptionSetMacro.swift +++ b/MacroExamplesPlugin/OptionSetMacro.swift @@ -55,22 +55,135 @@ extension TupleExprElementListSyntax { } } +// Mirrored from the library because I'm too lazy to factor it out right now +private enum OptionSetForm: Equatable { + case staticVariables + case nestedOptionsEnum(String = "Options") + case nestedOptionSet(String = "Set") +} + public struct OptionSetMacro { + /// Decode a "form:" argument to the option set macro, if present. + private static func decodeOptionSetFormArgument( + of attribute: AttributeSyntax, + in context: some MacroExpansionContext + ) -> OptionSetForm? { + // Dig out the "form" argument, if there is one. + guard case let .argumentList(arguments) = attribute.argument, + let formArgument = arguments.first(labeled: "form")?.expression else { + // No argument, this is fine. + return nil + } + + // If it was explicitly `nil`, then we've been asked to infer the form from + // the structure. + if formArgument.is(NilLiteralExprSyntax.self) { + return nil + } + + // If there is a call (e.g., for .nestedOptionsEnum("Flags")), dig out + // the argument string ("Flags") and look through the call. + let enumElement: ExprSyntax + let callArgument: String? + if let call = formArgument.as(FunctionCallExprSyntax.self) { + if call.argumentList.isEmpty { + callArgument = nil + } else if call.argumentList.count == 1, + let firstArg = call.argumentList.first, + let stringLiteral = firstArg.expression.as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(stringArgument)? = stringLiteral.segments.first { + callArgument = stringArgument.content.text + } else { + // FIXME: Diagnose extra/wrong arguments. + return nil + } + + enumElement = call.calledExpression + } else { + callArgument = nil + enumElement = formArgument + } + + // Make sure we have `.` or `OptionSetForm.`. + guard let memberAccess = enumElement.as(MemberAccessExprSyntax.self), + memberAccess.base == nil || + memberAccess.base?.trimmedDescription == "OptionSetForm" + else { + // FIXME: Produce an error; we don't recognize the argument. + return nil + } + + switch memberAccess.name.text { + case "staticVariables": + return .staticVariables + + case "nestedOptionsEnum": + return .nestedOptionsEnum(callArgument ?? "Options") + + case "nestedOptionSet": + return .nestedOptionSet(callArgument ?? "Set") + + default: + // FIXME: Produce an error; we don't recognize the enum case. + return nil + } + } + + /// Find a nested enum with the given name in the given declaration. + static func findNestedEnum(named: String, in decl: some DeclGroupSyntax) -> EnumDeclSyntax? { + return decl.members.members.lazy.compactMap { member in + if let enumDecl = member.decl.as(EnumDeclSyntax.self), + enumDecl.identifier.text == named { + return enumDecl + } + + return nil + }.first + } + + /// Infer the form of the option set from from declaration to which the + /// option set macro is attached. + private static func inferOptionSetForm(_ decl: some DeclGroupSyntax) -> OptionSetForm? { + // If the macro is attached to an enum, produce a nested option set struct. + if decl.is(EnumDeclSyntax.self) { + return .nestedOptionSet() + } + + // If the macro is attached to something other than a struct, we can't + // infer anything. + guard let structDecl = decl.as(StructDeclSyntax.self) else { + return nil + } + + // If there is a nested "Options" enum, use it. + if let _ = findNestedEnum(named: "Options", in: structDecl) { + return .nestedOptionsEnum() + } + + // If there is at least one static variable that meets the criteria, we + // can update static variables. + for member in structDecl.members.members { + if let varDecl = member.decl.as(VariableDeclSyntax.self), + let _ = varDecl.getOptionSetItemCandidateName(structName: structDecl.identifier) { + return .staticVariables + } + } + + // There isn't enough structure to infer the form of the option set. + return nil + } + /// Decodes the arguments to the macro expansion. /// /// - Returns: the important arguments used by the various roles of this /// macro inhabits, or nil if an error occurred. - static func decodeExpansion( + fileprivate static func decodeExpansion( of attribute: AttributeSyntax, attachedTo decl: some DeclGroupSyntax, in context: some MacroExpansionContext - ) -> (StructDeclSyntax, TypeSyntax)? { + ) -> (TypeSyntax, OptionSetForm)? { // Only apply to structs. - guard let structDecl = decl.as(StructDeclSyntax.self) else { - context.diagnose(OptionSetMacroDiagnostic.requiresStruct.diagnose(at: decl)) - return nil - } - // Retrieve the raw type from the attribute. guard let genericArgs = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.genericArgumentClause, let rawType = genericArgs.arguments.first?.argumentType else { @@ -78,8 +191,17 @@ public struct OptionSetMacro { return nil } + let form: OptionSetForm + if let specifiedForm = decodeOptionSetFormArgument(of: attribute, in: context) { + form = specifiedForm + } else if let inferredForm = inferOptionSetForm(decl) { + form = inferredForm + } else { + // FIXME: produce an error + return nil + } - return (structDecl, rawType) + return (rawType, form) } } @@ -155,7 +277,13 @@ extension OptionSetMacro: MemberAttributeMacro { in context: some MacroExpansionContext ) throws -> [AttributeSyntax] { // Decode the expansion arguments. - guard let (structDecl, _) = decodeExpansion(of: attribute, attachedTo: declaration, in: context) else { + // + // The member-attribute expansion of the OptionSet macro only applies to the static-variables + // form of the option set, where the static variables already exist and need to be + // annotated with the appropriate `@Bitfield` attribute. + guard let (_, form) = decodeExpansion(of: attribute, attachedTo: declaration, in: context), + form == .staticVariables, + let structDecl = declaration.as(StructDeclSyntax.self) else { return [] } @@ -184,7 +312,7 @@ extension OptionSetMacro: MemberAttributeMacro { bit += 1 } - // If we did not found our member in the list, fail. This could happen + // If we did not find our member in the list, fail. This could happen // if the item came from another macro expansion. if !found { context.diagnose( @@ -192,7 +320,7 @@ extension OptionSetMacro: MemberAttributeMacro { return [] } - return ["@OptionSetItem(bit: \(literal: bit))"] + return ["@Bitfield(bit: \(literal: bit))"] } } @@ -203,10 +331,25 @@ extension OptionSetMacro: ConformanceMacro { in context: some MacroExpansionContext ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { // Decode the expansion arguments. - guard let (structDecl, _) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { + // + // The conformance expansion is only relevant to the nested-options-enum + // and static-variables forms, which add the conformance. In both cases, + // we are dealing with a struct. + guard let (_, form) = decodeExpansion(of: attribute, attachedTo: decl, in: context), + let structDecl = decl.as(StructDeclSyntax.self) else { return [] } + switch form { + case .nestedOptionSet: + // The nested option set form doesn't add any conformances; the conformance + // is on the inner type. + return [] + + case .nestedOptionsEnum, .staticVariables: + break + } + // If there is an explicit conformance to OptionSet already, don't add one. if let inheritedTypes = structDecl.inheritanceClause?.inheritedTypeCollection, inheritedTypes.contains(where: { inherited in inherited.typeName.trimmedDescription == "OptionSet" }) { @@ -217,25 +360,87 @@ extension OptionSetMacro: ConformanceMacro { } } +extension EnumDeclSyntax { + /// Retrieve a flattened set of all of the case elements in the enum. + var allCaseElements: [EnumCaseElementSyntax] { + return members.members.flatMap { member in + guard let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) else { + return Array() + } + + return Array(caseDecl.elements) + } + } +} + extension OptionSetMacro: MemberMacro { + /// Create the set of static variables that provided the option values, using the + /// cases of the given enum as input. + private static func makeStaticVariables( + forCasesOf enumDecl: EnumDeclSyntax, + access: ModifierListSyntax.Element? + ) -> [DeclSyntax] { + let allCases = enumDecl.allCaseElements + return allCases.map { (element) -> DeclSyntax in + """ + + \(access) static let \(element.identifier.trimmed): Self = + Self(rawValue: 1 << \(enumDecl.identifier.trimmed).\(element.identifier).rawValue) + """ + } + } + public static func expansion( of attribute: AttributeSyntax, providingMembersOf decl: some DeclGroupSyntax, in context: some MacroExpansionContext ) throws -> [DeclSyntax] { // Decode the expansion arguments. - guard let (_, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { + guard let (rawType, form) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { return [] } // Dig out the access control keyword we need. let access = decl.modifiers?.first(where: \.isNeededAccessLevelModifier) - return [ - "\(access)typealias RawValue = \(rawType)", - "\(access)var rawValue: RawValue", - "\(access)init() { self.rawValue = 0 }", - "\(access)init(rawValue: RawValue) { self.rawValue = rawValue }", + // Across all forms, the raw value members are the same. + // FIXME: We should filter out any of these that were already provided. + let rawValueMembers: [DeclSyntax] = [ + "\n\(access)typealias RawValue = \(rawType)", + "\n\(access)var rawValue: RawValue", + "\n\(access)init() { self.rawValue = 0 }", + "\n\(access)init(rawValue: RawValue) { self.rawValue = rawValue }", ] + + switch form { + case .staticVariables: + // When annotating static variables, we only need the raw-value members. + // Everything else has already been declared. + return rawValueMembers + + case .nestedOptionsEnum(let optionsSetEnumName): + guard let optionSetEnum = findNestedEnum(named: optionsSetEnumName, in: decl) else { + // FIXME: Diagnose missing option enum + return rawValueMembers + } + + return rawValueMembers + makeStaticVariables(forCasesOf: optionSetEnum, access: access) + + case .nestedOptionSet(_): + guard let enumDecl = decl.as(EnumDeclSyntax.self) else { + // FIXME: Diagnose not-on-an-enum + return rawValueMembers + } + + return [ + """ + + \(access)struct Set: OptionSet {\(raw: (rawValueMembers + makeStaticVariables(forCasesOf: enumDecl, access: access)).map { + $0.description + }.joined(separator: "")) + } + """ + ] + } } } diff --git a/MacroExamplesPluginTest/MacroExamplesPluginTest.swift b/MacroExamplesPluginTest/MacroExamplesPluginTest.swift index 05d3abe..11bc4b0 100644 --- a/MacroExamplesPluginTest/MacroExamplesPluginTest.swift +++ b/MacroExamplesPluginTest/MacroExamplesPluginTest.swift @@ -7,7 +7,7 @@ import XCTest var testMacros: [String: Macro.Type] = [ "stringify" : StringifyMacro.self, "OptionSet" : OptionSetMacro.self, - "OptionSetItem" : OptionSetItemMacro.self, + "Bitfield" : BitfieldMacro.self, ] final class MacroExamplesPluginTests: XCTestCase { @@ -30,7 +30,7 @@ final class MacroExamplesPluginTests: XCTestCase { ) } - func testOptionSet() { + func testOptionSetWithStaticVariables() { let sf: SourceFileSyntax = """ @OptionSet @@ -76,11 +76,102 @@ final class MacroExamplesPluginTests: XCTestCase { } static let express: ShippingOptions = [.nextDay, .secondDay] - static let all: ShippingOptions = [.express, .priority, .standard]typealias RawValue = UInt8var rawValue: RawValueinit() { self.rawValue = 0 }init(rawValue: RawValue) { self.rawValue = rawValue } + static let all: ShippingOptions = [.express, .priority, .standard] + typealias RawValue = UInt8 + var rawValue: RawValue + init() { self.rawValue = 0 } + init(rawValue: RawValue) { self.rawValue = rawValue } } """# ) + } + func testOptionSetWithNestedOptionsEnum() { + let sf: SourceFileSyntax = + """ + @OptionSet + struct ShippingOptions { + private enum Options { + case nextDay, secondDay + case priority, standard + } + + static let express: ShippingOptions = [.nextDay, .secondDay] + static let all: ShippingOptions = [.express, .priority, .standard] + + } + """ + let context = BasicMacroExpansionContext.init( + sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")] + ) + let transformedSF = sf.expand(macros: testMacros, in: context) + XCTAssertEqual( + transformedSF.description, + #""" + + struct ShippingOptions { + private enum Options { + case nextDay, secondDay + case priority, standard + } + + static let express: ShippingOptions = [.nextDay, .secondDay] + static let all: ShippingOptions = [.express, .priority, .standard] + typealias RawValue = UInt8 + var rawValue: RawValue + init() { self.rawValue = 0 } + init(rawValue: RawValue) { self.rawValue = rawValue } + static let nextDay: Self = + Self(rawValue: 1 << Options.nextDay.rawValue) + static let secondDay: Self = + Self(rawValue: 1 << Options.secondDay.rawValue) + static let priority: Self = + Self(rawValue: 1 << Options.priority.rawValue) + static let standard: Self = + Self(rawValue: 1 << Options.standard.rawValue) + + } + """# + ) + } + + func testOptionSetWithNestedOptionSet() { + let sf: SourceFileSyntax = + """ + @OptionSet + enum ShippingOptions { + case nextDay, secondDay + case priority, standard + } + """ + let context = BasicMacroExpansionContext.init( + sourceFiles: [sf: .init(moduleName: "MyModule", fullFilePath: "test.swift")] + ) + let transformedSF = sf.expand(macros: testMacros, in: context) + XCTAssertEqual( + transformedSF.description, + #""" + + enum ShippingOptions { + case nextDay, secondDay + case priority, standard + struct Set: OptionSet { + typealias RawValue = UInt8 + var rawValue: RawValue + init() { self.rawValue = 0 } + init(rawValue: RawValue) { self.rawValue = rawValue } + static let nextDay: Self = + Self(rawValue: 1 << ShippingOptions.nextDay.rawValue) + static let secondDay: Self = + Self(rawValue: 1 << ShippingOptions.secondDay.rawValue) + static let priority: Self = + Self(rawValue: 1 << ShippingOptions.priority.rawValue) + static let standard: Self = + Self(rawValue: 1 << ShippingOptions.standard.rawValue) + } + } + """# + ) } }