Skip to content

Commit bc10e79

Browse files
committed
MacroSystem should remove the initializer for an accessor macro
SE-0389 specifies that a macro returning either a getter or setter should remove the initializer, if one exists. Resolves rdar://117442713 (#2310)
1 parent ebd7026 commit bc10e79

File tree

4 files changed

+148
-26
lines changed

4 files changed

+148
-26
lines changed

Examples/Tests/MacroExamples/Implementation/ComplexMacros/DictionaryIndirectionMacroTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,15 @@ final class DictionaryStorageMacroTests: XCTestCase {
3232
""",
3333
expandedSource: """
3434
struct Point {
35-
var x: Int = 1 {
35+
var x: Int {
3636
get {
3737
_storage["x", default: 1] as! Int
3838
}
3939
set {
4040
_storage["x"] = newValue
4141
}
4242
}
43-
var y: Int = 2 {
43+
var y: Int {
4444
get {
4545
_storage["y", default: 2] as! Int
4646
}

Examples/Tests/MacroExamples/Implementation/ComplexMacros/ObservableMacroTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ final class ObservableMacroTests: XCTestCase {
7777
}
7878
}
7979
80-
var isHappy: Bool = true {
80+
var isHappy: Bool {
8181
get {
8282
_registrar.beginAccess(\.isHappy)
8383
defer {

Sources/SwiftSyntaxMacroExpansion/MacroSystem.swift

Lines changed: 54 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,7 @@ private func expandAccessorMacroWithExistingAccessors(
307307
return nil
308308
}
309309

310-
// Separate the accessor from any existing accessors by two spaces
310+
// Separate the accessor from any existing accessors by an empty line
311311
let indentedSource = "\n" + expanded.indented(by: attachedTo.indentationOfFirstLine + indentationWidth)
312312
return "\(raw: indentedSource)"
313313
}
@@ -702,13 +702,27 @@ private class MacroApplication<Context: MacroExpansionContext>: SyntaxRewriter {
702702
context.addDiagnostics(from: MacroApplicationError.accessorMacroOnVariableWithMultipleBindings, node: node)
703703
return DeclSyntax(node)
704704
}
705-
node.bindings[node.bindings.startIndex].accessorBlock = expandAccessors(of: node, existingAccessors: binding.accessorBlock)
705+
706+
707+
let expansion = expandAccessors(of: node, existingAccessors: binding.accessorBlock)
708+
if expansion.accessors != binding.accessorBlock {
709+
if binding.initializer != nil, expansion.expandsGetSet {
710+
// The accessor block will have a leading space, but there will already be a
711+
// space between the variable and the to-be-removed initializer. Remove the
712+
// leading trivia on the accessor block so we don't double up.
713+
node.bindings[node.bindings.startIndex].accessorBlock = expansion.accessors?.with(\.leadingTrivia, [])
714+
node.bindings[node.bindings.startIndex].initializer = nil
715+
} else {
716+
node.bindings[node.bindings.startIndex].accessorBlock = expansion.accessors
717+
}
718+
}
719+
706720
return DeclSyntax(node)
707721
}
708722

709723
override func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax {
710724
var node = super.visit(node).cast(SubscriptDeclSyntax.self)
711-
node.accessorBlock = expandAccessors(of: node, existingAccessors: node.accessorBlock)
725+
node.accessorBlock = expandAccessors(of: node, existingAccessors: node.accessorBlock).accessors
712726
return DeclSyntax(node)
713727
}
714728
}
@@ -869,14 +883,21 @@ extension MacroApplication {
869883
}
870884
}
871885

872-
/// Expand all 'accessor' macros attached to `storage` and return the `storage`
873-
/// node.
886+
/// Expand all 'accessor' macros attached to `storage`.
874887
///
875-
/// - Returns: The storage node with all macro-synthesized accessors applied.
876-
private func expandAccessors(of storage: some DeclSyntaxProtocol, existingAccessors: AccessorBlockSyntax?) -> AccessorBlockSyntax? {
888+
/// - Returns: The final accessors block that includes both the existing
889+
/// and expanded accessors, as well as whether any `get`/`set` were
890+
/// expanded (in which case any initializer on `storage` should be
891+
/// removed).
892+
private func expandAccessors(of storage: some DeclSyntaxProtocol, existingAccessors: AccessorBlockSyntax?) -> (accessors: AccessorBlockSyntax?, expandsGetSet: Bool) {
877893
let accessorMacros = macroAttributes(attachedTo: DeclSyntax(storage), ofType: AccessorMacro.Type.self)
878894

879895
var newAccessorsBlock = existingAccessors
896+
var expandsGetSet = false
897+
func checkExpansions(_ accessors: AccessorDeclListSyntax?) {
898+
guard let accessors else { return }
899+
expandsGetSet = expandsGetSet || accessors.contains(where: \.isGetOrSet)
900+
}
880901

881902
for macro in accessorMacros {
882903
do {
@@ -894,6 +915,8 @@ extension MacroApplication {
894915
in: context,
895916
indentationWidth: indentationWidth
896917
) {
918+
checkExpansions(newAccessors)
919+
897920
// If existingAccessors is not `nil`, then we also set
898921
// `newAccessorBlock` above to a a non-nil value, so
899922
// `newAccessorsBlock` also isn’t `nil`.
@@ -902,31 +925,33 @@ extension MacroApplication {
902925
indentationWidth: self.indentationWidth
903926
)
904927
}
905-
} else {
906-
let newAccessors = try expandAccessorMacroWithoutExistingAccessors(
907-
definition: macro.definition,
908-
attributeNode: macro.attributeNode,
909-
attachedTo: DeclSyntax(storage),
910-
in: context,
911-
indentationWidth: indentationWidth
912-
)
913-
if newAccessorsBlock == nil {
914-
newAccessorsBlock = newAccessors
915-
} else if let newAccessors = newAccessors {
916-
guard case .accessors(let accessorList) = newAccessors.accessors else {
917-
throw MacroApplicationError.malformedAccessor
918-
}
919-
newAccessorsBlock = newAccessorsBlock!.addingAccessors(
928+
} else if let newAccessors = try expandAccessorMacroWithoutExistingAccessors(
929+
definition: macro.definition,
930+
attributeNode: macro.attributeNode,
931+
attachedTo: DeclSyntax(storage),
932+
in: context,
933+
indentationWidth: indentationWidth
934+
) {
935+
guard case .accessors(let accessorList) = newAccessors.accessors else {
936+
throw MacroApplicationError.malformedAccessor
937+
}
938+
939+
checkExpansions(accessorList)
940+
941+
if let oldBlock = newAccessorsBlock {
942+
newAccessorsBlock = oldBlock.addingAccessors(
920943
from: accessorList,
921944
indentationWidth: self.indentationWidth
922945
)
946+
} else {
947+
newAccessorsBlock = newAccessors
923948
}
924949
}
925950
} catch {
926951
context.addDiagnostics(from: error, node: macro.attributeNode)
927952
}
928953
}
929-
return newAccessorsBlock
954+
return (newAccessorsBlock, expandsGetSet)
930955
}
931956
}
932957

@@ -1130,3 +1155,9 @@ private extension AttributeSyntax {
11301155
return (detach(in: context, foldingWith: operatorTable) as Syntax).cast(Self.self)
11311156
}
11321157
}
1158+
1159+
private extension AccessorDeclSyntax {
1160+
var isGetOrSet: Bool {
1161+
return accessorSpecifier.tokenKind == .keyword(.get) || accessorSpecifier.tokenKind == .keyword(.set)
1162+
}
1163+
}

Tests/SwiftSyntaxMacroExpansionTest/AccessorMacroTests.swift

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,4 +320,95 @@ final class AccessorMacroTests: XCTestCase {
320320
macros: ["Test": TestMacro.self]
321321
)
322322
}
323+
324+
func testInitializerRemovedForGetSet() {
325+
assertMacroExpansion(
326+
"""
327+
@constantOne
328+
var x: Int = 1
329+
""",
330+
expandedSource: """
331+
var x: Int {
332+
get {
333+
return 1
334+
}
335+
}
336+
""",
337+
macros: ["constantOne": ConstantOneGetter.self],
338+
indentationWidth: indentationWidth
339+
)
340+
341+
// Bit of an odd case, compiler has the type but we don't know it in `MacroSystem`
342+
assertMacroExpansion(
343+
"""
344+
@constantOne
345+
var x = 1
346+
""",
347+
expandedSource: """
348+
var x {
349+
get {
350+
return 1
351+
}
352+
}
353+
""",
354+
macros: ["constantOne": ConstantOneGetter.self],
355+
indentationWidth: indentationWidth
356+
)
357+
}
358+
359+
func testInitializerRemainsForObserver() {
360+
struct DidSetAdder: AccessorMacro {
361+
static func expansion(
362+
of node: AttributeSyntax,
363+
providingAccessorsOf declaration: some DeclSyntaxProtocol,
364+
in context: some MacroExpansionContext
365+
) throws -> [AccessorDeclSyntax] {
366+
return [
367+
"""
368+
didSet {
369+
}
370+
"""
371+
]
372+
}
373+
}
374+
375+
assertMacroExpansion(
376+
"""
377+
@addDidSet
378+
var x = 1
379+
""",
380+
expandedSource: """
381+
var x = 1 {
382+
didSet {
383+
}
384+
}
385+
""",
386+
macros: ["addDidSet": DidSetAdder.self],
387+
indentationWidth: indentationWidth
388+
)
389+
390+
// Invalid semantically, but we shouldn't remove the initializer as the
391+
// macro did not produce a getter/setter
392+
assertMacroExpansion(
393+
"""
394+
@addDidSet
395+
var x = 1 {
396+
get {
397+
return 1
398+
}
399+
}
400+
""",
401+
expandedSource: """
402+
var x = 1 {
403+
get {
404+
return 1
405+
}
406+
didSet {
407+
}
408+
}
409+
""",
410+
macros: ["addDidSet": DidSetAdder.self],
411+
indentationWidth: indentationWidth
412+
)
413+
}
323414
}

0 commit comments

Comments
 (0)