Skip to content

Commit 8fd9dad

Browse files
authored
Merge pull request #1861 from hborla/5.9-extension-macros
[5.9][Macros] Add attached `extension` macros.
2 parents c73bfbd + cf1e15b commit 8fd9dad

File tree

14 files changed

+224
-8
lines changed

14 files changed

+224
-8
lines changed

Examples/Sources/ExamplePlugin/ExamplePlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ struct ThePlugin: CompilerPlugin {
1010
PeerValueWithSuffixNameMacro.self,
1111
MemberDeprecatedMacro.self,
1212
EquatableConformanceMacro.self,
13+
SendableExtensionMacro.self,
1314
DidSetPrintMacro.self,
1415
PrintAnyMacro.self,
1516
]

Examples/Sources/ExamplePlugin/Macros.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,26 @@ struct EquatableConformanceMacro: ConformanceMacro {
8686
}
8787
}
8888

89+
public struct SendableExtensionMacro: ExtensionMacro {
90+
public static func expansion(
91+
of node: AttributeSyntax,
92+
attachedTo: some DeclGroupSyntax,
93+
providingExtensionsOf type: some TypeSyntaxProtocol,
94+
in context: some MacroExpansionContext
95+
) throws -> [ExtensionDeclSyntax] {
96+
let sendableExtension: DeclSyntax =
97+
"""
98+
extension \(type.trimmed): Sendable {}
99+
"""
100+
101+
guard let extensionDecl = sendableExtension.as(ExtensionDeclSyntax.self) else {
102+
return []
103+
}
104+
105+
return [extensionDecl]
106+
}
107+
}
108+
89109
/// Add 'didSet' printing the new value.
90110
struct DidSetPrintMacro: AccessorMacro {
91111
static func expansion(

Sources/SwiftCompilerPluginMessageHandling/CompilerPluginMessageHandler.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,15 @@ extension CompilerPluginMessageHandler {
122122
expandingSyntax: expandingSyntax
123123
)
124124

125-
case .expandAttachedMacro(let macro, let macroRole, let discriminator, let attributeSyntax, let declSyntax, let parentDeclSyntax):
125+
case .expandAttachedMacro(let macro, let macroRole, let discriminator, let attributeSyntax, let declSyntax, let parentDeclSyntax, let extendedTypeSyntax):
126126
try expandAttachedMacro(
127127
macro: macro,
128128
macroRole: macroRole,
129129
discriminator: discriminator,
130130
attributeSyntax: attributeSyntax,
131131
declSyntax: declSyntax,
132-
parentDeclSyntax: parentDeclSyntax
132+
parentDeclSyntax: parentDeclSyntax,
133+
extendedTypeSyntax: extendedTypeSyntax
133134
)
134135

135136
case .loadPluginLibrary(let libraryPath, let moduleName):

Sources/SwiftCompilerPluginMessageHandling/Macros.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ extension CompilerPluginMessageHandler {
8686
discriminator: String,
8787
attributeSyntax: PluginMessage.Syntax,
8888
declSyntax: PluginMessage.Syntax,
89-
parentDeclSyntax: PluginMessage.Syntax?
89+
parentDeclSyntax: PluginMessage.Syntax?,
90+
extendedTypeSyntax: PluginMessage.Syntax?
9091
) throws {
9192
let sourceManager = SourceManager()
9293
let context = PluginMacroExpansionContext(
@@ -100,6 +101,9 @@ extension CompilerPluginMessageHandler {
100101
).cast(AttributeSyntax.self)
101102
let declarationNode = sourceManager.add(declSyntax).cast(DeclSyntax.self)
102103
let parentDeclNode = parentDeclSyntax.map { sourceManager.add($0).cast(DeclSyntax.self) }
104+
let extendedType = extendedTypeSyntax.map {
105+
sourceManager.add($0).cast(TypeSyntax.self)
106+
}
103107

104108
// TODO: Make this a 'String?' and remove non-'hasExpandMacroResult' branches.
105109
let expandedSources: [String]?
@@ -115,6 +119,7 @@ extension CompilerPluginMessageHandler {
115119
attributeNode: attributeNode,
116120
declarationNode: declarationNode,
117121
parentDeclNode: parentDeclNode,
122+
extendedType: extendedType,
118123
in: context
119124
)
120125
if let expansions, hostCapability.hasExpandMacroResult {
@@ -159,6 +164,7 @@ private extension MacroRole {
159164
case .peer: self = .peer
160165
case .conformance: self = .conformance
161166
case .codeItem: self = .codeItem
167+
case .extension: self = .extension
162168
}
163169
}
164170
}

Sources/SwiftCompilerPluginMessageHandling/PluginMessages.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ internal enum HostToPluginMessage: Codable {
3434
discriminator: String,
3535
attributeSyntax: PluginMessage.Syntax,
3636
declSyntax: PluginMessage.Syntax,
37-
parentDeclSyntax: PluginMessage.Syntax?
37+
parentDeclSyntax: PluginMessage.Syntax?,
38+
extendedTypeSyntax: PluginMessage.Syntax?
3839
)
3940

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

7879
/*namespace*/ internal enum PluginMessage {
79-
static var PROTOCOL_VERSION_NUMBER: Int { 5 } // Added 'expandMacroResult'.
80+
static var PROTOCOL_VERSION_NUMBER: Int { 6 } // Added 'expandMacroResult'.
8081

8182
struct HostCapability: Codable {
8283
var protocolVersion: Int
@@ -107,6 +108,7 @@ internal enum PluginToHostMessage: Codable {
107108
case peer
108109
case conformance
109110
case codeItem
111+
case `extension`
110112
}
111113

112114
struct SourceLocation: Codable {

Sources/SwiftParser/Attributes.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,14 +329,44 @@ extension Parser {
329329
)
330330
)
331331
case nil:
332+
let isAttached = self.peek().isAttachedKeyword
332333
return parseAttribute(argumentMode: .customAttribute) { parser in
333-
let arguments = parser.parseArgumentListElements(pattern: .none)
334+
let arguments: [RawTupleExprElementSyntax]
335+
if isAttached {
336+
arguments = parser.parseAttachedArguments()
337+
} else {
338+
arguments = parser.parseArgumentListElements(pattern: .none)
339+
}
340+
334341
return .argumentList(RawTupleExprElementListSyntax(elements: arguments, arena: parser.arena))
335342
}
336343
}
337344
}
338345
}
339346

347+
extension Parser {
348+
mutating func parseAttachedArguments() -> [RawTupleExprElementSyntax] {
349+
let (unexpectedBeforeRole, role) = self.expect(.identifier, TokenSpec(.extension, remapping: .identifier), default: .identifier)
350+
let roleTrailingComma = self.consume(if: .comma)
351+
let roleElement = RawTupleExprElementSyntax(
352+
label: nil,
353+
colon: nil,
354+
expression: RawExprSyntax(
355+
RawIdentifierExprSyntax(
356+
unexpectedBeforeRole,
357+
identifier: role,
358+
declNameArguments: nil,
359+
arena: self.arena
360+
)
361+
),
362+
trailingComma: roleTrailingComma,
363+
arena: self.arena
364+
)
365+
let additionalArgs = self.parseArgumentListElements(pattern: .none)
366+
return [roleElement] + additionalArgs
367+
}
368+
}
369+
340370
extension Parser {
341371
mutating func parseDifferentiableAttribute() -> RawAttributeSyntax {
342372
let (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)

Sources/SwiftParser/Types.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1063,6 +1063,10 @@ extension Lexer.Lexeme {
10631063
|| self.rawTokenKind == .prefixOperator
10641064
}
10651065

1066+
var isAttachedKeyword: Bool {
1067+
return self.rawTokenKind == .identifier && self.tokenText == "attached"
1068+
}
1069+
10661070
var isEllipsis: Bool {
10671071
return self.isAnyOperator && self.tokenText == "..."
10681072
}

Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public enum MacroRole {
2222
case peer
2323
case conformance
2424
case codeItem
25+
case `extension`
2526
}
2627

2728
extension MacroRole {
@@ -35,6 +36,7 @@ extension MacroRole {
3536
case .peer: return "PeerMacro"
3637
case .conformance: return "ConformanceMacro"
3738
case .codeItem: return "CodeItemMacro"
39+
case .extension: return "ExtensionMacro"
3840
}
3941
}
4042
}
@@ -45,6 +47,7 @@ private enum MacroExpansionError: Error, CustomStringConvertible {
4547
case parentDeclGroupNil
4648
case declarationNotDeclGroup
4749
case declarationNotIdentified
50+
case noExtendedTypeSyntax
4851
case noFreestandingMacroRoles(Macro.Type)
4952

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

67+
case .noExtendedTypeSyntax:
68+
return "no extended type for extension macro"
69+
6470
case .noFreestandingMacroRoles(let type):
6571
return "macro implementation type '\(type)' does not conform to any freestanding macro protocol"
6672

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

116-
case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.expression, _), (.declaration, _),
122+
case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.extension, _), (.expression, _), (.declaration, _),
117123
(.codeItem, _):
118124
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
119125
}
@@ -178,6 +184,7 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
178184
attributeNode: AttributeSyntax,
179185
declarationNode: DeclSyntax,
180186
parentDeclNode: DeclSyntax?,
187+
extendedType: TypeSyntax?,
181188
in context: Context
182189
) -> [String]? {
183190
do {
@@ -295,6 +302,39 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>
295302
return "extension \(typeName) : \(protocolName) \(whereClause) {}"
296303
}
297304

305+
case (let attachedMacro as ExtensionMacro.Type, .extension):
306+
guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) else {
307+
// Compiler error: type mismatch.
308+
throw MacroExpansionError.declarationNotDeclGroup
309+
}
310+
311+
guard let extendedType = extendedType else {
312+
throw MacroExpansionError.noExtendedTypeSyntax
313+
}
314+
315+
// Local function to expand an extension macro once we've opened up
316+
// the existential.
317+
func expandExtensionMacro(
318+
_ node: some DeclGroupSyntax
319+
) throws -> [ExtensionDeclSyntax] {
320+
return try attachedMacro.expansion(
321+
of: attributeNode,
322+
attachedTo: node,
323+
providingExtensionsOf: extendedType,
324+
in: context
325+
)
326+
}
327+
328+
let extensions = try _openExistential(
329+
declGroup,
330+
do: expandExtensionMacro
331+
)
332+
333+
// Form a buffer of peer declarations to return to the caller.
334+
return extensions.map {
335+
$0.formattedExpansion(definition.formatMode)
336+
}
337+
298338
default:
299339
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
300340
}
@@ -323,6 +363,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
323363
attributeNode: AttributeSyntax,
324364
declarationNode: DeclSyntax,
325365
parentDeclNode: DeclSyntax?,
366+
extendedType: TypeSyntax?,
326367
in context: Context
327368
) -> String? {
328369
let expandedSources = expandAttachedMacroWithoutCollapsing(
@@ -331,6 +372,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
331372
attributeNode: attributeNode,
332373
declarationNode: declarationNode,
333374
parentDeclNode: parentDeclNode,
375+
extendedType: extendedType,
334376
in: context
335377
)
336378
return expandedSources.map {

Sources/SwiftSyntaxMacros/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ add_swift_host_library(SwiftSyntaxMacros
1313
MacroProtocols/ConformanceMacro.swift
1414
MacroProtocols/DeclarationMacro.swift
1515
MacroProtocols/ExpressionMacro.swift
16+
MacroProtocols/ExtensionMacro.swift
1617
MacroProtocols/FreestandingMacro.swift
1718
MacroProtocols/Macro.swift
1819
MacroProtocols/Macro+Format.swift
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
13+
import SwiftSyntax
14+
15+
/// Describes a macro that can add extensions to the declaration it's
16+
/// attached to.
17+
public protocol ExtensionMacro: AttachedMacro {
18+
/// Expand an attached extension macro to produce a set of extensions.
19+
///
20+
/// - Parameters:
21+
/// - node: The custom attribute describing the attached macro.
22+
/// - declaration: The declaration the macro attribute is attached to.
23+
/// - type: The type to provide extensions of.
24+
/// - context: The context in which to perform the macro expansion.
25+
///
26+
/// - Returns: the set of extension declarations introduced by the macro,
27+
/// which are always inserted at top-level scope. Each extension must extend
28+
/// the `type` parameter.
29+
static func expansion(
30+
of node: AttributeSyntax,
31+
attachedTo declaration: some DeclGroupSyntax,
32+
providingExtensionsOf type: some TypeSyntaxProtocol,
33+
in context: some MacroExpansionContext
34+
) throws -> [ExtensionDeclSyntax]
35+
}

Sources/SwiftSyntaxMacros/MacroSystem.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
110110
return true
111111
}
112112

113-
return !(macro is PeerMacro.Type || macro is MemberMacro.Type || macro is AccessorMacro.Type || macro is MemberAttributeMacro.Type || macro is ConformanceMacro.Type)
113+
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)
114114
}
115115

116116
if newAttributes.isEmpty {
@@ -433,6 +433,23 @@ extension MacroApplication {
433433
}
434434
}
435435

436+
let extensionMacroAttrs = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ExtensionMacro.Type.self)
437+
let extendedTypeSyntax = TypeSyntax("\(extendedType.trimmed)")
438+
for (attribute, extensionMacro) in extensionMacroAttrs {
439+
do {
440+
let newExtensions = try extensionMacro.expansion(
441+
of: attribute,
442+
attachedTo: decl,
443+
providingExtensionsOf: extendedTypeSyntax,
444+
in: context
445+
)
446+
447+
extensions.append(contentsOf: newExtensions.map(DeclSyntax.init))
448+
} catch {
449+
context.addDiagnostics(from: error, node: attribute)
450+
}
451+
}
452+
436453
return extensions
437454
}
438455

Tests/SwiftParserTest/AttributeTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,4 +595,20 @@ final class AttributeTests: XCTestCase {
595595
"""
596596
)
597597
}
598+
599+
func testAttachedExtensionAttribute() {
600+
assertParse(
601+
"""
602+
@attached(extension)
603+
macro m()
604+
"""
605+
)
606+
607+
assertParse(
608+
"""
609+
@attached(extension, names: named(test))
610+
macro m()
611+
"""
612+
)
613+
}
598614
}

0 commit comments

Comments
 (0)