-
Notifications
You must be signed in to change notification settings - Fork 440
[Macros] Add attached extension
macros.
#1859
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
aa6cc4a
5d77ef4
50bacd8
d11f4c7
1286637
3e2c782
9d1a869
0c1634b
0ac000d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -332,14 +332,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) | ||
} | ||
|
||
Comment on lines
+335
to
+343
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of matching the string contents of the current token here, it’s much clearer to add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you! |
||
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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ public enum MacroRole { | |
case peer | ||
case conformance | ||
case codeItem | ||
case `extension` | ||
} | ||
|
||
extension MacroRole { | ||
|
@@ -35,6 +36,7 @@ extension MacroRole { | |
case .peer: return "PeerMacro" | ||
case .conformance: return "ConformanceMacro" | ||
case .codeItem: return "CodeItemMacro" | ||
case .extension: return "ExtensionMacro" | ||
} | ||
} | ||
} | ||
|
@@ -45,6 +47,7 @@ private enum MacroExpansionError: Error, CustomStringConvertible { | |
case parentDeclGroupNil | ||
case declarationNotDeclGroup | ||
case declarationNotIdentified | ||
case noExtendedTypeSyntax | ||
case noFreestandingMacroRoles(Macro.Type) | ||
|
||
var description: String { | ||
|
@@ -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" | ||
|
||
|
@@ -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) | ||
} | ||
|
@@ -178,6 +184,7 @@ public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext> | |
attributeNode: AttributeSyntax, | ||
declarationNode: DeclSyntax, | ||
parentDeclNode: DeclSyntax?, | ||
extendedType: TypeSyntax?, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I noticed the new parameter introduced doesn't have documentation in the btw: I was wondering, instead of adding a new parameter for a specific case from enum MacroRoleContext {
case expression
case declaration
case accessor
case memberAttribute(parentDeclNode: DeclSyntax)
case member
case peer
case conformance
case codeItem
case `extension`(extendedType: TypeSyntax)
}
public func expandAttachedMacroWithoutCollapsing<Context: MacroExpansionContext>(
definition: Macro.Type,
macroRole: MacroRoleContext,
attributeNode: AttributeSyntax,
declarationNode: DeclSyntax,
in context: Context
) -> [String]? { (...) I'd love to hear your thoughts on this! This is just something that popped into my mind, and I'm not sure if it makes any sense - just a thought. 🤔😊 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually had the same thought while I was implementing this! But @rintaro pointed out to me that we might also need to make the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah, I see. |
||
in context: Context | ||
) -> [String]? { | ||
do { | ||
|
@@ -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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suspect we don't need to do |
||
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) | ||
} | ||
|
@@ -323,6 +363,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>( | |
attributeNode: AttributeSyntax, | ||
declarationNode: DeclSyntax, | ||
parentDeclNode: DeclSyntax?, | ||
extendedType: TypeSyntax?, | ||
in context: Context | ||
) -> String? { | ||
let expandedSources = expandAttachedMacroWithoutCollapsing( | ||
|
@@ -331,6 +372,7 @@ public func expandAttachedMacro<Context: MacroExpansionContext>( | |
attributeNode: attributeNode, | ||
declarationNode: declarationNode, | ||
parentDeclNode: parentDeclNode, | ||
extendedType: extendedType, | ||
in: context | ||
) | ||
return expandedSources.map { | ||
|
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had an idea! We could make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh! That is a really good idea |
||
/// 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] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -115,7 +115,8 @@ class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter { | |
|| macro is MemberMacro.Type | ||
|| macro is AccessorMacro.Type | ||
|| macro is MemberAttributeMacro.Type | ||
|| macro is ConformanceMacro.Type) | ||
|| macro is ConformanceMacro.Type | ||
|| macro is ExtensionMacro.Type) | ||
} | ||
|
||
if newAttributes.isEmpty { | ||
|
@@ -438,6 +439,23 @@ extension MacroApplication { | |
} | ||
} | ||
|
||
let extensionMacroAttrs = getMacroAttributes(attachedTo: decl.as(DeclSyntax.self)!, ofType: ExtensionMacro.Type.self) | ||
let extendedTypeSyntax = TypeSyntax("\(extendedType.trimmed)") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the proposal it's mentioned:
I'm not sure if I've understood this part of the proposal correctly. Does it mean that if I create this macro: public struct SomeExtensionMacro: ExtensionMacro {
public static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
[ExtensionDeclSyntax(extendedType: type, memberBlock: MemberDeclBlockSyntax {})]
}
} and I attach it like so struct Outer {
@SomeExtensionMacro
struct Inner {}
} are we expecting that let extendedType: Syntax
if let identified = decl.asProtocol(IdentifiedDeclSyntax.self) {
extendedType = Syntax(identified.identifier.trimmed)
} else if let ext = decl.as(ExtensionDeclSyntax.self) {
extendedType = Syntax(ext.extendedType.trimmed)
} else {
return []
} can only extract the identifier from the root type. Here is a test case covering this example using func testExtensionExpansionOfNestedType() {
assertMacroExpansion(
"""
struct Outer {
@AddSendableExtension
struct Inner {
}
}
""",
expandedSource:
"""
struct Outer {
struct Inner {
}
}
extension Outer.Inner: Sendable {
}
""",
macros: testMacros,
indentationWidth: indentationWidth
)
} right now it's failing with message:
Am I understanding it correctly that this shouldn't fail? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a great question, and there's a little bit of nuance here because the expansion performed by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't fully aware of that, I really appreciate the explanation. |
||
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) | ||
} | ||
} | ||
|
||
Comment on lines
+442
to
+458
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not entirely sure what the assumptions are about how we're structuring code in this project, but I wonder whether it might be better to extract this code into a separate method. The name and doc-comment of this method suggest that the responsibility of this method is solely to expand conformances - I guess expanding extensions could be seen as a bit different responsibility. Just a thought! 💜 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Conformance macros are effectively a special case of extension macros. I'm going to take Doug's suggestion here in a follow-up PR and make There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds really cool! Can't wait to see the result! |
||
return extensions | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -629,4 +629,20 @@ final class AttributeTests: XCTestCase { | |
""" | ||
) | ||
} | ||
|
||
func testAttachedExtensionAttribute() { | ||
assertParse( | ||
""" | ||
@attached(extension) | ||
macro m() | ||
""" | ||
) | ||
|
||
assertParse( | ||
""" | ||
@attached(extension, names: named(test)) | ||
macro m() | ||
""" | ||
) | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.