Skip to content

Commit b3a3f1f

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 b3a3f1f

File tree

4 files changed

+149
-26
lines changed

4 files changed

+149
-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: 55 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,26 @@ 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+
let expansion = expandAccessors(of: node, existingAccessors: binding.accessorBlock)
707+
if expansion.accessors != binding.accessorBlock {
708+
if binding.initializer != nil, expansion.expandsGetSet {
709+
// The accessor block will have a leading space, but there will already be a
710+
// space between the variable and the to-be-removed initializer. Remove the
711+
// leading trivia on the accessor block so we don't double up.
712+
node.bindings[node.bindings.startIndex].accessorBlock = expansion.accessors?.with(\.leadingTrivia, [])
713+
node.bindings[node.bindings.startIndex].initializer = nil
714+
} else {
715+
node.bindings[node.bindings.startIndex].accessorBlock = expansion.accessors
716+
}
717+
}
718+
706719
return DeclSyntax(node)
707720
}
708721

709722
override func visit(_ node: SubscriptDeclSyntax) -> DeclSyntax {
710723
var node = super.visit(node).cast(SubscriptDeclSyntax.self)
711-
node.accessorBlock = expandAccessors(of: node, existingAccessors: node.accessorBlock)
724+
node.accessorBlock = expandAccessors(of: node, existingAccessors: node.accessorBlock).accessors
712725
return DeclSyntax(node)
713726
}
714727
}
@@ -869,14 +882,23 @@ extension MacroApplication {
869882
}
870883
}
871884

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

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

881903
for macro in accessorMacros {
882904
do {
@@ -894,6 +916,8 @@ extension MacroApplication {
894916
in: context,
895917
indentationWidth: indentationWidth
896918
) {
919+
checkExpansions(newAccessors)
920+
897921
// If existingAccessors is not `nil`, then we also set
898922
// `newAccessorBlock` above to a a non-nil value, so
899923
// `newAccessorsBlock` also isn’t `nil`.
@@ -902,31 +926,33 @@ extension MacroApplication {
902926
indentationWidth: self.indentationWidth
903927
)
904928
}
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(
929+
} else if let newAccessors = try expandAccessorMacroWithoutExistingAccessors(
930+
definition: macro.definition,
931+
attributeNode: macro.attributeNode,
932+
attachedTo: DeclSyntax(storage),
933+
in: context,
934+
indentationWidth: indentationWidth
935+
) {
936+
guard case .accessors(let accessorList) = newAccessors.accessors else {
937+
throw MacroApplicationError.malformedAccessor
938+
}
939+
940+
checkExpansions(accessorList)
941+
942+
if let oldBlock = newAccessorsBlock {
943+
newAccessorsBlock = oldBlock.addingAccessors(
920944
from: accessorList,
921945
indentationWidth: self.indentationWidth
922946
)
947+
} else {
948+
newAccessorsBlock = newAccessors
923949
}
924950
}
925951
} catch {
926952
context.addDiagnostics(from: error, node: macro.attributeNode)
927953
}
928954
}
929-
return newAccessorsBlock
955+
return (newAccessorsBlock, expandsGetSet)
930956
}
931957
}
932958

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

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)