Skip to content

Commit a7a147d

Browse files
committed
[Macros] Add attached extension macros.
1 parent a9cff1a commit a7a147d

File tree

6 files changed

+125
-2
lines changed

6 files changed

+125
-2
lines changed

Sources/SwiftParser/Attributes.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,14 +332,44 @@ extension Parser {
332332
)
333333
)
334334
case nil:
335+
let isAttached = self.peek().isAttachedKeyword
335336
return parseAttribute(argumentMode: .customAttribute) { parser in
336-
let arguments = parser.parseArgumentListElements(pattern: .none)
337+
let arguments: [RawTupleExprElementSyntax]
338+
if isAttached {
339+
arguments = parser.parseAttachedArguments()
340+
} else {
341+
arguments = parser.parseArgumentListElements(pattern: .none)
342+
}
343+
337344
return .argumentList(RawTupleExprElementListSyntax(elements: arguments, arena: parser.arena))
338345
}
339346
}
340347
}
341348
}
342349

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

Sources/SwiftParser/Types.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1076,6 +1076,10 @@ extension Lexer.Lexeme {
10761076
|| self.rawTokenKind == .prefixOperator
10771077
}
10781078

1079+
var isAttachedKeyword: Bool {
1080+
return self.rawTokenKind == .identifier && self.tokenText == "attached"
1081+
}
1082+
10791083
var isEllipsis: Bool {
10801084
return self.isAnyOperator && self.tokenText == "..."
10811085
}

Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public enum MacroRole {
1010
case peer
1111
case conformance
1212
case codeItem
13+
case `extension`
1314
}
1415

1516
extension MacroRole {
@@ -23,6 +24,7 @@ extension MacroRole {
2324
case .peer: return "PeerMacro"
2425
case .conformance: return "ConformanceMacro"
2526
case .codeItem: return "CodeItemMacro"
27+
case .extension: return "ExtensionMacro"
2628
}
2729
}
2830
}
@@ -101,7 +103,7 @@ public func expandFreestandingMacro(
101103
let rewritten = try codeItemMacroDef.expansion(of: node, in: context)
102104
expandedSyntax = Syntax(CodeBlockItemListSyntax(rewritten))
103105

104-
case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.expression, _), (.declaration, _),
106+
case (.accessor, _), (.memberAttribute, _), (.member, _), (.peer, _), (.conformance, _), (.extension, _), (.expression, _), (.declaration, _),
105107
(.codeItem, _):
106108
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
107109
}
@@ -283,6 +285,42 @@ public func expandAttachedMacro<Context: MacroExpansionContext>(
283285
return "extension \(typeName) : \(protocolName) \(whereClause) {}"
284286
}
285287

288+
case (let attachedMacro as ExtensionMacro.Type, .extension):
289+
guard let declGroup = declarationNode.asProtocol(DeclGroupSyntax.self) else {
290+
// Compiler error: type mismatch.
291+
throw MacroExpansionError.declarationNotDeclGroup
292+
}
293+
guard let identified = declarationNode.asProtocol(IdentifiedDeclSyntax.self)
294+
else {
295+
// Compiler error: type mismatch.
296+
throw MacroExpansionError.declarationNotIdentified
297+
}
298+
299+
let type: TypeSyntax = "\(identified.identifier)"
300+
301+
// Local function to expand a extension macro once we've opened up
302+
// the existential.
303+
func expandExtensionMacro(
304+
_ node: some DeclGroupSyntax
305+
) throws -> [ExtensionDeclSyntax] {
306+
return try attachedMacro.expansion(
307+
of: attributeNode,
308+
attachedTo: node,
309+
providingExtensionsOf: type,
310+
in: context
311+
)
312+
}
313+
314+
let extensions = try _openExistential(
315+
declGroup,
316+
do: expandExtensionMacro
317+
)
318+
319+
// Form a buffer of peer declarations to return to the caller.
320+
return extensions.map {
321+
$0.formattedExpansion(definition.formatMode)
322+
}
323+
286324
default:
287325
throw MacroExpansionError.unmatchedMacroRole(definition, macroRole)
288326
}

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: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 conformances to the declaration it's
16+
/// attached to.
17+
public protocol ExtensionMacro: AttachedMacro {
18+
/// Expand an attached conformance macro to produce a set of conformances.
19+
///
20+
/// - Parameters:
21+
/// - node: The custom attribute describing the attached macro.
22+
/// - declaration: The declaration the macro attribute is attached to.
23+
/// - context: The context in which to perform the macro expansion.
24+
///
25+
/// - Returns: the set of `(type, where-clause?)` pairs that each provide the
26+
/// protocol type to which the declared type conforms, along with
27+
/// an optional where clause.
28+
static func expansion(
29+
of node: AttributeSyntax,
30+
attachedTo decl: some DeclGroupSyntax,
31+
providingExtensionsOf type: some TypeSyntaxProtocol,
32+
in context: some MacroExpansionContext
33+
) throws -> [ExtensionDeclSyntax]
34+
}

Tests/SwiftParserTest/AttributeTests.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,4 +629,20 @@ final class AttributeTests: XCTestCase {
629629
"""
630630
)
631631
}
632+
633+
func testAttachedExtensionAttribute() {
634+
assertParse(
635+
"""
636+
@attached(extension)
637+
macro m()
638+
"""
639+
)
640+
641+
assertParse(
642+
"""
643+
@attached(extension, names: named(test))
644+
macro m()
645+
"""
646+
)
647+
}
632648
}

0 commit comments

Comments
 (0)