diff --git a/Package.swift b/Package.swift index cad08953c45..5e657564a78 100644 --- a/Package.swift +++ b/Package.swift @@ -196,16 +196,14 @@ let package = Package( .testTarget( name: "SwiftSyntaxMacroExpansionTest", - dependencies: [ - "SwiftSyntax", "_SwiftSyntaxTestSupport", "SwiftSyntaxMacroExpansion", "SwiftSyntaxBuilder", - ] + dependencies: ["SwiftSyntax", "_SwiftSyntaxTestSupport", "SwiftSyntaxMacroExpansion", "SwiftSyntaxBuilder"] ), // MARK: SwiftSyntaxMacrosTestSupport .target( name: "SwiftSyntaxMacrosTestSupport", - dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftParser", "SwiftSyntaxMacros"] + dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftParser", "SwiftSyntaxMacros", "SwiftSyntaxMacroExpansion"] ), // MARK: SwiftParser diff --git a/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift new file mode 100644 index 00000000000..4815fc8eef3 --- /dev/null +++ b/Sources/SwiftSyntaxMacroExpansion/BasicMacroExpansionContext.swift @@ -0,0 +1,194 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 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 +// +//===----------------------------------------------------------------------===// + +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +/// An implementation of the `MacroExpansionContext` protocol that is +/// suitable for testing purposes. +public class BasicMacroExpansionContext { + /// A single source file that is known to the macro expansion context. + public struct KnownSourceFile { + /// The name of the module in which this source file resides. + let moduleName: String + + /// The full path to the file. + let fullFilePath: String + + public init(moduleName: String, fullFilePath: String) { + self.moduleName = moduleName + self.fullFilePath = fullFilePath + } + } + + /// Create a new macro evaluation context. + public init( + expansionDiscriminator: String = "__macro_local_", + sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] + ) { + self.expansionDiscriminator = expansionDiscriminator + self.sourceFiles = sourceFiles + } + + /// The set of diagnostics that were emitted as part of expanding the + /// macro. + public private(set) var diagnostics: [Diagnostic] = [] + + /// Mapping from the root source file syntax nodes to the known source-file + /// information about that source file. + private var sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] + + /// Mapping from intentionally-disconnected syntax node roots to the + /// absolute offsets that have within a given source file, which is used + /// to establish the link between a node that been intentionally disconnected + /// from a source file to hide information from the macro implementation. + private var disconnectedNodes: [Syntax: (SourceFileSyntax, Int)] = [:] + + /// The macro expansion discriminator, which is used to form unique names + /// when requested. + /// + /// The expansion discriminator is combined with the `uniqueNames` counters + /// to produce unique names. + private var expansionDiscriminator: String = "" + + /// Counter for each of the uniqued names. + /// + /// Used in conjunction with `expansionDiscriminator`. + private var uniqueNames: [String: Int] = [:] + +} + +extension BasicMacroExpansionContext { + /// Note that the given node that was at the given position in the provided + /// source file has been disconnected and is now a new root. + private func addDisconnected( + _ node: some SyntaxProtocol, + at offset: AbsolutePosition, + in sourceFile: SourceFileSyntax + ) { + disconnectedNodes[Syntax(node)] = (sourceFile, offset.utf8Offset) + } + + /// Detach the given node, and record where it came from. + public func detach(_ node: Node) -> Node { + let detached = node.detached + + if let rootSourceFile = node.root.as(SourceFileSyntax.self) { + addDisconnected(detached, at: node.position, in: rootSourceFile) + } + + return detached + } +} + +extension String { + /// Retrieve the base name of a string that represents a path, removing the + /// directory. + fileprivate var basename: String { + guard let lastSlash = lastIndex(of: "/") else { + return self + } + + return String(self[index(after: lastSlash)...]) + } + +} +extension BasicMacroExpansionContext: MacroExpansionContext { + /// Generate a unique name for use in the macro. + public func makeUniqueName(_ providedName: String) -> TokenSyntax { + // If provided with an empty name, substitute in something. + let name = providedName.isEmpty ? "__local" : providedName + + // Grab a unique index value for this name. + let uniqueIndex = uniqueNames[name, default: 0] + uniqueNames[name] = uniqueIndex + 1 + + // Start with the expansion discriminator. + var resultString = expansionDiscriminator + + // Mangle the name + resultString += "\(name.count)\(name)" + + // Mangle the operator for unique macro names. + resultString += "fMu" + + // Mangle the index. + if uniqueIndex > 0 { + resultString += "\(uniqueIndex - 1)" + } + resultString += "_" + + return TokenSyntax(.identifier(resultString), presence: .present) + } + + /// Produce a diagnostic while expanding the macro. + public func diagnose(_ diagnostic: Diagnostic) { + diagnostics.append(diagnostic) + } + + public func location( + of node: some SyntaxProtocol, + at position: PositionInSyntaxNode, + filePathMode: SourceLocationFilePathMode + ) -> AbstractSourceLocation? { + // Dig out the root source file and figure out how we need to adjust the + // offset of the given syntax node to adjust for it. + let rootSourceFile: SourceFileSyntax + let offsetAdjustment: Int + if let directRootSourceFile = node.root.as(SourceFileSyntax.self) { + // The syntax node came from the source file itself. + rootSourceFile = directRootSourceFile + offsetAdjustment = 0 + } else if let (adjustedSourceFile, offset) = disconnectedNodes[Syntax(node)] { + // The syntax node came from a disconnected root, so adjust for that. + rootSourceFile = adjustedSourceFile + offsetAdjustment = offset + } else { + return nil + } + + guard let knownRoot = sourceFiles[rootSourceFile] else { + return nil + } + + // Determine the filename to use in the resulting location. + let fileName: String + switch filePathMode { + case .fileID: + fileName = "\(knownRoot.moduleName)/\(knownRoot.fullFilePath.basename)" + + case .filePath: + fileName = knownRoot.fullFilePath + } + + // Find the node's offset relative to its root. + let rawPosition: AbsolutePosition + switch position { + case .beforeLeadingTrivia: + rawPosition = node.position + + case .afterLeadingTrivia: + rawPosition = node.positionAfterSkippingLeadingTrivia + + case .beforeTrailingTrivia: + rawPosition = node.endPositionBeforeTrailingTrivia + + case .afterTrailingTrivia: + rawPosition = node.endPosition + } + + // Do the location lookup. + let converter = SourceLocationConverter(file: fileName, tree: rootSourceFile) + return AbstractSourceLocation(converter.location(for: rawPosition.advanced(by: offsetAdjustment))) + } +} diff --git a/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt b/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt index 38ba9a9cfb8..cb84c48bfdf 100644 --- a/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt +++ b/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt @@ -1,7 +1,10 @@ add_swift_host_library(SwiftSyntaxMacroExpansion + BasicMacroExpansionContext.swift FunctionParameterUtils.swift MacroExpansion.swift MacroReplacement.swift + MacroSystem.swift + Syntax+MacroEvaluation.swift ) target_link_libraries(SwiftSyntaxMacroExpansion PUBLIC diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift new file mode 100644 index 00000000000..cdf48e00931 --- /dev/null +++ b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift @@ -0,0 +1,631 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 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 +// +//===----------------------------------------------------------------------===// + +import SwiftDiagnostics +import SwiftSyntax +@_spi(MacroExpansion) import SwiftSyntaxMacros + +private func expandMemberAttributeMacro(attribute: AttributeSyntax, attachedTo: DeclSyntax) -> AttributeListSyntax { + fatalError("unimplemented") +} + +private func expandMemberMacro(attribute: AttributeSyntax, attachedTo: DeclGroupSyntax) -> MemberDeclListSyntax { + fatalError("unimplemented") +} + +private func expandPeerMacro(attribute: AttributeSyntax, attachedTo: DeclSyntax) -> CodeBlockItemListSyntax { + fatalError("unimplemented") +} + +private func expandConformanceMacro(attribute: AttributeSyntax, attachedTo: DeclSyntax) -> CodeBlockItemListSyntax { + fatalError("unimplemented") +} + +private func expandAccessorMacro(attribute: AttributeSyntax, attachedTo: DeclSyntax) -> AccessorListSyntax { + fatalError("unimplemented") +} + +/// Describes the kinds of errors that can occur within a macro system. +enum MacroSystemError: Error { + /// Indicates that a macro with the given name has already been defined. + case alreadyDefined(new: Macro.Type, existing: Macro.Type) + + /// Indicates that an unknown macro was encountered during expansion. + case unknownMacro(name: String, node: Syntax) + + /// Indicates that a macro evaluated as an expression by the given node + /// is not an expression macro. + case requiresExpressionMacro(macro: Macro.Type, node: Syntax) + + /// Indicates that a macro evaluated as a code item by the given node + /// is not suitable for code items. + case requiresCodeItemMacro(macro: Macro.Type, node: Syntax) + + /// Indicates that a macro produced diagnostics during evaluation. The + /// diagnostics might not specifically include errors, but will be reported + /// nonetheless. + case evaluationDiagnostics(node: Syntax, diagnostics: [Diagnostic]) +} + +/// A system of known macros that can be expanded syntactically +struct MacroSystem { + var macros: [String: Macro.Type] = [:] + + /// Create an empty macro system. + init() {} + + /// Add a macro to the system. + /// + /// Throws an error if there is already a macro with this name. + mutating func add(_ macro: Macro.Type, name: String) throws { + if let knownMacro = macros[name] { + throw MacroSystemError.alreadyDefined(new: macro, existing: knownMacro) + } + + macros[name] = macro + } + + /// Look for a macro with the given name. + func lookup(_ macroName: String) -> Macro.Type? { + return macros[macroName] + } +} + +/// Syntax rewriter that evaluates any macros encountered along the way. +class MacroApplication: SyntaxRewriter { + let macroSystem: MacroSystem + var context: Context + var skipNodes: Set = [] + + /// A stack of member attribute macos to expand when iterating over a ``MemberDeclListSyntax``. + var memberAttributeMacros: [([(AttributeSyntax, MemberAttributeMacro.Type)], DeclSyntax)] = [] + + init( + macroSystem: MacroSystem, + context: Context + ) { + self.macroSystem = macroSystem + self.context = context + super.init(viewMode: .sourceAccurate) + } + + override func visitAny(_ node: Syntax) -> Syntax? { + if skipNodes.contains(node) { + return nil + } + + if node.evaluatedMacroName != nil { + return node.evaluateMacro( + with: macroSystem, + context: context + ) + } + + if let declSyntax = node.as(DeclSyntax.self), + let attributedNode = node.asProtocol(WithAttributesSyntax.self), + let attributes = attributedNode.attributes + { + // Visit the node. + skipNodes.insert(node) + let visitedNode = self.visit(declSyntax).asProtocol(WithAttributesSyntax.self)! + skipNodes.remove(node) + + // Remove any attached attributes. + let newAttributes = attributes.filter { + guard case let .attribute(attribute) = $0 else { + return true + } + + guard let attributeName = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.name.text, + let macro = macroSystem.macros[attributeName] + else { + return true + } + + return + !(macro is PeerMacro.Type + || macro is MemberMacro.Type + || macro is AccessorMacro.Type + || macro is MemberAttributeMacro.Type + || macro is ConformanceMacro.Type + || macro is ExtensionMacro.Type) + } + + if newAttributes.isEmpty { + return Syntax(fromProtocol: visitedNode.with(\.attributes, nil)) + } + + return Syntax(fromProtocol: visitedNode.with(\.attributes, AttributeListSyntax(newAttributes))) + } + + return nil + } + + override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax { + var newItems: [CodeBlockItemSyntax] = [] + for item in node { + if let expansion = item.item.asProtocol(FreestandingMacroExpansionSyntax.self), + let macro = macroSystem.macros[expansion.macro.text] + { + func _expand(expansion: some FreestandingMacroExpansionSyntax) throws { + if let macro = macro as? CodeItemMacro.Type { + let expandedItemList = try macro.expansion( + of: expansion, + in: context + ) + newItems.append(contentsOf: expandedItemList) + } else if let macro = macro as? DeclarationMacro.Type { + var expandedItemList = try macro.expansion( + of: expansion, + in: context + ) + if let declExpansion = expansion.as(MacroExpansionDeclSyntax.self) { + let attributes = macro.propagateFreestandingMacroAttributes ? declExpansion.attributes : nil + let modifiers = macro.propagateFreestandingMacroModifiers ? declExpansion.modifiers : nil + expandedItemList = expandedItemList.map { + $0.applying(attributes: attributes, modifiers: modifiers) + } + } + newItems.append( + contentsOf: expandedItemList.map { + CodeBlockItemSyntax(item: .decl($0)) + } + ) + } else if let macro = macro as? ExpressionMacro.Type { + let expandedExpr = try macro.expansion( + of: expansion, + in: context + ) + newItems.append(CodeBlockItemSyntax(item: .init(expandedExpr))) + } + } + do { + try _openExistential(expansion, do: _expand) + } catch { + context.addDiagnostics(from: error, node: node) + } + + continue + } + + // Recurse on the child node. + let newItem = visit(item.item) + newItems.append(item.with(\.item, newItem)) + + // Expand any peer declarations or conformances triggered by macros used + // as attributes. + if case let .decl(decl) = item.item { + let peers = expandPeers(of: decl) + newItems.append( + contentsOf: peers.map { + newDecl in CodeBlockItemSyntax(item: .decl(newDecl)) + } + ) + + if let declGroup = decl.asProtocol(DeclGroupSyntax.self) { + newItems.append( + contentsOf: expandConformances(of: declGroup).map { + newDecl in CodeBlockItemSyntax(item: .decl(newDecl)) + } + ) + } + } + } + + return CodeBlockItemListSyntax(newItems) + } + + override func visit(_ node: MemberDeclListSyntax) -> MemberDeclListSyntax { + var newItems: [MemberDeclListItemSyntax] = [] + for item in node { + // Expand declaration macros, which produce zero or more declarations. + if let declExpansion = item.decl.as(MacroExpansionDeclSyntax.self), + let macro = macroSystem.macros[declExpansion.macro.text], + let freestandingMacro = macro as? DeclarationMacro.Type + { + do { + var expandedList = try freestandingMacro.expansion( + of: declExpansion, + in: context + ) + let attributes = freestandingMacro.propagateFreestandingMacroAttributes ? declExpansion.attributes : nil + let modifiers = freestandingMacro.propagateFreestandingMacroModifiers ? declExpansion.modifiers : nil + expandedList = expandedList.map { + $0.applying(attributes: attributes, modifiers: modifiers) + } + + newItems.append( + contentsOf: expandedList.map { decl in + return MemberDeclListItemSyntax(decl: decl) + } + ) + } catch { + context.addDiagnostics(from: error, node: declExpansion) + } + + continue + } + + // Expand member attribute members attached to the declaration context. + let attributedMember: MemberDeclListSyntax.Element + if let (macroAttributes, decl) = memberAttributeMacros.last { + attributedMember = expandAttributes( + for: macroAttributes, + attachedTo: decl, + annotating: item + ) + } else { + attributedMember = item + } + + // Recurse on the child node. + let newDecl = visit(attributedMember.decl) + newItems.append(attributedMember.with(\.decl, newDecl)) + + // Expand any peer declarations triggered by macros used as attributes. + let peers = expandPeers(of: item.decl) + newItems.append( + contentsOf: peers.map { + newDecl in MemberDeclListItemSyntax(decl: newDecl) + } + ) + } + + return .init(newItems) + } + + func visit( + declGroup: DeclType + ) -> DeclSyntax { + memberAttributeMacros.append( + ( + getMacroAttributes(attachedTo: DeclSyntax(declGroup), ofType: MemberAttributeMacro.Type.self), + DeclSyntax(declGroup) + ) + ) + defer { memberAttributeMacros.removeLast() } + + // Expand any attached member macros. + let expandedDeclGroup = expandMembers(of: declGroup) + + // Recurse into member decls. + let newMembers = visit(expandedDeclGroup.memberBlock) + + return DeclSyntax(expandedDeclGroup.with(\.memberBlock, newMembers)) + } + + override func visit(_ node: ActorDeclSyntax) -> DeclSyntax { + return visit(declGroup: node) + } + + override func visit(_ node: StructDeclSyntax) -> DeclSyntax { + return visit(declGroup: node) + } + + override func visit(_ node: EnumDeclSyntax) -> DeclSyntax { + return visit(declGroup: node) + } + + override func visit(_ node: ClassDeclSyntax) -> DeclSyntax { + return visit(declGroup: node) + } + + override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax { + return visit(declGroup: node) + } + + override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax { + return visit(declGroup: node) + } + + // Properties + override func visit(_ node: VariableDeclSyntax) -> DeclSyntax { + let visitedNode = super.visit(node) + guard let visitedVarDecl = visitedNode.as(VariableDeclSyntax.self) else { + return visitedNode + } + + guard let binding = visitedVarDecl.bindings.first, + visitedVarDecl.bindings.count == 1 + else { + return DeclSyntax(node) + } + + var accessors: [AccessorDeclSyntax] = [] + + let accessorMacroAttributes = getMacroAttributes(attachedTo: DeclSyntax(node), ofType: AccessorMacro.Type.self) + for (accessorAttr, accessorMacro) in accessorMacroAttributes { + do { + let newAccessors = try accessorMacro.expansion( + of: accessorAttr, + providingAccessorsOf: visitedNode, + in: context + ) + + accessors.append(contentsOf: newAccessors) + } catch { + // FIXME: record the error + } + } + + if accessors.isEmpty { + return visitedNode + } + + return DeclSyntax( + visitedVarDecl.with( + \.bindings, + visitedVarDecl.bindings.replacing( + childAt: 0, + with: binding.with( + \.accessor, + .accessors( + .init( + leftBrace: .leftBraceToken(leadingTrivia: .space), + accessors: .init(accessors), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + ) + ) + ) + ) + ) + } + + // Subscripts +} + +extension MacroApplication { + private func getMacroAttributes( + attachedTo decl: DeclSyntax, + ofType: MacroType.Type + ) -> [(AttributeSyntax, MacroType)] { + guard let attributedNode = decl.asProtocol(WithAttributesSyntax.self), + let attributes = attributedNode.attributes + else { + return [] + } + + return attributes.compactMap { + guard case let .attribute(attribute) = $0, + let attributeName = attribute.attributeName.as(SimpleTypeIdentifierSyntax.self)?.name.text, + let macro = macroSystem.macros[attributeName], + let macroType = macro as? MacroType + else { + return nil + } + + return (attribute, macroType) + } + } + + // If any of the custom attributes associated with the given declaration + // refer to "peer" declaration macros, expand them and return the resulting + // set of peer declarations. + private func expandPeers(of decl: DeclSyntax) -> [DeclSyntax] { + var peers: [DeclSyntax] = [] + let macroAttributes = getMacroAttributes(attachedTo: decl, ofType: PeerMacro.Type.self) + for (attribute, peerMacro) in macroAttributes { + do { + let newPeers = try peerMacro.expansion(of: attribute, providingPeersOf: decl, in: context) + peers.append(contentsOf: newPeers) + } catch { + context.addDiagnostics(from: error, node: attribute) + } + } + + return peers + } + + // If any of the custom attributes associated with the given declaration + // refer to conformance macros, expand them and return the resulting + // set of extension declarations. + private func expandConformances(of decl: DeclGroupSyntax) -> [DeclSyntax] { + let extendedType: Syntax + if let identified = decl.asProtocol(IdentifiedDeclSyntax.self) { + extendedType = Syntax(identified.identifier.trimmed) + } else if let ext = decl.as(ExtensionDeclSyntax.self) { + extendedType = Syntax(ext.extendedType.trimmed) + } else { + return [] + } + + var extensions: [DeclSyntax] = [] + let macroAttributes = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ConformanceMacro.Type.self) + for (attribute, conformanceMacro) in macroAttributes { + do { + let newConformances = try conformanceMacro.expansion(of: attribute, providingConformancesOf: decl, in: context) + + for (type, whereClause) in newConformances { + var ext: DeclSyntax = """ + extension \(extendedType): \(type) { } + """ + if let whereClause { + ext = DeclSyntax((ext.cast(ExtensionDeclSyntax.self)).with(\.genericWhereClause, whereClause)) + } + + extensions.append(DeclSyntax(ext)) + } + } catch { + context.addDiagnostics(from: error, node: attribute) + } + } + + let extensionMacroAttrs = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ExtensionMacro.Type.self) + let extendedTypeSyntax = TypeSyntax("\(extendedType.trimmed)") + for (attribute, extensionMacro) in extensionMacroAttrs { + do { + let newExtensions = try extensionMacro.expansion( + of: attribute, + attachedTo: decl, + providingExtensionsOf: extendedTypeSyntax, + in: context + ) + + extensions.append(contentsOf: newExtensions.map(DeclSyntax.init)) + } catch { + context.addDiagnostics(from: error, node: attribute) + } + } + + return extensions + } + + /// Expands any attached custom attributes that refer to member declaration macros, + /// and returns result of adding those members to the given declaration. + private func expandMembers( + of decl: Decl + ) -> Decl { + var newMembers: [DeclSyntax] = [] + let macroAttributes = getMacroAttributes(attachedTo: DeclSyntax(decl), ofType: MemberMacro.Type.self) + for (attribute, memberMacro) in macroAttributes { + do { + try newMembers.append( + contentsOf: memberMacro.expansion( + of: attribute, + providingMembersOf: decl, + in: context + ) + ) + } catch { + context.addDiagnostics(from: error, node: attribute) + } + } + + // FIXME: Is there a better way to add N members to a decl? + return decl.with( + \.memberBlock, + newMembers.reduce(decl.memberBlock) { partialMembers, newMember in + partialMembers.addMember(.init(decl: newMember)) + } + ) + } + + private func expandMemberAttribute( + attribute: AttributeSyntax, + macro: MemberAttributeMacro.Type, + decl: DeclGroupSyntax, + member: DeclSyntax, + in context: MacroExpansionContext + ) throws -> [AttributeSyntax] { + #if false + _openExistential(decl) { d in + return try! macro.expansion( + of: attribute, + attachedTo: d, + annotating: member, + in: context + ) + } + #else + return [] + #endif + } + + private func expandAttributes( + for macroAttributes: [(AttributeSyntax, MemberAttributeMacro.Type)], + attachedTo decl: DeclSyntax, + annotating member: MemberDeclListSyntax.Element + ) -> MemberDeclListSyntax.Element { + guard let attributedDecl = member.decl.asProtocol(WithAttributesSyntax.self) else { + return member + } + + var attributes: [AttributeSyntax] = [] + for (attribute, attributeMacro) in macroAttributes { + do { + let typedDecl = decl.asProtocol(DeclGroupSyntax.self)! + + func expand(_ decl: some DeclGroupSyntax) throws -> [AttributeSyntax] { + return try attributeMacro.expansion( + of: attribute, + attachedTo: decl, + providingAttributesFor: member.decl, + in: context + ) + } + + attributes.append( + contentsOf: try _openExistential(typedDecl, do: expand) + ) + } catch { + context.addDiagnostics(from: error, node: attribute) + } + } + + let newAttributes = attributes.reduce(attributedDecl.attributes ?? .init([])) { + $0.appending(AttributeListSyntax.Element($1)) + } + + let newDecl = attributedDecl.with(\.attributes, newAttributes).as(DeclSyntax.self)! + return member.with(\.decl, newDecl) + } +} + +extension DeclSyntax { + /// Returns this node with `attributes` and `modifiers` prepended to the + /// node’s attributes and modifiers, respectively. If the node doesn’t contain + /// attributes or modifiers, `attributes` or `modifiers` are ignored and not + /// applied. + func applying( + attributes: AttributeListSyntax?, + modifiers: ModifierListSyntax? + ) -> DeclSyntax { + func _combine(_ left: C, _ right: C?) -> C? { + guard let right = right else { return left } + var elems: [C.Element] = [] + elems.append(contentsOf: left) + elems.append(contentsOf: right) + return C(elems) + } + var node = self + if let attributes = attributes, + let withAttrs = node.asProtocol(WithAttributesSyntax.self) + { + node = withAttrs.with( + \.attributes, + _combine(attributes, withAttrs.attributes) + ).cast(DeclSyntax.self) + } + if let modifiers = modifiers, + let withModifiers = node.asProtocol(WithModifiersSyntax.self) + { + node = withModifiers.with( + \.modifiers, + _combine(modifiers, withModifiers.modifiers) + ).cast(DeclSyntax.self) + } + return node + } +} + +extension SyntaxProtocol { + /// Expand all uses of the given set of macros within this syntax + /// node. + public func expand( + macros: [String: Macro.Type], + in context: some MacroExpansionContext + ) -> Syntax { + // Build the macro system. + var system = MacroSystem() + for (macroName, macroType) in macros { + try! system.add(macroType, name: macroName) + } + + let applier = MacroApplication( + macroSystem: system, + context: context + ) + + return applier.rewrite(self) + } +} diff --git a/Sources/SwiftSyntaxMacroExpansion/Syntax+MacroEvaluation.swift b/Sources/SwiftSyntaxMacroExpansion/Syntax+MacroEvaluation.swift new file mode 100644 index 00000000000..a770b49b3d6 --- /dev/null +++ b/Sources/SwiftSyntaxMacroExpansion/Syntax+MacroEvaluation.swift @@ -0,0 +1,113 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 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 +// +//===----------------------------------------------------------------------===// + +import SwiftDiagnostics +import SwiftSyntax +import SwiftSyntaxMacros + +extension SyntaxProtocol { + /// Detach the current node and inform the macro expansion context, + /// if it needs to know. + fileprivate func detach(in context: MacroExpansionContext) -> Self { + if let basicContext = context as? BasicMacroExpansionContext { + return basicContext.detach(self) + } + + return self.detached + } +} + +extension MacroExpansionExprSyntax { + /// Evaluate the given macro for this syntax node, producing the expanded + /// result and (possibly) some diagnostics. + func evaluateMacro( + _ macro: Macro.Type, + in context: some MacroExpansionContext + ) -> ExprSyntax { + guard let exprMacro = macro as? ExpressionMacro.Type else { + return ExprSyntax(self) + } + + // Handle the rewrite. + do { + return try exprMacro.expansion(of: detach(in: context), in: context) + } catch { + context.addDiagnostics(from: error, node: self) + return ExprSyntax(self) + } + } +} + +extension MacroExpansionDeclSyntax { + /// Evaluate the given macro for this syntax node, producing the expanded + /// result and (possibly) some diagnostics. + func evaluateMacro( + _ macro: Macro.Type, + in context: some MacroExpansionContext + ) -> Syntax { + // TODO: declaration/statement macros + + return Syntax(self) + } +} + +extension Syntax { + /// Determine the name of the macro that is evaluated by this syntax node, + /// if indeed it is a macro evaluation. For example, "#stringify(x)" has the + /// name "stringify". + var evaluatedMacroName: String? { + switch self.as(SyntaxEnum.self) { + case .macroExpansionDecl(let expansion): + return expansion.macro.text + + case .macroExpansionExpr(let expansion): + return expansion.macro.text + + default: + return nil + } + } + + /// Evaluate the given macro and return the resulting syntax tree along with + /// any errors along the way. + /// + /// This operation only makes sense when `evaluatedMacroName` produces a + /// non-nil value, indicating that this syntax node is a macro evaluation of + /// some kind. + func evaluateMacro( + with macroSystem: MacroSystem, + context: some MacroExpansionContext + ) -> Syntax { + // If this isn't a macro evaluation node, do nothing. + guard let macroName = evaluatedMacroName else { + return self + } + + // Look for a macro with the given name. Otherwise, fail. + guard let macro = macroSystem.macros[macroName] else { + return self + } + + switch self.as(SyntaxEnum.self) { + case .macroExpansionDecl(let expansion): + return expansion.evaluateMacro(macro, in: context) + + case .macroExpansionExpr(let expansion): + return Syntax( + expansion.evaluateMacro(macro, in: context) + ) + + default: + fatalError("switch is out-of-sync with evaluatedMacroName") + } + } +} diff --git a/Sources/SwiftSyntaxMacros/BasicMacroExpansionContext.swift b/Sources/SwiftSyntaxMacros/BasicMacroExpansionContext.swift index 813486e7cdc..e76261b3e0a 100644 --- a/Sources/SwiftSyntaxMacros/BasicMacroExpansionContext.swift +++ b/Sources/SwiftSyntaxMacros/BasicMacroExpansionContext.swift @@ -15,8 +15,10 @@ import SwiftSyntax /// An implementation of the `MacroExpansionContext` protocol that is /// suitable for testing purposes. +@available(*, deprecated, message: "use SwiftSyntaxMacroExpansion.BasicMacroExpansionContext instead") public class BasicMacroExpansionContext { /// A single source file that is known to the macro expansion context. + @available(*, deprecated, message: "use SwiftSyntaxMacroExpansion.BasicMacroExpansionContext.KnownSourceFile instead") public struct KnownSourceFile { /// The name of the module in which this source file resides. let moduleName: String @@ -24,6 +26,8 @@ public class BasicMacroExpansionContext { /// The full path to the file. let fullFilePath: String + @available(*, deprecated, message: "use SwiftSyntaxMacroExpansion.BasicMacroExpansionContext.KnownSourceFile instead") + @_disfavoredOverload // deprecated. public init(moduleName: String, fullFilePath: String) { self.moduleName = moduleName self.fullFilePath = fullFilePath @@ -31,6 +35,8 @@ public class BasicMacroExpansionContext { } /// Create a new macro evaluation context. + @available(*, deprecated, message: "use SwiftSyntaxMacroExpansion.BasicMacroExpansionContext instead") + @_disfavoredOverload // deprecated. public init( expansionDiscriminator: String = "__macro_local_", sourceFiles: [SourceFileSyntax: KnownSourceFile] = [:] diff --git a/Sources/SwiftSyntaxMacros/MacroSystem.swift b/Sources/SwiftSyntaxMacros/MacroSystem.swift index 3f7ba87f8c7..9c2e96234d1 100644 --- a/Sources/SwiftSyntaxMacros/MacroSystem.swift +++ b/Sources/SwiftSyntaxMacros/MacroSystem.swift @@ -591,6 +591,8 @@ extension DeclSyntax { extension SyntaxProtocol { /// Expand all uses of the given set of macros within this syntax /// node. + @available(*, deprecated, message: "Use SwiftSyntaxMacroExpansion instead") + @_disfavoredOverload // deprecated. public func expand( macros: [String: Macro.Type], in context: some MacroExpansionContext diff --git a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift index 1d57d5fadbf..eaa095a7e5a 100644 --- a/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift +++ b/Sources/SwiftSyntaxMacrosTestSupport/Assertions.swift @@ -17,6 +17,7 @@ import SwiftParser import SwiftParserDiagnostics import SwiftSyntax import SwiftSyntaxMacros +import SwiftSyntaxMacroExpansion import XCTest // MARK: - Note