diff --git a/lib/IDE/CompletionLookup.cpp b/lib/IDE/CompletionLookup.cpp index f752cf3ea3b2..a322c5a8e671 100644 --- a/lib/IDE/CompletionLookup.cpp +++ b/lib/IDE/CompletionLookup.cpp @@ -1783,7 +1783,8 @@ void CompletionLookup::addEnumElementRef(const EnumElementDecl *EED, void CompletionLookup::addMacroExpansion(const MacroDecl *MD, DeclVisibilityKind Reason) { if (!MD->hasName() || !MD->isAccessibleFrom(CurrDeclContext) || - MD->shouldHideFromEditor()) + MD->shouldHideFromEditor() || + !isFreestandingMacro(MD->getMacroRoles())) return; CodeCompletionResultBuilder Builder( diff --git a/lib/Macros/CMakeLists.txt b/lib/Macros/CMakeLists.txt index 5142c1a735b5..ed98b3d9f578 100644 --- a/lib/Macros/CMakeLists.txt +++ b/lib/Macros/CMakeLists.txt @@ -56,4 +56,5 @@ function(add_swift_macro_library name) set_property(GLOBAL APPEND PROPERTY SWIFT_MACRO_PLUGINS ${name}) endfunction() +add_subdirectory(Sources/SwiftMacros) add_subdirectory(Sources/ObservationMacros) diff --git a/lib/Macros/Sources/SwiftMacros/CMakeLists.txt b/lib/Macros/Sources/SwiftMacros/CMakeLists.txt new file mode 100644 index 000000000000..b4fdee3e2be8 --- /dev/null +++ b/lib/Macros/Sources/SwiftMacros/CMakeLists.txt @@ -0,0 +1,15 @@ +#===--- CMakeLists.txt - Swift macros library ----------------------===# +# +# This source file is part of the Swift.org open source project +# +# Copyright (c) 2023 Apple Inc. and the Swift project authors +# Licensed under Apache License v2.0 with Runtime Library Exception +# +# See https://swift.org/LICENSE.txt for license information +# See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +# +#===----------------------------------------------------------------------===# + +add_swift_macro_library(SwiftMacros + OptionSetMacro.swift +) diff --git a/lib/Macros/Sources/SwiftMacros/OptionSetMacro.swift b/lib/Macros/Sources/SwiftMacros/OptionSetMacro.swift new file mode 100644 index 000000000000..38f89b8ad250 --- /dev/null +++ b/lib/Macros/Sources/SwiftMacros/OptionSetMacro.swift @@ -0,0 +1,207 @@ +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxBuilder +import SwiftSyntaxMacros + +enum OptionSetMacroDiagnostic { + case requiresStruct + case requiresStringLiteral(String) + case requiresOptionsEnum(String) + case requiresOptionsEnumRawType +} + +extension OptionSetMacroDiagnostic: DiagnosticMessage { + func diagnose(at node: Node) -> Diagnostic { + Diagnostic(node: Syntax(node), message: self) + } + + var message: String { + switch self { + case .requiresStruct: + return "'OptionSet' macro can only be applied to a struct" + + case .requiresStringLiteral(let name): + return "'OptionSet' macro argument \(name) must be a string literal" + + case .requiresOptionsEnum(let name): + return "'OptionSet' macro requires nested options enum '\(name)'" + + case .requiresOptionsEnumRawType: + return "'OptionSet' macro requires a raw type" + } + } + + var severity: DiagnosticSeverity { .error } + + var diagnosticID: MessageID { + MessageID(domain: "Swift", id: "OptionSet.\(self)") + } +} + + +/// 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? { + return first { element in + if let label = element.label, label.text == name { + return true + } + + return false + } + } +} + +public struct OptionSetMacro { + /// 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< + Decl: DeclGroupSyntax, + Context: MacroExpansionContext + >( + of attribute: AttributeSyntax, + attachedTo decl: Decl, + in context: Context + ) -> (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 + } + + // 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. + let optionsEnums: [EnumDeclSyntax] = decl.members.members.compactMap({ member in + if let enumDecl = member.decl.as(EnumDeclSyntax.self), + enumDecl.identifier.text == optionsEnumName { + return enumDecl + } + + return nil + }) + + guard let optionsEnum = optionsEnums.first else { + context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnum(optionsEnumName).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 { + context.diagnose(OptionSetMacroDiagnostic.requiresOptionsEnumRawType.diagnose(at: attribute)) + return nil + } + + + return (structDecl, optionsEnum, rawType) + } +} + +extension OptionSetMacro: ConformanceMacro { + public static func expansion< + Decl: DeclGroupSyntax, + Context: MacroExpansionContext + >( + of attribute: AttributeSyntax, + providingConformancesOf decl: Decl, + in context: Context + ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { + // Decode the expansion arguments. + guard let (structDecl, _, _) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { + return [] + } + + // 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" }) { + return [] + } + + return [("OptionSet", nil)] + } +} + +extension OptionSetMacro: MemberMacro { + public static func expansion< + Decl: DeclGroupSyntax, + Context: MacroExpansionContext + >( + of attribute: AttributeSyntax, + providingMembersOf decl: Decl, + in context: Context + ) throws -> [DeclSyntax] { + // Decode the expansion arguments. + guard let (_, optionsEnum, rawType) = decodeExpansion(of: attribute, attachedTo: decl, in: context) else { + return [] + } + + // Find all of the case elements. + var caseElements: [EnumCaseElementSyntax] = [] + for member in optionsEnum.members.members { + if let caseDecl = member.decl.as(EnumCaseDeclSyntax.self) { + caseElements.append(contentsOf: 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 + } +} + +extension DeclModifierSyntax { + var isNeededAccessLevelModifier: Bool { + switch self.name.tokenKind { + case .keyword(.public): return true + default: return false + } + } +} + +extension SyntaxStringInterpolation { + // It would be nice for SwiftSyntaxBuilder to provide this out-of-the-box. + mutating func appendInterpolation(_ node: Node?) { + if let node = node { + appendInterpolation(node) + } + } +} diff --git a/stdlib/public/core/Macros.swift b/stdlib/public/core/Macros.swift index 4cfb44a0bd91..6b8a3eae506e 100644 --- a/stdlib/public/core/Macros.swift +++ b/stdlib/public/core/Macros.swift @@ -60,3 +60,33 @@ public macro column() -> T = /// Produces the shared object handle for the macro expansion location. @freestanding(expression) public macro dsohandle() -> UnsafeRawPointer = Builtin.DSOHandleMacro + +/// 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: +/// +/// @OptionSet +/// struct ShippingOptions { +/// private enum Options: Int { +/// case nextDay +/// case secondDay +/// case priority +/// case standard +/// } +/// } +@attached(member) +@attached(conformance) +public macro OptionSet() = + #externalMacro(module: "SwiftMacros", type: "OptionSetMacro") diff --git a/test/Macros/option_set.swift b/test/Macros/option_set.swift new file mode 100644 index 000000000000..dadd37c81e3b --- /dev/null +++ b/test/Macros/option_set.swift @@ -0,0 +1,24 @@ +// RUN: %target-run-simple-swift(-Xfrontend -plugin-path -Xfrontend %swift-host-lib-dir/plugins) +// REQUIRES: executable_test +// REQUIRES: OS=macosx + +import Swift + +@OptionSet +struct ShippingOptions { + private enum Options: Int { + case nextDay + case secondDay + case priority + case standard + } + + static let express: ShippingOptions = [.nextDay, .secondDay] + static let all: ShippingOptions = [.express, .priority, .standard] +} + +let options = ShippingOptions.express +assert(options.contains(.nextDay)) +assert(options.contains(.secondDay)) +assert(!options.contains(.standard)) +