diff --git a/Package.swift b/Package.swift index 952a5c264ae..b90434cd9ac 100644 --- a/Package.swift +++ b/Package.swift @@ -57,6 +57,7 @@ let package = Package( .library(name: "SwiftSyntax", targets: ["SwiftSyntax"]), .library(name: "SwiftSyntaxBuilder", targets: ["SwiftSyntaxBuilder"]), .library(name: "SwiftSyntaxMacros", targets: ["SwiftSyntaxMacros"]), + .library(name: "SwiftSyntaxMacroExpansion", targets: ["SwiftSyntaxMacroExpansion"]), .library(name: "SwiftSyntaxMacrosTestSupport", targets: ["SwiftSyntaxMacrosTestSupport"]), ], targets: [ @@ -113,7 +114,7 @@ let package = Package( .target( name: "SwiftCompilerPluginMessageHandling", - dependencies: ["SwiftDiagnostics", "SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxMacros"], + dependencies: ["SwiftDiagnostics", "SwiftOperators", "SwiftParser", "SwiftSyntax", "SwiftSyntaxMacros", "SwiftSyntaxMacroExpansion"], exclude: ["CMakeLists.txt"] ), @@ -178,6 +179,12 @@ let package = Package( exclude: ["CMakeLists.txt"] ), + .target( + name: "SwiftSyntaxMacroExpansion", + dependencies: ["SwiftSyntax", "SwiftSyntaxMacros"], + exclude: ["CMakeLists.txt"] + ), + .testTarget( name: "SwiftSyntaxMacrosTest", dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftOperators", "SwiftParser", "SwiftSyntaxBuilder", "SwiftSyntaxMacros", "SwiftSyntaxMacrosTestSupport"] diff --git a/Sources/CMakeLists.txt b/Sources/CMakeLists.txt index bae4daa7df7..3b317b0891d 100644 --- a/Sources/CMakeLists.txt +++ b/Sources/CMakeLists.txt @@ -39,5 +39,6 @@ add_subdirectory(SwiftParserDiagnostics) add_subdirectory(SwiftOperators) add_subdirectory(SwiftSyntaxBuilder) add_subdirectory(SwiftSyntaxMacros) +add_subdirectory(SwiftSyntaxMacroExpansion) add_subdirectory(SwiftCompilerPluginMessageHandling) add_subdirectory(SwiftIDEUtils) diff --git a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt index 657126f5034..aa8bd6a31dc 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt +++ b/Sources/SwiftCompilerPluginMessageHandling/CMakeLists.txt @@ -20,4 +20,5 @@ target_link_libraries(SwiftCompilerPluginMessageHandling PUBLIC SwiftDiagnostics SwiftParser SwiftSyntaxMacros + SwiftSyntaxMacroExpansion SwiftOperators) diff --git a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift index ab0859b4fcd..c56e82c1833 100644 --- a/Sources/SwiftCompilerPluginMessageHandling/Macros.swift +++ b/Sources/SwiftCompilerPluginMessageHandling/Macros.swift @@ -14,6 +14,7 @@ import SwiftBasicFormat import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxMacros +import SwiftSyntaxMacroExpansion extension CompilerPluginMessageHandler { /// Get concrete macro type from a pair of module name and type name. @@ -35,7 +36,7 @@ extension CompilerPluginMessageHandler { expansionDiscriminator: discriminator ) - let expandedSource: String + let expandedSource: String? do { guard let macroSyntax = syntax.asProtocol(FreestandingMacroExpansionSyntax.self) else { throw MacroExpansionError.freestandingMacroSyntaxIsNotMacro @@ -44,34 +45,14 @@ extension CompilerPluginMessageHandler { throw MacroExpansionError.macroTypeNotFound } - switch macroDefinition { - case let exprMacroDef as ExpressionMacro.Type: - func _expand(node: some FreestandingMacroExpansionSyntax) throws -> ExprSyntax { - try exprMacroDef.expansion(of: node, in: context) - } - let rewritten = try _openExistential(macroSyntax, do: _expand) - expandedSource = rewritten.formattedExpansion(macroDefinition.formatMode) - - case let declMacroDef as DeclarationMacro.Type: - func _expand(node: some FreestandingMacroExpansionSyntax) throws -> [DeclSyntax] { - try declMacroDef.expansion(of: node, in: context) - } - let rewritten = try _openExistential(macroSyntax, do: _expand) - expandedSource = CodeBlockItemListSyntax(rewritten.map { CodeBlockItemSyntax(item: .decl($0)) }).formattedExpansion(macroDefinition.formatMode) - - case let codeItemMacroDef as CodeItemMacro.Type: - func _expand(node: some FreestandingMacroExpansionSyntax) throws -> [CodeBlockItemSyntax] { - try codeItemMacroDef.expansion(of: node, in: context) - } - let rewritten = try _openExistential(macroSyntax, do: _expand) - expandedSource = CodeBlockItemListSyntax(rewritten).formattedExpansion(macroDefinition.formatMode) - - default: - throw MacroExpansionError.unmathedMacroRole - } + expandedSource = SwiftSyntaxMacroExpansion.expandFreestandingMacro( + definition: macroDefinition, + node: macroSyntax, + in: context + ) } catch { context.addDiagnostics(from: error, node: syntax) - expandedSource = "" + expandedSource = nil } let diagnostics = context.diagnostics.map { @@ -99,132 +80,25 @@ extension CompilerPluginMessageHandler { let attributeNode = sourceManager.add(attributeSyntax).cast(AttributeSyntax.self) let declarationNode = sourceManager.add(declSyntax).cast(DeclSyntax.self) + let parentDeclNode = parentDeclSyntax.map { sourceManager.add($0).cast(DeclSyntax.self) } - let expandedSources: [String] + let expandedSources: [String]? do { guard let macroDefinition = resolveMacro(macro) else { throw MacroExpansionError.macroTypeNotFound } - switch (macroDefinition, macroRole) { - case (let attachedMacro as AccessorMacro.Type, .accessor): - let accessors = try attachedMacro.expansion( - of: attributeNode, - providingAccessorsOf: declarationNode, - in: context - ) - expandedSources = accessors.map { - $0.formattedExpansion(macroDefinition.formatMode) - } - - case (let attachedMacro as MemberAttributeMacro.Type, .memberAttribute): - guard - let parentDeclSyntax = parentDeclSyntax, - let parentDeclGroup = sourceManager.add(parentDeclSyntax).asProtocol(DeclGroupSyntax.self) - else { - // Compiler error: 'parentDecl' is mandatory for MemberAttributeMacro. - throw MacroExpansionError.invalidExpansionMessage - } - - // Local function to expand a member atribute macro once we've opened up - // the existential. - func expandMemberAttributeMacro( - _ node: some DeclGroupSyntax - ) throws -> [AttributeSyntax] { - return try attachedMacro.expansion( - of: attributeNode, - attachedTo: node, - providingAttributesFor: declarationNode, - in: context - ) - } - - let attributes = try _openExistential( - parentDeclGroup, - do: expandMemberAttributeMacro - ) - - // Form a buffer containing an attribute list to return to the caller. - expandedSources = attributes.map { - $0.formattedExpansion(macroDefinition.formatMode) - } - - case (let attachedMacro as MemberMacro.Type, .member): - guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) - else { - // Compiler error: declNode for member macro must be DeclGroupSyntax. - throw MacroExpansionError.invalidExpansionMessage - } - - // Local function to expand a member macro once we've opened up - // the existential. - func expandMemberMacro( - _ node: some DeclGroupSyntax - ) throws -> [DeclSyntax] { - return try attachedMacro.expansion( - of: attributeNode, - providingMembersOf: node, - in: context - ) - } - - let members = try _openExistential(declGroup, do: expandMemberMacro) - - // Form a buffer of member declarations to return to the caller. - expandedSources = members.map { $0.formattedExpansion(macroDefinition.formatMode) } - - case (let attachedMacro as PeerMacro.Type, .peer): - let peers = try attachedMacro.expansion( - of: attributeNode, - providingPeersOf: declarationNode, - in: context - ) - - // Form a buffer of peer declarations to return to the caller. - expandedSources = peers.map { - $0.formattedExpansion(macroDefinition.formatMode) - } - - case (let attachedMacro as ConformanceMacro.Type, .conformance): - guard - let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self), - let identified = declarationNode.asProtocol(IdentifiedDeclSyntax.self) - else { - // Compiler error: type mismatch. - throw MacroExpansionError.invalidExpansionMessage - } - - // Local function to expand a conformance macro once we've opened up - // the existential. - func expandConformanceMacro( - _ node: some DeclGroupSyntax - ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { - return try attachedMacro.expansion( - of: attributeNode, - providingConformancesOf: node, - in: context - ) - } - - let conformances = try _openExistential( - declGroup, - do: expandConformanceMacro - ) - - // Form a buffer of extension declarations to return to the caller. - expandedSources = conformances.map { typeSyntax, whereClause in - let typeName = identified.identifier.trimmedDescription - let protocolName = typeSyntax.trimmedDescription - let whereClause = whereClause?.trimmedDescription ?? "" - return "extension \(typeName) : \(protocolName) \(whereClause) {}" - } - - default: - throw MacroExpansionError.unmathedMacroRole - } + expandedSources = SwiftSyntaxMacroExpansion.expandAttachedMacro( + definition: macroDefinition, + macroRole: MacroRole(messageMacroRole: macroRole), + attributeNode: attributeNode, + declarationNode: declarationNode, + parentDeclNode: parentDeclNode, + in: context + ) } catch { context.addDiagnostics(from: error, node: attributeNode) - expandedSources = [] + expandedSources = nil } let diagnostics = context.diagnostics.map { @@ -236,17 +110,17 @@ extension CompilerPluginMessageHandler { } } -fileprivate extension SyntaxProtocol { - /// Perform a format if required and then trim any leading/trailing - /// whitespace. - func formattedExpansion(_ mode: FormatMode) -> String { - let formatted: Syntax - switch mode { - case .auto: - formatted = self.formatted() - case .disabled: - formatted = Syntax(self) +private extension MacroRole { + init(messageMacroRole: PluginMessage.MacroRole) { + switch messageMacroRole { + case .expression: self = .expression + case .declaration: self = .declaration + case .accessor: self = .accessor + case .memberAttribute: self = .memberAttribute + case .member: self = .member + case .peer: self = .peer + case .conformance: self = .conformance + case .codeItem: self = .codeItem } - return formatted.trimmedDescription(matching: { $0.isWhitespace }) } } diff --git a/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt b/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt new file mode 100644 index 00000000000..2a043c00719 --- /dev/null +++ b/Sources/SwiftSyntaxMacroExpansion/CMakeLists.txt @@ -0,0 +1,8 @@ +add_swift_host_library(SwiftSyntaxMacroExpansion + MacroExpansion.swift +) + +target_link_libraries(SwiftSyntaxMacroExpansion PUBLIC + SwiftSyntax + SwiftSyntaxMacros +) diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift b/Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift new file mode 100644 index 00000000000..282dbb1801c --- /dev/null +++ b/Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift @@ -0,0 +1,203 @@ +import SwiftSyntax +import SwiftSyntaxMacros + +public enum MacroRole { + case expression + case declaration + case accessor + case memberAttribute + case member + case peer + case conformance + case codeItem +} + +/// Simple diagnostic message +private struct MacroExpansionError: Error, CustomStringConvertible { + let description: String +} + +/// Expand `@freestanding(XXX)` macros. +public func expandFreestandingMacro( + definition: Macro.Type, + node: FreestandingMacroExpansionSyntax, + in context: some MacroExpansionContext +) -> String? { + do { + func _expand(node: some FreestandingMacroExpansionSyntax) throws -> String { + let expandedSyntax: Syntax + switch definition { + case let exprMacroDef as ExpressionMacro.Type: + expandedSyntax = try Syntax(exprMacroDef.expansion(of: node, in: context)) + + case let declMacroDef as DeclarationMacro.Type: + let rewritten = try declMacroDef.expansion(of: node, in: context) + expandedSyntax = Syntax( + CodeBlockItemListSyntax( + rewritten.map { + CodeBlockItemSyntax(item: .decl($0)) + } + ) + ) + + case let codeItemMacroDef as CodeItemMacro.Type: + let rewritten = try codeItemMacroDef.expansion(of: node, in: context) + expandedSyntax = Syntax(CodeBlockItemListSyntax(rewritten)) + + default: + throw MacroExpansionError(description: "macro doesn't conform to required macro role") + } + return expandedSyntax.formattedExpansion(definition.formatMode) + } + return try _openExistential(node, do: _expand) + } catch { + context.addDiagnostics(from: error, node: node) + return nil + } +} + +/// Expand `@attached(XXX)` macros. +public func expandAttachedMacro( + definition: Macro.Type, + macroRole: MacroRole, + attributeNode: AttributeSyntax, + declarationNode: DeclSyntax, + parentDeclNode: DeclSyntax?, + in context: Context +) -> [String]? { + do { + switch (definition, macroRole) { + case (let attachedMacro as AccessorMacro.Type, .accessor): + let accessors = try attachedMacro.expansion( + of: attributeNode, + providingAccessorsOf: declarationNode, + in: context + ) + return accessors.map { + $0.formattedExpansion(definition.formatMode) + } + + case (let attachedMacro as MemberAttributeMacro.Type, .memberAttribute): + guard + let parentDeclGroup = parentDeclNode?.asProtocol(DeclGroupSyntax.self) + else { + // Compiler error: 'parentDecl' is mandatory for MemberAttributeMacro. + throw MacroExpansionError(description: "parent decl group is nil") + } + + // Local function to expand a member attribute macro once we've opened up + // the existential. + func expandMemberAttributeMacro( + _ node: some DeclGroupSyntax + ) throws -> [AttributeSyntax] { + return try attachedMacro.expansion( + of: attributeNode, + attachedTo: node, + providingAttributesFor: declarationNode, + in: context + ) + } + + let attributes = try _openExistential( + parentDeclGroup, + do: expandMemberAttributeMacro + ) + + // Form a buffer containing an attribute list to return to the caller. + return attributes.map { + $0.formattedExpansion(definition.formatMode) + } + + case (let attachedMacro as MemberMacro.Type, .member): + guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) + else { + // Compiler error: declNode for member macro must be DeclGroupSyntax. + throw MacroExpansionError(description: "declaration is not a decl group syntax") + } + + // Local function to expand a member macro once we've opened up + // the existential. + func expandMemberMacro( + _ node: some DeclGroupSyntax + ) throws -> [DeclSyntax] { + return try attachedMacro.expansion( + of: attributeNode, + providingMembersOf: node, + in: context + ) + } + + let members = try _openExistential(declGroup, do: expandMemberMacro) + + // Form a buffer of member declarations to return to the caller. + return members.map { $0.formattedExpansion(definition.formatMode) } + + case (let attachedMacro as PeerMacro.Type, .peer): + let peers = try attachedMacro.expansion( + of: attributeNode, + providingPeersOf: declarationNode, + in: context + ) + + // Form a buffer of peer declarations to return to the caller. + return peers.map { + $0.formattedExpansion(definition.formatMode) + } + + case (let attachedMacro as ConformanceMacro.Type, .conformance): + guard + let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self), + let identified = declarationNode.asProtocol(IdentifiedDeclSyntax.self) + else { + // Compiler error: type mismatch. + throw MacroExpansionError(description: "declaration is not a identified decl group") + } + + // Local function to expand a conformance macro once we've opened up + // the existential. + func expandConformanceMacro( + _ node: some DeclGroupSyntax + ) throws -> [(TypeSyntax, GenericWhereClauseSyntax?)] { + return try attachedMacro.expansion( + of: attributeNode, + providingConformancesOf: node, + in: context + ) + } + + let conformances = try _openExistential( + declGroup, + do: expandConformanceMacro + ) + + // Form a buffer of extension declarations to return to the caller. + return conformances.map { typeSyntax, whereClause in + let typeName = identified.identifier.trimmedDescription + let protocolName = typeSyntax.trimmedDescription + let whereClause = whereClause?.trimmedDescription ?? "" + return "extension \(typeName) : \(protocolName) \(whereClause) {}" + } + + default: + throw MacroExpansionError(description: "macro doesn't conform to required macro role") + } + } catch { + context.addDiagnostics(from: error, node: attributeNode) + return nil + } +} + +fileprivate extension SyntaxProtocol { + /// Perform a format if required and then trim any leading/trailing + /// whitespace. + func formattedExpansion(_ mode: FormatMode) -> String { + let formatted: Syntax + switch mode { + case .auto: + formatted = self.formatted() + case .disabled: + formatted = Syntax(self) + } + return formatted.trimmedDescription(matching: { $0.isWhitespace }) + } +}