Skip to content
1 change: 1 addition & 0 deletions Examples/Sources/ExamplePlugin/ExamplePlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ struct ThePlugin: CompilerPlugin {
PeerValueWithSuffixNameMacro.self,
MemberDeprecatedMacro.self,
EquatableConformanceMacro.self,
SendableExtensionMacro.self,
DidSetPrintMacro.self,
PrintAnyMacro.self,
]
Expand Down
20 changes: 20 additions & 0 deletions Examples/Sources/ExamplePlugin/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,26 @@ struct EquatableConformanceMacro: ConformanceMacro {
}
}

public struct SendableExtensionMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let sendableExtension: DeclSyntax =
"""
extension \(type.trimmed): Sendable {}
"""

guard let extensionDecl = sendableExtension.as(ExtensionDeclSyntax.self) else {
return []
}

return [extensionDecl]
}
}

/// Add 'didSet' printing the new value.
struct DidSetPrintMacro: AccessorMacro {
static func expansion(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,15 @@ extension CompilerPluginMessageHandler {
expandingSyntax: expandingSyntax
)

case .expandAttachedMacro(let macro, let macroRole, let discriminator, let attributeSyntax, let declSyntax, let parentDeclSyntax):
case .expandAttachedMacro(let macro, let macroRole, let discriminator, let attributeSyntax, let declSyntax, let parentDeclSyntax, let extendedTypeSyntax):
try expandAttachedMacro(
macro: macro,
macroRole: macroRole,
discriminator: discriminator,
attributeSyntax: attributeSyntax,
declSyntax: declSyntax,
parentDeclSyntax: parentDeclSyntax
parentDeclSyntax: parentDeclSyntax,
extendedTypeSyntax: extendedTypeSyntax
)

case .loadPluginLibrary(let libraryPath, let moduleName):
Expand Down
8 changes: 7 additions & 1 deletion Sources/SwiftCompilerPluginMessageHandling/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ extension CompilerPluginMessageHandler {
discriminator: String,
attributeSyntax: PluginMessage.Syntax,
declSyntax: PluginMessage.Syntax,
parentDeclSyntax: PluginMessage.Syntax?
parentDeclSyntax: PluginMessage.Syntax?,
extendedTypeSyntax: PluginMessage.Syntax?
) throws {
let sourceManager = SourceManager()
let context = PluginMacroExpansionContext(
Expand All @@ -100,6 +101,9 @@ extension CompilerPluginMessageHandler {
).cast(AttributeSyntax.self)
let declarationNode = sourceManager.add(declSyntax).cast(DeclSyntax.self)
let parentDeclNode = parentDeclSyntax.map { sourceManager.add($0).cast(DeclSyntax.self) }
let extendedType = extendedTypeSyntax.map {
sourceManager.add($0).cast(TypeSyntax.self)
}

// TODO: Make this a 'String?' and remove non-'hasExpandMacroResult' branches.
let expandedSources: [String]?
Expand All @@ -115,6 +119,7 @@ extension CompilerPluginMessageHandler {
attributeNode: attributeNode,
declarationNode: declarationNode,
parentDeclNode: parentDeclNode,
extendedType: extendedType,
in: context
)
if let expansions, hostCapability.hasExpandMacroResult {
Expand Down Expand Up @@ -159,6 +164,7 @@ private extension MacroRole {
case .peer: self = .peer
case .conformance: self = .conformance
case .codeItem: self = .codeItem
case .extension: self = .extension
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ internal enum HostToPluginMessage: Codable {
discriminator: String,
attributeSyntax: PluginMessage.Syntax,
declSyntax: PluginMessage.Syntax,
parentDeclSyntax: PluginMessage.Syntax?
parentDeclSyntax: PluginMessage.Syntax?,
extendedTypeSyntax: PluginMessage.Syntax?
)

/// Optionally implemented message to load a dynamic link library.
Expand Down Expand Up @@ -76,7 +77,7 @@ internal enum PluginToHostMessage: Codable {
}

/*namespace*/ internal enum PluginMessage {
static var PROTOCOL_VERSION_NUMBER: Int { 5 } // Added 'expandMacroResult'.
static var PROTOCOL_VERSION_NUMBER: Int { 6 } // Added 'expandMacroResult'.

struct HostCapability: Codable {
var protocolVersion: Int
Expand Down Expand Up @@ -107,6 +108,7 @@ internal enum PluginToHostMessage: Codable {
case peer
case conformance
case codeItem
case `extension`
}

struct SourceLocation: Codable {
Expand Down
32 changes: 31 additions & 1 deletion Sources/SwiftParser/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,14 +329,44 @@ extension Parser {
)
)
case nil:
let isAttached = self.peek().isAttachedKeyword
return parseAttribute(argumentMode: .customAttribute) { parser in
let arguments = parser.parseArgumentListElements(pattern: .none)
let arguments: [RawTupleExprElementSyntax]
if isAttached {
arguments = parser.parseAttachedArguments()
} else {
arguments = parser.parseArgumentListElements(pattern: .none)
}

return .argumentList(RawTupleExprElementListSyntax(elements: arguments, arena: parser.arena))
}
}
}
}

extension Parser {
mutating func parseAttachedArguments() -> [RawTupleExprElementSyntax] {
let (unexpectedBeforeRole, role) = self.expect(.identifier, TokenSpec(.extension, remapping: .identifier), default: .identifier)
let roleTrailingComma = self.consume(if: .comma)
let roleElement = RawTupleExprElementSyntax(
label: nil,
colon: nil,
expression: RawExprSyntax(
RawIdentifierExprSyntax(
unexpectedBeforeRole,
identifier: role,
declNameArguments: nil,
arena: self.arena
)
),
trailingComma: roleTrailingComma,
arena: self.arena
)
let additionalArgs = self.parseArgumentListElements(pattern: .none)
return [roleElement] + additionalArgs
}
}

extension Parser {
mutating func parseDifferentiableAttribute() -> RawAttributeSyntax {
let (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)
Expand Down
4 changes: 4 additions & 0 deletions Sources/SwiftParser/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,10 @@ extension Lexer.Lexeme {
|| self.rawTokenKind == .prefixOperator
}

var isAttachedKeyword: Bool {
return self.rawTokenKind == .identifier && self.tokenText == "attached"
}

var isEllipsis: Bool {
return self.isAnyOperator && self.tokenText == "..."
}
Expand Down
44 changes: 43 additions & 1 deletion Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public enum MacroRole {
case peer
case conformance
case codeItem
case `extension`
}

extension MacroRole {
Expand All @@ -35,6 +36,7 @@ extension MacroRole {
case .peer: return "PeerMacro"
case .conformance: return "ConformanceMacro"
case .codeItem: return "CodeItemMacro"
case .extension: return "ExtensionMacro"
}
}
}
Expand All @@ -45,6 +47,7 @@ private enum MacroExpansionError: Error, CustomStringConvertible {
case parentDeclGroupNil
case declarationNotDeclGroup
case declarationNotIdentified
case noExtendedTypeSyntax
case noFreestandingMacroRoles(Macro.Type)

var description: String {
Expand All @@ -61,6 +64,9 @@ private enum MacroExpansionError: Error, CustomStringConvertible {
case .declarationNotIdentified:
return "declaration is not a 'Identified' syntax"

case .noExtendedTypeSyntax:
return "no extended type for extension macro"

case .noFreestandingMacroRoles(let type):
return "macro implementation type '\(type)' does not conform to any freestanding macro protocol"

Expand Down Expand Up @@ -113,7 +119,7 @@ public func expandFreestandingMacro(
let rewritten = try codeItemMacroDef.expansion(of: node, in: context)
expandedSyntax = Syntax(CodeBlockItemListSyntax(rewritten))

case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.expression, _), (.declaration, _),
case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.extension, _), (.expression, _), (.declaration, _),
(.codeItem, _):
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
}
Expand Down Expand Up @@ -178,6 +184,7 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
attributeNode: AttributeSyntax,
declarationNode: DeclSyntax,
parentDeclNode: DeclSyntax?,
extendedType: TypeSyntax?,
in context: Context
) -> [String]? {
do {
Expand Down Expand Up @@ -295,6 +302,39 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
return "extension \(typeName) : \(protocolName) \(whereClause) {}"
}

case (let attachedMacro as ExtensionMacro.Type, .extension):
guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) else {
// Compiler error: type mismatch.
throw MacroExpansionError.declarationNotDeclGroup
}

guard let extendedType = extendedType else {
throw MacroExpansionError.noExtendedTypeSyntax
}

// Local function to expand an extension macro once we've opened up
// the existential.
func expandExtensionMacro(
_ node: some DeclGroupSyntax
) throws -> [ExtensionDeclSyntax] {
return try attachedMacro.expansion(
of: attributeNode,
attachedTo: node,
providingExtensionsOf: extendedType,
in: context
)
}

let extensions = try _openExistential(
declGroup,
do: expandExtensionMacro
)

// Form a buffer of peer declarations to return to the caller.
return extensions.map {
$0.formattedExpansion(definition.formatMode)
}

default:
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
}
Expand Down Expand Up @@ -323,6 +363,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
attributeNode: AttributeSyntax,
declarationNode: DeclSyntax,
parentDeclNode: DeclSyntax?,
extendedType: TypeSyntax?,
in context: Context
) -> String? {
let expandedSources = expandAttachedMacroWithoutCollapsing(
Expand All @@ -331,6 +372,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
attributeNode: attributeNode,
declarationNode: declarationNode,
parentDeclNode: parentDeclNode,
extendedType: extendedType,
in: context
)
return expandedSources.map {
Expand Down
1 change: 1 addition & 0 deletions Sources/SwiftSyntaxMacros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ add_swift_host_library(SwiftSyntaxMacros
MacroProtocols/ConformanceMacro.swift
MacroProtocols/DeclarationMacro.swift
MacroProtocols/ExpressionMacro.swift
MacroProtocols/ExtensionMacro.swift
MacroProtocols/FreestandingMacro.swift
MacroProtocols/Macro.swift
MacroProtocols/Macro+Format.swift
Expand Down
35 changes: 35 additions & 0 deletions Sources/SwiftSyntaxMacros/MacroProtocols/ExtensionMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//===----------------------------------------------------------------------===//
//
// 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 SwiftSyntax

/// Describes a macro that can add extensions to the declaration it's
/// attached to.
public protocol ExtensionMacro: AttachedMacro {
/// Expand an attached extension macro to produce a set of extensions.
///
/// - Parameters:
/// - node: The custom attribute describing the attached macro.
/// - declaration: The declaration the macro attribute is attached to.
/// - type: The type to provide extensions of.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: the set of extension declarations introduced by the macro,
/// which are always inserted at top-level scope. Each extension must extend
/// the `type` parameter.
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax]
}
19 changes: 18 additions & 1 deletion Sources/SwiftSyntaxMacros/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
return true
}

return !(macro is PeerMacro.Type || macro is MemberMacro.Type || macro is AccessorMacro.Type || macro is MemberAttributeMacro.Type || macro is ConformanceMacro.Type)
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 {
Expand Down Expand Up @@ -433,6 +433,23 @@ extension MacroApplication {
}
}

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
}

Expand Down
16 changes: 16 additions & 0 deletions Tests/SwiftParserTest/AttributeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -595,4 +595,20 @@ final class AttributeTests: XCTestCase {
"""
)
}

func testAttachedExtensionAttribute() {
assertParse(
"""
@attached(extension)
macro m()
"""
)

assertParse(
"""
@attached(extension, names: named(test))
macro m()
"""
)
}
}
Loading