diff --git a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift index 8d7012da811..41f630008ec 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift @@ -125,7 +125,7 @@ extension PluginProviderMessageHandler { attributeSyntax, foldingWith: .standardOperators ).cast(AttributeSyntax.self) - let declarationNode = sourceManager.add(declSyntax).cast(DeclSyntax.self) + let declarationNode = sourceManager.add(declSyntax) let parentDeclNode = parentDeclSyntax.map { sourceManager.add($0).cast(DeclSyntax.self) } let extendedType = extendedTypeSyntax.map { sourceManager.add($0).cast(TypeSyntax.self) @@ -156,7 +156,7 @@ extension PluginProviderMessageHandler { definition: macroDefinition, macroRole: role, attributeNode: attributeNode, - declarationNode: declarationNode, + node: declarationNode, parentDeclNode: parentDeclNode, extendedType: extendedType, conformanceList: conformanceList, diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift b/Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift index 4e35c7f240f..ffd31a16ef4 100644 --- a/Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift +++ b/Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift @@ -211,6 +211,7 @@ public func expandFreestandingMacro( /// - Returns: A list of expanded source text. Upon failure (i.e. /// `definition.expansion()` throws) returns `nil`, and the diagnostics /// representing the `Error` are guaranteed to be added to context. +@available(*, deprecated, message: "Change the 'declarationNode' argument label to 'node'") public func expandAttachedMacroWithoutCollapsing( definition: Macro.Type, macroRole: MacroRole, @@ -221,10 +222,51 @@ public func expandAttachedMacroWithoutCollapsing conformanceList: InheritedTypeListSyntax?, in context: Context, indentationWidth: Trivia? = nil +) -> [String]? { + expandAttachedMacroWithoutCollapsing( + definition: definition, + macroRole: macroRole, + attributeNode: attributeNode, + node: declarationNode, + parentDeclNode: parentDeclNode, + extendedType: extendedType, + conformanceList: conformanceList, + in: context, + indentationWidth: indentationWidth + ) +} + +/// Expand `@attached(XXX)` macros. +/// +/// - Parameters: +/// - definition: a type that conforms to one or more attached `Macro` protocols. +/// - macroRole: indicates which `Macro` protocol expansion should be performed +/// - attributeNode: attribute syntax node (e.g. `@macroName(argument)`). +/// - node: target syntax node to apply the expansion. This is either a declaration +/// or a closure syntax node. +/// - parentDeclNode: Only used for `MacroRole.memberAttribute`. The parent +/// context node of `declarationNode`. +/// - context: context of the expansion. +/// - indentationWidth: The indentation that should be added for each additional +/// nesting level +/// - Returns: A list of expanded source text. Upon failure (i.e. +/// `definition.expansion()` throws) returns `nil`, and the diagnostics +/// representing the `Error` are guaranteed to be added to context. +public func expandAttachedMacroWithoutCollapsing( + definition: Macro.Type, + macroRole: MacroRole, + attributeNode: AttributeSyntax, + node: some SyntaxProtocol, + parentDeclNode: DeclSyntax?, + extendedType: TypeSyntax?, + conformanceList: InheritedTypeListSyntax?, + in context: Context, + indentationWidth: Trivia? = nil ) -> [String]? { do { switch (definition, macroRole) { case (let attachedMacro as AccessorMacro.Type, .accessor): + let declarationNode = node.cast(DeclSyntax.self) let accessors = try attachedMacro.expansion( of: attributeNode, providingAccessorsOf: declarationNode, @@ -235,6 +277,7 @@ public func expandAttachedMacroWithoutCollapsing } case (let attachedMacro as MemberAttributeMacro.Type, .memberAttribute): + let declarationNode = node.cast(DeclSyntax.self) guard let parentDeclGroup = parentDeclNode?.asProtocol(DeclGroupSyntax.self) else { @@ -255,7 +298,7 @@ public func expandAttachedMacroWithoutCollapsing } case (let attachedMacro as MemberMacro.Type, .member): - guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) + guard let declGroup = node.asProtocol(DeclGroupSyntax.self) else { // Compiler error: declNode for member macro must be DeclGroupSyntax. throw MacroExpansionError.declarationNotDeclGroup @@ -274,6 +317,7 @@ public func expandAttachedMacroWithoutCollapsing } case (let attachedMacro as PeerMacro.Type, .peer): + let declarationNode = node.cast(DeclSyntax.self) let peers = try attachedMacro.expansion( of: attributeNode, providingPeersOf: declarationNode, @@ -286,7 +330,7 @@ public func expandAttachedMacroWithoutCollapsing } case (let attachedMacro as ExtensionMacro.Type, .extension): - guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) else { + guard let declGroup = node.asProtocol(DeclGroupSyntax.self) else { // Compiler error: type mismatch. throw MacroExpansionError.declarationNotDeclGroup } @@ -294,7 +338,7 @@ public func expandAttachedMacroWithoutCollapsing let extensionOf: TypeSyntax if let extendedType { extensionOf = extendedType - } else if let identified = declarationNode.asProtocol(NamedDeclSyntax.self) { + } else if let identified = node.asProtocol(NamedDeclSyntax.self) { // Fallback for old compilers with a new plugin, where extensionOf = TypeSyntax(IdentifierTypeSyntax(name: identified.name)) } else { @@ -318,7 +362,7 @@ public func expandAttachedMacroWithoutCollapsing case (let attachedMacro as PreambleMacro.Type, .preamble): guard - let declToPass = Syntax(declarationNode).asProtocol(SyntaxProtocol.self) + let declToPass = Syntax(node).asProtocol(SyntaxProtocol.self) as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax) else { // Compiler error: declaration must have a body. @@ -335,19 +379,26 @@ public func expandAttachedMacroWithoutCollapsing } case (let attachedMacro as BodyMacro.Type, .body): - guard - let declToPass = Syntax(declarationNode).asProtocol(SyntaxProtocol.self) - as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax) - else { + let body: [CodeBlockItemSyntax] + if let closureSyntax = node.as(ClosureExprSyntax.self) { + body = try attachedMacro.expansion( + of: attributeNode, + providingBodyFor: closureSyntax, + in: context + ) + } else if let declToPass = Syntax(node).asProtocol(SyntaxProtocol.self) + as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax) + { + body = try attachedMacro.expansion( + of: attributeNode, + providingBodyFor: declToPass, + in: context + ) + } else { // Compiler error: declaration must have a body. throw MacroExpansionError.declarationHasNoBody } - let body = try attachedMacro.expansion( - of: attributeNode, - providingBodyFor: declToPass, - in: context - ) return body.map { $0.formattedExpansion(definition.formatMode, indentationWidth: indentationWidth) } @@ -376,6 +427,7 @@ public func expandAttachedMacroWithoutCollapsing /// - Returns: expanded source text. Upon failure (i.e. `defintion.expansion()` /// throws) returns `nil`, and the diagnostics representing the `Error` are /// guaranteed to be added to context. +@available(*, deprecated, message: "Change the 'declarationNode' argument label to 'node'") public func expandAttachedMacro( definition: Macro.Type, macroRole: MacroRole, @@ -386,12 +438,52 @@ public func expandAttachedMacro( conformanceList: InheritedTypeListSyntax?, in context: Context, indentationWidth: Trivia? = nil +) -> String? { + expandAttachedMacro( + definition: definition, + macroRole: macroRole, + attributeNode: attributeNode, + node: declarationNode, + parentDeclNode: parentDeclNode, + extendedType: extendedType, + conformanceList: conformanceList, + in: context, + indentationWidth: indentationWidth + ) +} + +/// Expand `@attached(XXX)` macros. +/// +/// - Parameters: +/// - definition: a type that conforms to one or more attached `Macro` protocols. +/// - macroRole: indicates which `Macro` protocol expansion should be performed +/// - attributeNode: attribute syntax node (e.g. `@macroName(argument)`). +/// - node: target declaration syntax node to apply the expansion. This is either +/// a declaration or a closure syntax node. +/// - parentDeclNode: Only used for `MacroRole.memberAttribute`. The parent +/// context node of `declarationNode`. +/// - context: context of the expansion. +/// - indentationWidth: The indentation that should be added for each additional +/// nesting level +/// - Returns: expanded source text. Upon failure (i.e. `defintion.expansion()` +/// throws) returns `nil`, and the diagnostics representing the `Error` are +/// guaranteed to be added to context. +public func expandAttachedMacro( + definition: Macro.Type, + macroRole: MacroRole, + attributeNode: AttributeSyntax, + node: some SyntaxProtocol, + parentDeclNode: DeclSyntax?, + extendedType: TypeSyntax?, + conformanceList: InheritedTypeListSyntax?, + in context: Context, + indentationWidth: Trivia? = nil ) -> String? { let expandedSources = expandAttachedMacroWithoutCollapsing( definition: definition, macroRole: macroRole, attributeNode: attributeNode, - declarationNode: declarationNode, + node: node, parentDeclNode: parentDeclNode, extendedType: extendedType, conformanceList: conformanceList, @@ -411,7 +503,7 @@ public func expandAttachedMacro( return collapse( expansions: expandedSources, for: macroRole, - attachedTo: declarationNode, + attachedTo: node, indentationWidth: collapseIndentationWidth ) } diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift index b77cf33ec02..de6adebf527 100644 --- a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift +++ b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift @@ -194,7 +194,7 @@ private func expandMemberMacro( definition: definition, macroRole: .member, attributeNode: attributeNode.detach(in: context, foldingWith: .standardOperators), - declarationNode: attachedTo.detach(in: context), + node: attachedTo.detach(in: context), parentDeclNode: nil, extendedType: nil, conformanceList: conformanceList, @@ -223,7 +223,7 @@ private func expandMemberAttributeMacro( definition: definition, macroRole: .memberAttribute, attributeNode: attributeNode.detach(in: context, foldingWith: .standardOperators), - declarationNode: member.detach(in: context), + node: member.detach(in: context), parentDeclNode: declaration.detach(in: context), extendedType: nil, conformanceList: nil, @@ -255,7 +255,7 @@ private func expandPeerMacroMember( definition: definition, macroRole: .peer, attributeNode: attributeNode.detach(in: context, foldingWith: .standardOperators), - declarationNode: attachedTo.detach(in: context), + node: attachedTo.detach(in: context), parentDeclNode: nil, extendedType: nil, conformanceList: nil, @@ -287,7 +287,7 @@ private func expandPeerMacroCodeItem( definition: definition, macroRole: .peer, attributeNode: attributeNode.detach(in: context, foldingWith: .standardOperators), - declarationNode: attachedTo.detach(in: context), + node: attachedTo.detach(in: context), parentDeclNode: nil, extendedType: nil, conformanceList: nil, @@ -319,7 +319,7 @@ private func expandAccessorMacroWithoutExistingAccessors( definition: definition, macroRole: .accessor, attributeNode: attributeNode.detach(in: context, foldingWith: .standardOperators), - declarationNode: attachedTo.detach(in: context), + node: attachedTo.detach(in: context), parentDeclNode: nil, extendedType: nil, conformanceList: nil, @@ -354,7 +354,7 @@ private func expandAccessorMacroWithExistingAccessors( definition: definition, macroRole: .accessor, attributeNode: attributeNode.detach(in: context, foldingWith: .standardOperators), - declarationNode: attachedTo.detach(in: context), + node: attachedTo.detach(in: context), parentDeclNode: nil, extendedType: nil, conformanceList: nil, @@ -391,7 +391,7 @@ private func expandExtensionMacro( definition: definition, macroRole: .extension, attributeNode: attributeNode.detach(in: context, foldingWith: .standardOperators), - declarationNode: attachedTo.detach(in: context), + node: attachedTo.detach(in: context), parentDeclNode: nil, extendedType: extendedType.detach(in: context), conformanceList: conformanceList, @@ -423,7 +423,7 @@ private func expandPreambleMacro( in: context, foldingWith: .standardOperators ), - declarationNode: DeclSyntax(decl.detach(in: context)), + node: DeclSyntax(decl.detach(in: context)), parentDeclNode: nil, extendedType: nil, conformanceList: nil, @@ -444,7 +444,7 @@ private func expandPreambleMacro( private func expandBodyMacro( definition: BodyMacro.Type, attributeNode: AttributeSyntax, - attachedTo decl: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, + attachedTo node: some SyntaxProtocol, in context: some MacroExpansionContext, indentationWidth: Trivia ) -> CodeBlockSyntax? { @@ -456,7 +456,7 @@ private func expandBodyMacro( in: context, foldingWith: .standardOperators ), - declarationNode: DeclSyntax(decl.detach(in: context)), + node: Syntax(node.detach(in: context)), parentDeclNode: nil, extendedType: nil, conformanceList: nil, @@ -473,9 +473,16 @@ private func expandBodyMacro( // Remove any indentation from the first line using `drop(while:)` and then // prepend a space when it's being introduced on a declaration that has no // body yet. - let leadingWhitespace = decl.body == nil ? " " : "" + let leadingWhitespace: String + if let decl = node as? (DeclSyntaxProtocol & WithOptionalCodeBlockSyntax), + decl.body == nil + { + leadingWhitespace = " " + } else { + leadingWhitespace = "" + } let indentedSource = - leadingWhitespace + expanded.indented(by: decl.indentationOfFirstLine).drop(while: { $0.isWhitespace }) + leadingWhitespace + expanded.indented(by: node.indentationOfFirstLine).drop(while: { $0.isWhitespace }) return "\(raw: indentedSource)" } @@ -733,6 +740,19 @@ private class MacroApplication: SyntaxRewriter { return AttributeRemover(removingWhere: { attributesToRemove.contains($0) }).rewrite(visitedNode) } + if var closureSyntax = node.as(ClosureExprSyntax.self) { + closureSyntax = visitClosureBodyMacros(closureSyntax) + + // Visit the node, disabling the `visitAny` handling. + skipVisitAnyHandling.insert(Syntax(closureSyntax)) + let visitedNode = self.visit(closureSyntax).cast(ClosureExprSyntax.self) + skipVisitAnyHandling.remove(Syntax(closureSyntax)) + + let attributesToRemove = self.macroAttributes(attachedTo: visitedNode).map(\.attributeNode) + + return AttributeRemover(removingWhere: { attributesToRemove.contains($0) }).rewrite(visitedNode) + } + return nil } @@ -802,6 +822,41 @@ private class MacroApplication: SyntaxRewriter { return node.with(\.body, body.with(\.statements, preamble + body.statements)) } + func visitClosureBodyMacros( + _ node: ClosureExprSyntax + ) -> ClosureExprSyntax { + // Expand body macro. + let expandedBodies = expandMacros( + attachedTo: node, + ofType: BodyMacro.Type.self + ) { attributeNode, definition, _ in + expandBodyMacro( + definition: definition, + attributeNode: attributeNode, + attachedTo: node, + in: contextGenerator(Syntax(node)), + indentationWidth: indentationWidth + ).map { [$0] } + } + + // Dig out the body. + let body: CodeBlockSyntax + switch expandedBodies.count { + case 0: + // Nothing changes + return node + + case 1: + body = expandedBodies[0] + + default: + contextGenerator(Syntax(node)).addDiagnostics(from: MacroExpansionError.moreThanOneBodyMacro, node: node) + body = expandedBodies[0] + } + + return node.with(\.statements, body.statements) + } + override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax { var newItems: [CodeBlockItemSyntax] = [] func addResult(_ node: CodeBlockItemSyntax) { @@ -1006,9 +1061,16 @@ extension MacroApplication { /// /// The macros must be registered in `macroSystem`. private func macroAttributes( - attachedTo decl: DeclSyntax + attachedTo decl: some SyntaxProtocol ) -> [(attributeNode: AttributeSyntax, spec: MacroSpec)] { - guard let attributedNode = decl.asProtocol(WithAttributesSyntax.self) else { + let attributedNode: (any WithAttributesSyntax)? + if let closure = decl.as(ClosureExprSyntax.self) { + attributedNode = closure.signature?.asProtocol(WithAttributesSyntax.self) + } else { + attributedNode = decl.asProtocol(WithAttributesSyntax.self) + } + + guard let attributedNode else { return [] } @@ -1029,7 +1091,7 @@ extension MacroApplication { /// /// The macros must be registered in `macroSystem`. private func macroAttributes( - attachedTo decl: DeclSyntax, + attachedTo decl: some SyntaxProtocol, ofType: MacroType.Type ) -> [(attributeNode: AttributeSyntax, definition: MacroType, conformanceList: InheritedTypeListSyntax)] { return macroAttributes(attachedTo: decl) @@ -1049,7 +1111,7 @@ extension MacroApplication { ExpandedNodeCollection: Sequence, MacroType >( - attachedTo decl: DeclSyntax, + attachedTo decl: some SyntaxProtocol, ofType: MacroType.Type, expandMacro: ( diff --git a/Sources/SwiftSyntaxMacros/MacroProtocols/BodyMacro.swift b/Sources/SwiftSyntaxMacros/MacroProtocols/BodyMacro.swift index 347d8be1ca4..f741de36bc4 100644 --- a/Sources/SwiftSyntaxMacros/MacroProtocols/BodyMacro.swift +++ b/Sources/SwiftSyntaxMacros/MacroProtocols/BodyMacro.swift @@ -29,4 +29,31 @@ public protocol BodyMacro: AttachedMacro { providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, in context: some MacroExpansionContext ) throws -> [CodeBlockItemSyntax] + + /// Expand a macro described by the given custom attribute and + /// attached to the given closure and evaluated within a + /// particular expansion context. + /// + /// The macro expansion can replace the body of the given closure. + static func expansion( + of node: AttributeSyntax, + providingBodyFor closure: ClosureExprSyntax, + in context: some MacroExpansionContext + ) throws -> [CodeBlockItemSyntax] +} + +private struct ClosureNotSupported: Error, CustomStringConvertible { + var description: String { + "Function body macro cannot be applied to closure" + } +} + +extension BodyMacro { + public static func expansion( + of node: AttributeSyntax, + providingBodyFor closure: ClosureExprSyntax, + in context: some MacroExpansionContext + ) throws -> [CodeBlockItemSyntax] { + throw ClosureNotSupported() + } } diff --git a/Tests/SwiftSyntaxMacroExpansionTest/BodyMacroTests.swift b/Tests/SwiftSyntaxMacroExpansionTest/BodyMacroTests.swift index 3190c51bc62..0bd5374d4b9 100644 --- a/Tests/SwiftSyntaxMacroExpansionTest/BodyMacroTests.swift +++ b/Tests/SwiftSyntaxMacroExpansionTest/BodyMacroTests.swift @@ -63,6 +63,37 @@ struct RemoteBodyMacro: BodyMacro { } } +struct StartTaskMacro: BodyMacro { + static func expansion( + of node: AttributeSyntax, + providingBodyFor declaration: some DeclSyntaxProtocol & WithOptionalCodeBlockSyntax, + in context: some MacroExpansionContext + ) throws -> [CodeBlockItemSyntax] { + guard let taskBody = declaration.body else { + return [] + } + return [ + """ + Task \(taskBody.trimmed) + """ + ] + } + + static func expansion( + of node: AttributeSyntax, + providingBodyFor closure: ClosureExprSyntax, + in context: some MacroExpansionContext + ) throws -> [CodeBlockItemSyntax] { + return [ + """ + Task { + \(closure.statements.trimmed) + } + """ + ] + } +} + final class BodyMacroTests: XCTestCase { private let indentationWidth: Trivia = .spaces(2) @@ -199,4 +230,23 @@ final class BodyMacroTests: XCTestCase { macros: ["SourceLocationMacro": SourceLocationMacro.self] ) } + + func testClosureBodyExpansion() { + assertMacroExpansion( + """ + { @StartTask in + a + b + } + """, + expandedSource: """ + { in + Task { + a + b + } + } + """, + macros: ["StartTask": StartTaskMacro.self], + indentationWidth: indentationWidth + ) + } }