-
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
Conversation
`expandAttachedMacro`.
@swift-ci please test |
@swift-ci please test |
) | ||
} | ||
|
||
let extensions = try _openExistential( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect we don't need to do _openExistential
tricks nowadays, but it's fine this way
|
||
/// Describes a macro that can add conformances to the declaration it's | ||
/// attached to. | ||
public protocol ExtensionMacro: AttachedMacro { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had an idea! We could make ConformanceMacro
refine ExtensionMacro
, with a default implementation of the extension entry point based on the conformance entry point. That way, any macro implementations that implement ConformanceMacro
today are automatically "upgraded" to ExtensionMacro
implementations, and they provide the appropriate @attached(extension...)
alongside the conformance one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh! That is a really good idea
macro m() | ||
""" | ||
) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
conformances:
as well? Shouldn't really matter since it's just parsed as a custom attribute anyway, but good to have the test case IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi there 👋,
As someone pretty fresh to this project, I've had a good look at this PR and wow - it's really cool! You've made some fantastic improvements here.
I've attempted to provide constructive feedback on some areas that I noticed while reviewing. Please bear in mind, my understanding of this project's conventions and code structure is still growing, so if any of my comments are off the mark or unrelated, I sincerely apologize. I deeply appreciate your understanding and patience.
Can't wait to get more involved with this project and learn from all of you. 💙
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed the new parameter introduced doesn't have documentation in the Parameters
doc-comment section above. It might be a good idea to add some info there for clarity. We could use something similar to what we have for parentDeclNode
, since it's also used exclusively for MacroRole.extension
. If we agree on this, we might want to add the same info for expandAttachedMacro(...)
function as well. Just a thought!
btw: I was wondering, instead of adding a new parameter for a specific case from MacroRole
, what if we created an enum with associated values? Here's an idea I had:
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 comment
The 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 extendedType
an associated value of PluginMessage.MacroRole
which would complicate the Codable
conformance, so it seemed more straightforward to pass extendedType
as an argument to expandAttachedMacro
. I'm still interested in exploring this approach further separately though, because it seems plausible that we might want more role-specific arguments, and those conceptually make sense as enum associated values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah, I see.
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) | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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 ConformanceMacro
refine ExtensionMacro
so that they can be implemented in terms of extension macros. As part of that, I will clean up this code to only operate on ExtensionMacro
, since ConformanceMacro
will be a refinement of it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds really cool! Can't wait to see the result!
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
In the proposal it's mentioned:
Each
ExtensionDeclSyntax
in the resulting array must use theprovidingExtensionsOf
parameter as the extended > type, which is a qualified type name. For example, for the following code:struct Outer { @MyProtocol struct Inner {} }The type syntax passed to
ExtensionMacro.expansion
forprovidingExtensionsOf
isOuter.Inner
.
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 extendedType.description
here will look something like this: Outer.Inner
? If I understand the assumptions correctly, I think this would happen because the code above:
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 @AddSendableExtension
macro:
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:
failed - Actual output (+) differed from expected output (-):
struct Outer {
struct Inner {
}
}
–extension Outer.Inner: Sendable {
–}
Actual expanded source:
struct Outer {
struct Inner {
}
}
extension Outer.Inner: Sendable {
}
Am I understanding it correctly that this shouldn't fail?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 MacroSystem
in SwiftSyntax is only used for unit testing macros, but the real expansion is integrated into the compiler (and implemented in swiftlang/swift#66967). It's not possible for MacroSystem
to form the same fully qualified type that the compiler can form in the general case, e.g. the compiler can look through type aliases, while the macro system here cannot. I believe there are also some known issues with where the expansion is inserted in MacroSystem
for conformance macros, because it doesn't insert them at the top-level like the compiler does. I definitely think we can improve what MacroSystem
does in many cases (e.g. we could form a qualified name from the lexical nesting structure), but it can't perfectly match what the compiler does during extension macro expansion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't fully aware of that, I really appreciate the explanation.
… macros and add a macro plugin test for extension macros.
@swift-ci please test |
1 similar comment
@swift-ci please test |
@swift-ci please test |
…with PluginMessages.swift in the Swift repository.
@swift-ci please test |
@swift-ci please test Windows |
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) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 attached
as a contextual keyword. I’m fixing that in #1881.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you!
The SwiftSyntax-side change to swiftlang/swift#66967