-
Notifications
You must be signed in to change notification settings - Fork 412
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
[WIP][Macro] Emit error if AccessorMacro is applied to node that is not a variable #2624
base: main
Are you sure you want to change the base?
Changes from 7 commits
fbc962f
09252c7
d870f06
7572558
d6b9cd2
ad9e5ae
83b3407
3fe3f77
e029a45
a9acf65
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 | ||||
---|---|---|---|---|---|---|
|
@@ -16,6 +16,7 @@ internal import SwiftOperators | |||||
@_spi(MacroExpansion) internal import SwiftParser | ||||||
public import SwiftSyntax | ||||||
internal import SwiftSyntaxBuilder | ||||||
internal import SwiftParserDiagnostics | ||||||
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) public import SwiftSyntaxMacros | ||||||
#else | ||||||
import SwiftDiagnostics | ||||||
|
@@ -24,6 +25,7 @@ import SwiftOperators | |||||
import SwiftSyntax | ||||||
import SwiftSyntaxBuilder | ||||||
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros | ||||||
import SwiftParserDiagnostics | ||||||
#endif | ||||||
|
||||||
// MARK: - Public entry function | ||||||
|
@@ -633,6 +635,7 @@ private enum MacroApplicationError: DiagnosticMessage, Error { | |||||
case accessorMacroOnVariableWithMultipleBindings | ||||||
case peerMacroOnVariableWithMultipleBindings | ||||||
case malformedAccessor | ||||||
case macroAttachedToInvalidDecl(String, String) | ||||||
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. Could you add labels for these associated values so it’s easier to tell what the two strings do? |
||||||
|
||||||
var diagnosticID: MessageID { | ||||||
return MessageID(domain: diagnosticDomain, id: "\(self)") | ||||||
|
@@ -650,6 +653,8 @@ private enum MacroApplicationError: DiagnosticMessage, Error { | |||||
return """ | ||||||
macro returned a malformed accessor. Accessors should start with an introducer like 'get' or 'set'. | ||||||
""" | ||||||
case let .macroAttachedToInvalidDecl(macroType, declType): | ||||||
return "'\(macroType)' macro cannot be attached to \(declType)" | ||||||
} | ||||||
} | ||||||
} | ||||||
|
@@ -666,6 +671,7 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter { | |||||
/// Store expanded extension while visiting member decls. This should be | ||||||
/// added to top-level 'CodeBlockItemList'. | ||||||
var extensions: [CodeBlockItemSyntax] = [] | ||||||
var expandedAttributes: [AttributeSyntax] = [] | ||||||
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 think this can be Also, I just tried to suggest a doc comment for it and couldn’t really come up with a great explanation, which is usually a sign that the design isn’t quite right (maybe you can find a good explanation). Thinking about it made me wonder if it would it be easier to
/// Attributes that look like macro attributes and should be expanded.
///
/// Attributes are added to this list from `visitAny`. Any attributes that weren’t actually expanded (eg. because they are accessor macros but not attached to a variable or subscript), will be diagnosed from `visitAny`.
var attributesToExpand: [AttributeSyntax] = []
Do you think that would work? 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 found that the id is still different. The It seems that in 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 also delved into the implementation of 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 think I’ll need to play around with the code myself a little to figure out how we want to continue, but am too busy with other things at the moment. I’ll definitely come back to you but it might take a few weeks. 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. Hi @ahoppen , are there any updates on this? 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 was super busy implementing background indexing in SourceKit-LSP over the last few weeks. I’m hoping that this will calm down now and I’ll allow me to get back to this PR this week. 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. Sorry for the delay, I found some time now. Looks like we were really close, the only issue was that in Diff
diff --git a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
index 23127070..f350c097 100644
--- a/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
+++ b/Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
@@ -669,7 +669,7 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
/// Store expanded extension while visiting member decls. This should be
/// added to top-level 'CodeBlockItemList'.
var extensions: [CodeBlockItemSyntax] = []
- var expandedAttributes: [AttributeSyntax] = []
+ /// Attributes that we are expecting to expand from `visitAny`. As macros are expanded, they should be removed from this list. Any attributes that are still left in this array after a `visitAny` call will generate diagnostics.
+ var attributesToExpand: [(attributeNode: AttributeSyntax, spec: MacroSpec)] = []
init(
macroSystem: MacroSystem,
@@ -707,6 +707,11 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
let attributedNode = node.asProtocol(WithAttributesSyntax.self),
!attributedNode.attributes.isEmpty
{
+ let previousAttributesToExpand = attributesToExpand
+ defer {
+ attributesToExpand = previousAttributesToExpand
+ }
+ attributesToExpand = self.macroAttributes(attachedTo: declSyntax)
// Apply body and preamble macros.
if let nodeWithBody = node.asProtocol(WithOptionalCodeBlockSyntax.self),
let declNodeWithBody = nodeWithBody as? any DeclSyntaxProtocol & WithOptionalCodeBlockSyntax
@@ -720,14 +725,8 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
skipVisitAnyHandling.remove(Syntax(declSyntax))
let attributesToRemove = self.macroAttributes(attachedTo: visitedNode)
- for (attribute, spec) in attributesToRemove {
- if let index = self.expandedAttributes.firstIndex(where: { expandedAttribute in
- expandedAttribute.position == attribute.position
- }) {
- self.expandedAttributes.remove(at: index)
- } else {
- self.diagnosticForUnexpandedMacro(attribute: attribute, macroType: spec.type, attachedTo: declSyntax)
- }
+ for attribute in attributesToExpand {
+ self.diagnosticForUnexpandedMacro(attribute: attribute.attributeNode, macroType: attribute.spec.type, attachedTo: declSyntax)
}
return AttributeRemover(removingWhere: { attributesToRemove.map(\.attributeNode).contains($0) }).rewrite(
visitedNode
@@ -961,10 +960,10 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
)
}
- override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
- var node = super.visit(node).cast(VariableDeclSyntax.self)
+ override func visit(_ originalNode: VariableDeclSyntax) -> DeclSyntax {
+ var node = super.visit(originalNode).cast(VariableDeclSyntax.self)
- guard !macroAttributes(attachedTo: DeclSyntax(node), ofType: AccessorMacro.Type.self).isEmpty else {
+ guard !macroAttributes(attachedTo: DeclSyntax(originalNode), ofType: AccessorMacro.Type.self).isEmpty else {
return DeclSyntax(node)
}
@@ -1061,6 +1060,7 @@ extension MacroApplication {
var result: [ExpandedNode] = []
for macroAttribute in macroAttributes(attachedTo: decl, ofType: ofType) {
+ attributesToExpand = attributesToExpand.filter { $0.attributeNode != macroAttribute.attributeNode }
do {
if let expanded = try expandMacro(
macroAttribute.attributeNode,
@@ -1068,7 +1068,6 @@ extension MacroApplication {
macroAttribute.conformanceList
) {
result += expanded
- self.expandedAttributes.append(macroAttribute.attributeNode)
}
} catch {
contextGenerator(Syntax(decl)).addDiagnostics(from: error, node: macroAttribute.attributeNode)
@@ -1216,7 +1215,7 @@ extension MacroApplication {
from: newAccessors,
indentationWidth: self.indentationWidth
)
- self.expandedAttributes.append(macro.attributeNode)
+ attributesToExpand = attributesToExpand.filter { $0.attributeNode != macro.attributeNode }
}
} else if let newAccessors = try expandAccessorMacroWithoutExistingAccessors(
definition: macro.definition,
@@ -1239,7 +1238,7 @@ extension MacroApplication {
} else {
newAccessorsBlock = newAccessors
}
- self.expandedAttributes.append(macro.attributeNode)
+ attributesToExpand = attributesToExpand.filter { $0.attributeNode != macro.attributeNode }
}
} catch {
contextGenerator(Syntax(storage)).addDiagnostics(from: error, node: macro.attributeNode) 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. Thanks for your help.
The changes of Should we use 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. Looking at the code again, I don’t understand what I did myself 🤦🏽 Do you have a test at hand that shows the issue? And I’m getting the feeling that instead of working around things ad-hoc here, the right solution is to productize #2118, which will allow us to keep track of the node’s in the original tree. 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've pushed my code. As we discussed above, unified diagnostic for unexpanded macros is already implemented. Because of the unexpected result of comparing two attributedNode, a lot of tests will fail. Thinking back to the problem I encountered at the beginning, it was to compare whether two attributeNodes were the same in the origin syntax tree. So I agree with that #2118 should be the right solution. |
||||||
|
||||||
init( | ||||||
macroSystem: MacroSystem, | ||||||
|
@@ -715,14 +721,55 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter { | |||||
let visitedNode = self.visit(declSyntax) | ||||||
skipVisitAnyHandling.remove(Syntax(declSyntax)) | ||||||
|
||||||
let attributesToRemove = self.macroAttributes(attachedTo: visitedNode).map(\.attributeNode) | ||||||
|
||||||
return AttributeRemover(removingWhere: { attributesToRemove.contains($0) }).rewrite(visitedNode) | ||||||
let attributesToRemove = self.macroAttributes(attachedTo: visitedNode) | ||||||
for (attribute, spec) in attributesToRemove { | ||||||
if let index = self.expandedAttributes.firstIndex(where: { expandedAttribute in | ||||||
expandedAttribute.position == attribute.position | ||||||
}) { | ||||||
self.expandedAttributes.remove(at: index) | ||||||
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. Is removing necessary for correctness or do we just do it to keep the number of elements in 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. It will be incorrect if it's possible that two attributes with same position are actually different during the process. For example, the list still contains an attribute expanded in the last visit, and in the same position there is attribute not expanded in this visit, I think it'll go wrong. I think we should remove it from the list if it has been checked to make sure it's correct 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. Ah, I thought we were fine because macros only ever introduce code after the the macro attribute itself, so I thought that the position would still uniquely identify a macro within the source file but thinking about it again, I realize that isn’t true because the macro itself gets removed, which shifts positions forward and thus allows another macro to have the same position. For example, you could have the following @NoOpMacroAB
@MacroThatExpandsToMemberAttributesMacroAndWhichIsAlsoAnAccessorMacro
sturct Foo {
func foo() {}
} which expands to sturct Foo {
@MacroThatExpandsToMemberAttributesMacroAndWhichIsAlsoAnAccessorMacro
func foo() {}
} And the two And yes, I think with the removal of attributes from I hope that I’m making sense here. I think it would be good to have a test case for what I described above and make sure that we do actually diagnose that |
||||||
} else { | ||||||
self.diagnosticForUnexpandedMacro(attribute: attribute, macroType: spec.type, attachedTo: declSyntax) | ||||||
} | ||||||
} | ||||||
return AttributeRemover(removingWhere: { attributesToRemove.map(\.attributeNode).contains($0) }).rewrite( | ||||||
visitedNode | ||||||
) | ||||||
} | ||||||
|
||||||
return nil | ||||||
} | ||||||
|
||||||
private func diagnosticForUnexpandedMacro(attribute: AttributeSyntax, macroType: Macro.Type, attachedTo: DeclSyntax) { | ||||||
var diagnosticError: Error? = nil | ||||||
var fixitMessage: FixItMessage? = nil | ||||||
switch macroType { | ||||||
case is AccessorMacro.Type: | ||||||
if (!attachedTo.is(VariableDeclSyntax.self) && !attachedTo.is(SubscriptDeclSyntax.self)) { | ||||||
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.
Suggested change
|
||||||
diagnosticError = MacroApplicationError.macroAttachedToInvalidDecl( | ||||||
MacroRole.accessor.description, | ||||||
attachedTo.kind.nameForDiagnostics ?? "" | ||||||
) | ||||||
fixitMessage = SwiftSyntaxMacros.MacroExpansionFixItMessage( | ||||||
"Remove '\(attribute.trimmedDescription)'" | ||||||
) | ||||||
} | ||||||
default: | ||||||
break | ||||||
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 think your error message 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. If that's the case, I believe there's no need to diagnose based on the type of |
||||||
} | ||||||
if let diagnosticError, let fixitMessage { | ||||||
contextGenerator(Syntax(attachedTo)).addDiagnostics( | ||||||
from: diagnosticError, | ||||||
node: attachedTo, | ||||||
fixIts: [ | ||||||
FixIt( | ||||||
message: fixitMessage, | ||||||
changes: [.replace(oldNode: Syntax(attribute), newNode: Syntax(AttributeListSyntax()))] | ||||||
) | ||||||
] | ||||||
) | ||||||
} | ||||||
} | ||||||
|
||||||
/// Visit for both the body and preamble macros. | ||||||
func visitBodyAndPreambleMacros<Node: DeclSyntaxProtocol & WithOptionalCodeBlockSyntax>( | ||||||
_ node: Node | ||||||
|
@@ -1023,6 +1070,7 @@ extension MacroApplication { | |||||
macroAttribute.conformanceList | ||||||
) { | ||||||
result += expanded | ||||||
self.expandedAttributes.append(macroAttribute.attributeNode) | ||||||
} | ||||||
} catch { | ||||||
contextGenerator(Syntax(decl)).addDiagnostics(from: error, node: macroAttribute.attributeNode) | ||||||
|
@@ -1170,6 +1218,7 @@ extension MacroApplication { | |||||
from: newAccessors, | ||||||
indentationWidth: self.indentationWidth | ||||||
) | ||||||
self.expandedAttributes.append(macro.attributeNode) | ||||||
} | ||||||
} else if let newAccessors = try expandAccessorMacroWithoutExistingAccessors( | ||||||
definition: macro.definition, | ||||||
|
@@ -1192,6 +1241,7 @@ extension MacroApplication { | |||||
} else { | ||||||
newAccessorsBlock = newAccessors | ||||||
} | ||||||
self.expandedAttributes.append(macro.attributeNode) | ||||||
} | ||||||
} catch { | ||||||
contextGenerator(Syntax(storage)).addDiagnostics(from: error, node: macro.attributeNode) | ||||||
|
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.
Are these imports still necessary if you use
nameKindForDiagnostics
? Same for the dependency declaration in the package manifest.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.
Yes, it doesn't need any more.