Skip to content
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

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,10 @@ let package = Package(

.target(
name: "SwiftSyntaxMacroExpansion",
dependencies: ["SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacros", "SwiftDiagnostics", "SwiftOperators"],
dependencies: [
"SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacros", "SwiftDiagnostics", "SwiftOperators",
"SwiftParserDiagnostics",
],
exclude: ["CMakeLists.txt"]
),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
#endif

extension SyntaxKind {
var nameForDiagnostics: String? {
public var nameForDiagnostics: String? {
switch self {
case .token:
return "token"
Expand Down
20 changes: 20 additions & 0 deletions Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ extension MacroRole {
case .body: return "BodyMacro"
}
}

var description: String {
switch self {
case .expression: return "expression"
case .declaration: return "declaration"
case .accessor: return "accessor"
case .memberAttribute: return "memberAttribute"
case .member: return "member"
case .peer: return "peer"
case .conformance: return "conformance"
case .codeItem: return "codeItem"
case .extension: return "extension"
case .preamble: return "preamble"
case .body: return "body"
}
}
}

/// Simple diagnostic message
Expand All @@ -63,6 +79,7 @@ enum MacroExpansionError: Error, CustomStringConvertible {
case noFreestandingMacroRoles(Macro.Type)
case moreThanOneBodyMacro
case preambleWithoutBody
case noAttachedMacroRoles(Macro.Type)

var description: String {
switch self {
Expand Down Expand Up @@ -92,6 +109,9 @@ enum MacroExpansionError: Error, CustomStringConvertible {

case .preambleWithoutBody:
return "preamble macro cannot be applied to a function with no body"

case .noAttachedMacroRoles(let type):
return "macro implementation type '\(type)' does not conform to any attached macro protocol"
}
}
}
Expand Down
56 changes: 53 additions & 3 deletions Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal import SwiftOperators
@_spi(MacroExpansion) internal import SwiftParser
public import SwiftSyntax
internal import SwiftSyntaxBuilder
internal import SwiftParserDiagnostics
Copy link
Member

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.

Copy link
Author

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.

@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) public import SwiftSyntaxMacros
#else
import SwiftDiagnostics
Expand All @@ -24,6 +25,7 @@ import SwiftOperators
import SwiftSyntax
import SwiftSyntaxBuilder
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros
import SwiftParserDiagnostics
#endif

// MARK: - Public entry function
Expand Down Expand Up @@ -633,6 +635,7 @@ private enum MacroApplicationError: DiagnosticMessage, Error {
case accessorMacroOnVariableWithMultipleBindings
case peerMacroOnVariableWithMultipleBindings
case malformedAccessor
case macroAttachedToInvalidDecl(String, String)
Copy link
Member

Choose a reason for hiding this comment

The 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)")
Expand All @@ -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)"
}
}
}
Expand All @@ -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] = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can be private.


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

  • Change this variable 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] = []
  • In visitAny set this variable to self.macroAttributes(attachedTo: declSyntax)
  • Whenever we expand a macro, remove it from the list (I think we might even do this based on the ID now and don’t need to resort to the position).
  • If there are macros left over in attributesToExpand, diagnose them.
  • After visitAny is done, restore attributesToExpand to the value it had before we assigned it to self.macroAttributes(attachedTo: declSyntax).

Do you think that would work?

Copy link
Author

@RayZhao1998 RayZhao1998 May 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we might even do this based on the ID now and don’t need to resort to the position

I found that the id is still different. The macroAttributes method depends on the attachedTo node. In visitAny the declSyntax's root is SourceFileSyntax, while in visit(for example visit(_ node: VariableDeclSyntax)) the node's root is VariableDeclSyntax(itself).

It seems that in visitChildren, the node will have a new layout and generate a new syntax without origin root info. Is this by design?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also delved into the implementation of AttributeSyntax.position. It's the position of the start of this node's leading trivia(not the root). For Attribute, it always represents its index in the AttributeList. So I don't think it's a proper way to compare two attributes by position

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @ahoppen , are there any updates on this?

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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 visit(_: VariableDeclSyntax) we needed to call macroAttributes(attachedTo:ofType:) on the original node, not the one that we already processed using super.visit.

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)

Copy link
Author

@RayZhao1998 RayZhao1998 Jun 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your help.

@@ -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)
     }

The changes of visit here are weird. When should we use originalNode and when to use the processed node?

Should we use originalNode when expandAccessor? If not, the comparison of attributedNode in expandAccessor will not be equal too.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Author

Choose a reason for hiding this comment

The 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,
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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 self.expandedAttributes low (which is a good reason on its own). Would be worth a comment though, I think.

Copy link
Author

@RayZhao1998 RayZhao1998 May 2, 2024

Choose a reason for hiding this comment

The 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

Copy link
Member

Choose a reason for hiding this comment

The 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 @MacroThatExpandsToMemberAttributesMacroAndWhichIsAlsoAnAccessorMacro have the same location despite being two different macro expansions.


And yes, I think with the removal of attributes from self.expandedAttributes, this should be fine because we always clear that list whenever removing attributes.

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 MacroThatExpandsToMemberAttributesMacroAndWhichIsAlsoAnAccessorMacro can’t be attached to func foo(). Also, feel free to pick a shorter name for MacroThatExpandsToMemberAttributesMacroAndWhichIsAlsoAnAccessorMacro. I only picked it to convey the meaning of the attribute without writing an implementation.

} 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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!attachedTo.is(VariableDeclSyntax.self) && !attachedTo.is(SubscriptDeclSyntax.self)) {
if !attachedTo.is(VariableDeclSyntax.self) && !attachedTo.is(SubscriptDeclSyntax.self) {

diagnosticError = MacroApplicationError.macroAttachedToInvalidDecl(
MacroRole.accessor.description,
attachedTo.kind.nameForDiagnostics ?? ""
)
fixitMessage = SwiftSyntaxMacros.MacroExpansionFixItMessage(
"Remove '\(attribute.trimmedDescription)'"
)
}
default:
break
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think your error message '\(macroType)' macro cannot be attached to \(declType) is sufficiently generic that we could apply it for all cases, not just the accessor macro attached to a non-variable and non-subscript case.

Copy link
Author

@RayZhao1998 RayZhao1998 May 4, 2024

Choose a reason for hiding this comment

The 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 macroType or the type of DeclSyntax. A unified diagnosis with message '\(macroType)' macro cannot be attached to \(declType) will suffice.

}
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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand All @@ -1192,6 +1241,7 @@ extension MacroApplication {
} else {
newAccessorsBlock = newAccessors
}
self.expandedAttributes.append(macro.attributeNode)
}
} catch {
contextGenerator(Syntax(storage)).addDiagnostics(from: error, node: macro.attributeNode)
Expand Down
15 changes: 11 additions & 4 deletions Sources/SwiftSyntaxMacros/MacroExpansionContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,28 @@ private struct ThrownErrorDiagnostic: DiagnosticMessage {

extension MacroExpansionContext {
/// Adds diagnostics from the error thrown during a macro expansion.
public func addDiagnostics(from error: Error, node: some SyntaxProtocol) {
public func addDiagnostics(from error: Error, node: some SyntaxProtocol, fixIts: [FixIt] = []) {
// Inspect the error to form an appropriate set of diagnostics.
var diagnostics: [Diagnostic]
if let diagnosticsError = error as? DiagnosticsError {
diagnostics = diagnosticsError.diagnostics
} else if let message = error as? DiagnosticMessage {
diagnostics = [Diagnostic(node: Syntax(node), message: message)]
diagnostics = [Diagnostic(node: Syntax(node), message: message, fixIts: fixIts)]
} else if let error = error as? SyntaxStringInterpolationInvalidNodeTypeError {
let diagnostic = Diagnostic(
node: Syntax(node),
message: ThrownErrorDiagnostic(message: "Internal macro error: \(error.description)")
message: ThrownErrorDiagnostic(message: "Internal macro error: \(error.description)"),
fixIts: fixIts
)
diagnostics = [diagnostic]
} else {
diagnostics = [Diagnostic(node: Syntax(node), message: ThrownErrorDiagnostic(message: String(describing: error)))]
diagnostics = [
Diagnostic(
node: Syntax(node),
message: ThrownErrorDiagnostic(message: String(describing: error)),
fixIts: fixIts
)
]
}

// Emit the diagnostics.
Expand Down
66 changes: 66 additions & 0 deletions Tests/SwiftSyntaxMacroExpansionTest/AccessorMacroTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -411,4 +411,70 @@ final class AccessorMacroTests: XCTestCase {
indentationWidth: indentationWidth
)
}

func testAccessorNotOnVariableOrSubscript() {
struct TestMacro: AccessorMacro {
static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
return []
}
}

assertMacroExpansion(
"@Test struct Foo {}",
expandedSource: "struct Foo {}",
diagnostics: [
DiagnosticSpec(
message: "'accessor' macro cannot be attached to struct",
line: 1,
column: 1,
fixIts: [FixItSpec(message: "Remove '@Test'")]
)
],
macros: ["Test": TestMacro.self]
)

assertMacroExpansion(
"@Test func Foo() {}",
expandedSource: "func Foo() {}",
diagnostics: [
// The compiler will reject this with "'accessor' macro cannot be attached to global function"
DiagnosticSpec(
message: "'accessor' macro cannot be attached to function",
line: 1,
column: 1,
fixIts: [FixItSpec(message: "Remove '@Test'")]
)
],
macros: ["Test": TestMacro.self]
)

assertMacroExpansion(
"""
struct Foo {
@Test
func Bar() {}
}
""",
expandedSource: """
struct Foo {
func Bar() {}
}
""",
diagnostics: [
// The compiler will reject this with "'accessor' macro cannot be attached to instance method"
DiagnosticSpec(
message: "'accessor' macro cannot be attached to function",
line: 2,
column: 3,
fixIts: [FixItSpec(message: "Remove '@Test'")]
)
],
macros: ["Test": TestMacro.self],
indentationWidth: indentationWidth
)
}
}