Skip to content

Commit 7572558

Browse files
committed
feat: improve diagnostics
1 parent d870f06 commit 7572558

File tree

5 files changed

+84
-17
lines changed

5 files changed

+84
-17
lines changed

Package.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,10 @@ let package = Package(
186186

187187
.target(
188188
name: "SwiftSyntaxMacroExpansion",
189-
dependencies: ["SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacros", "SwiftDiagnostics", "SwiftOperators"],
189+
dependencies: [
190+
"SwiftSyntax", "SwiftSyntaxBuilder", "SwiftSyntaxMacros", "SwiftDiagnostics", "SwiftOperators",
191+
"SwiftParserDiagnostics",
192+
],
190193
exclude: ["CMakeLists.txt"]
191194
),
192195

Sources/SwiftParserDiagnostics/SyntaxExtensions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ extension Syntax {
8585
extension SyntaxProtocol {
8686
/// A name that can be used to describe this node's type in diagnostics or `nil` if there is no good name for this node.
8787
/// If `allowBlockNames` is `false`, ``CodeBlockSyntax`` and ``MemberDeclBlockSyntax`` are not considered to have a good name and will return `nil`.
88-
func nodeTypeNameForDiagnostics(allowBlockNames: Bool) -> String? {
88+
public func nodeTypeNameForDiagnostics(allowBlockNames: Bool) -> String? {
8989
let syntax = Syntax(self)
9090
if !allowBlockNames && (syntax.is(CodeBlockSyntax.self) || syntax.is(MemberBlockSyntax.self)) {
9191
return nil

Sources/SwiftSyntaxMacroExpansion/MacroExpansion.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import SwiftSyntax
2020
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros
2121
#endif
2222

23-
public enum MacroRole: Sendable {
23+
public enum MacroRole: String, Sendable {
2424
case expression
2525
case declaration
2626
case accessor
@@ -63,6 +63,7 @@ enum MacroExpansionError: Error, CustomStringConvertible {
6363
case noFreestandingMacroRoles(Macro.Type)
6464
case moreThanOneBodyMacro
6565
case preambleWithoutBody
66+
case noAttachedMacroRoles(Macro.Type)
6667

6768
var description: String {
6869
switch self {
@@ -92,6 +93,9 @@ enum MacroExpansionError: Error, CustomStringConvertible {
9293

9394
case .preambleWithoutBody:
9495
return "preamble macro cannot be applied to a function with no body"
96+
97+
case .noAttachedMacroRoles(let type):
98+
return "macro implementation type '\(type)' does not conform to any attached macro protocol"
9599
}
96100
}
97101
}
@@ -173,6 +177,19 @@ public func inferFreestandingMacroRole(definition: Macro.Type) throws -> MacroRo
173177
}
174178
}
175179

180+
public func inferAttachedMacroRole(definition: Macro.Type) throws -> MacroRole {
181+
switch definition {
182+
case is AccessorMacro.Type: return .accessor
183+
case is MemberAttributeMacro.Type: return .memberAttribute
184+
case is MemberMacro.Type: return .member
185+
case is PeerMacro.Type: return .peer
186+
case is ExtensionMacro.Type: return .extension
187+
188+
default:
189+
throw MacroExpansionError.noAttachedMacroRoles(definition)
190+
}
191+
}
192+
176193
@available(*, deprecated, message: "pass a macro role, please!")
177194
public func expandFreestandingMacro(
178195
definition: Macro.Type,

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ internal import SwiftOperators
1616
@_spi(MacroExpansion) internal import SwiftParser
1717
public import SwiftSyntax
1818
internal import SwiftSyntaxBuilder
19+
internal import SwiftParserDiagnostics
1920
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) public import SwiftSyntaxMacros
2021
#else
2122
import SwiftDiagnostics
@@ -24,6 +25,7 @@ import SwiftOperators
2425
import SwiftSyntax
2526
import SwiftSyntaxBuilder
2627
@_spi(MacroExpansion) @_spi(ExperimentalLanguageFeature) import SwiftSyntaxMacros
28+
import SwiftParserDiagnostics
2729
#endif
2830

2931
// MARK: - Public entry function
@@ -633,7 +635,7 @@ private enum MacroApplicationError: DiagnosticMessage, Error {
633635
case accessorMacroOnVariableWithMultipleBindings
634636
case peerMacroOnVariableWithMultipleBindings
635637
case malformedAccessor
636-
case accessorMacroNotOnVariableOrSubscript
638+
case macroAttachedToInvalidDecl(String, String)
637639

638640
var diagnosticID: MessageID {
639641
return MessageID(domain: diagnosticDomain, id: "\(self)")
@@ -651,8 +653,8 @@ private enum MacroApplicationError: DiagnosticMessage, Error {
651653
return """
652654
macro returned a malformed accessor. Accessors should start with an introducer like 'get' or 'set'.
653655
"""
654-
case .accessorMacroNotOnVariableOrSubscript:
655-
return "accessor macro can only be applied to a variable or subscript"
656+
case let .macroAttachedToInvalidDecl(macroType, declType):
657+
return "'\(macroType)' macro cannot be attached to \(declType)"
656658
}
657659
}
658660
}
@@ -722,18 +724,19 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
722724
let attributesToRemove = self.macroAttributes(attachedTo: visitedNode)
723725
attributesToRemove.forEach { (attribute, spec) in
724726
if let index = self.expandedAttributes.firstIndex(where: { expandedAttribute in
725-
expandedAttribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text == attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text
727+
expandedAttribute.position == attribute.position
726728
}) {
727729
self.expandedAttributes.remove(at: index)
728730
} else {
729-
if let _ = spec.type as? AccessorMacro.Type,
730-
!declSyntax.is(VariableDeclSyntax.self) && !declSyntax.is(SubscriptDeclSyntax.self)
731-
{
732-
contextGenerator(node).addDiagnostics(
733-
from: MacroApplicationError.accessorMacroNotOnVariableOrSubscript,
734-
node: declSyntax
735-
)
736-
}
731+
if let macroRole = try? inferAttachedMacroRole(definition: spec.type), isInvalidAttachedMacro(macroRole: macroRole, attachedTo: declSyntax) {
732+
contextGenerator(node).addDiagnostics(
733+
from: MacroApplicationError.macroAttachedToInvalidDecl(
734+
macroRole.rawValue,
735+
declSyntax.nodeTypeNameForDiagnostics(allowBlockNames: true) ?? ""
736+
),
737+
node: declSyntax
738+
)
739+
}
737740
}
738741
}
739742
return AttributeRemover(removingWhere: { attributesToRemove.map(\.attributeNode).contains($0) }).rewrite(
@@ -744,6 +747,20 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
744747
return nil
745748
}
746749

750+
private func isInvalidAttachedMacro(macroRole: MacroRole, attachedTo: DeclSyntax) -> Bool {
751+
switch macroRole {
752+
case .accessor:
753+
// Only var decls and subscripts have accessors
754+
if (!attachedTo.is(VariableDeclSyntax.self) && !attachedTo.is(SubscriptDeclSyntax.self)) {
755+
return true
756+
}
757+
break
758+
default:
759+
break
760+
}
761+
return false
762+
}
763+
747764
/// Visit for both the body and preamble macros.
748765
func visitBodyAndPreambleMacros<Node: DeclSyntaxProtocol & WithOptionalCodeBlockSyntax>(
749766
_ node: Node

Tests/SwiftSyntaxMacroExpansionTest/AccessorMacroTests.swift

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ final class AccessorMacroTests: XCTestCase {
412412
)
413413
}
414414

415-
func testAccessorOnStruct() {
415+
func testAccessorNotOnVariableOrSubscript() {
416416
struct TestMacro: AccessorMacro {
417417
static func expansion(
418418
of node: AttributeSyntax,
@@ -427,9 +427,39 @@ final class AccessorMacroTests: XCTestCase {
427427
"@Test struct Foo {}",
428428
expandedSource: "struct Foo {}",
429429
diagnostics: [
430-
DiagnosticSpec(message: "accessor macro can only be applied to a variable or subscript", line: 1, column: 1)
430+
DiagnosticSpec(message: "'accessor' macro cannot be attached to struct", line: 1, column: 1)
431431
],
432432
macros: ["Test": TestMacro.self]
433433
)
434+
435+
assertMacroExpansion(
436+
"@Test func Foo() {}",
437+
expandedSource: "func Foo() {}",
438+
diagnostics: [
439+
// The compiler will reject this with "'accessor' macro cannot be attached to global function"
440+
DiagnosticSpec(message: "'accessor' macro cannot be attached to function", line: 1, column: 1)
441+
],
442+
macros: ["Test": TestMacro.self]
443+
)
444+
445+
assertMacroExpansion(
446+
"""
447+
struct Foo {
448+
@Test
449+
func Bar() {}
450+
}
451+
""",
452+
expandedSource: """
453+
struct Foo {
454+
func Bar() {}
455+
}
456+
""",
457+
diagnostics: [
458+
// The compiler will reject this with "'accessor' macro cannot be attached to instance method"
459+
DiagnosticSpec(message: "'accessor' macro cannot be attached to function", line: 2, column: 3)
460+
],
461+
macros: ["Test": TestMacro.self],
462+
indentationWidth: indentationWidth
463+
)
434464
}
435465
}

0 commit comments

Comments
 (0)