diff --git a/Sources/_SwiftSyntaxMacros/AccessorDeclarationMacro.swift b/Sources/_SwiftSyntaxMacros/AccessorDeclarationMacro.swift new file mode 100644 index 00000000000..1daff746879 --- /dev/null +++ b/Sources/_SwiftSyntaxMacros/AccessorDeclarationMacro.swift @@ -0,0 +1,22 @@ +// +// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax + +/// Describes a macro that adds accessors to a given declaration. +public protocol AccessorDeclarationMacro: DeclarationMacro { + /// Expand a macro that's expressed as a custom attribute attached to + /// the given declaration. The result is a set of accessors for the + /// declaration. + static func expansion( + of node: CustomAttributeSyntax, + attachedTo declaration: DeclSyntax, + in context: inout MacroExpansionContext + ) throws -> [AccessorDeclSyntax] +} diff --git a/Sources/_SwiftSyntaxMacros/CMakeLists.txt b/Sources/_SwiftSyntaxMacros/CMakeLists.txt index 4b2d8e48fa2..6aacc9e2c19 100644 --- a/Sources/_SwiftSyntaxMacros/CMakeLists.txt +++ b/Sources/_SwiftSyntaxMacros/CMakeLists.txt @@ -7,6 +7,7 @@ # See http://swift.org/CONTRIBUTORS.txt for Swift project authors add_swift_host_library(_SwiftSyntaxMacros + AccessorDeclarationMacro.swift DeclarationMacro.swift ExpressionMacro.swift FreestandingDeclarationMacro.swift diff --git a/Sources/_SwiftSyntaxMacros/MacroSystem.swift b/Sources/_SwiftSyntaxMacros/MacroSystem.swift index 4835f43faaa..45406581d6f 100644 --- a/Sources/_SwiftSyntaxMacros/MacroSystem.swift +++ b/Sources/_SwiftSyntaxMacros/MacroSystem.swift @@ -106,7 +106,7 @@ class MacroApplication: SyntaxRewriter { return true } - return !(macro is PeerDeclarationMacro.Type || macro is MemberDeclarationMacro.Type) + return !(macro is PeerDeclarationMacro.Type || macro is MemberDeclarationMacro.Type || macro is AccessorDeclarationMacro.Type) } if newAttributes.isEmpty { @@ -254,6 +254,60 @@ class MacroApplication: SyntaxRewriter { override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax { return visit(declGroup: node) } + + // Properties + override func visit(_ node: VariableDeclSyntax) -> DeclSyntax { + let visitedNode = super.visit(node) + guard let visitedVarDecl = visitedNode.as(VariableDeclSyntax.self) else { + return visitedNode + } + + guard let binding = visitedVarDecl.bindings.first, + visitedVarDecl.bindings.count == 1 + else { + return DeclSyntax(node) + } + + var accessors: [AccessorDeclSyntax] = [] + + let accessorMacroAttributes = getMacroAttributes(attachedTo: DeclSyntax(node), ofType: AccessorDeclarationMacro.Type.self) + for (accessorAttr, accessorMacro) in accessorMacroAttributes { + do { + let newAccessors = try accessorMacro.expansion( + of: accessorAttr, + attachedTo: DeclSyntax(visitedNode), + in: &context + ) + + accessors.append(contentsOf: newAccessors) + } catch { + // FIXME: record the error + } + } + + if accessors.isEmpty { + return visitedNode + } + + return DeclSyntax( + visitedVarDecl.withBindings( + visitedVarDecl.bindings.replacing( + childAt: 0, + with: binding.withAccessor( + .accessors( + .init( + leftBrace: .leftBraceToken(leadingTrivia: .space), + accessors: .init(accessors), + rightBrace: .rightBraceToken(leadingTrivia: .newline) + ) + ) + ) + ) + ) + ) + } + + // Subscripts } extension MacroApplication { diff --git a/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift b/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift index f34383471f6..c2642cfa04d 100644 --- a/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift +++ b/Tests/SwiftSyntaxMacrosTest/MacroSystemTests.swift @@ -208,6 +208,75 @@ struct DefineBitwidthNumberedStructsMacro: FreestandingDeclarationMacro { } } +public struct PropertyWrapper {} + +extension PropertyWrapper: AccessorDeclarationMacro { + public static func expansion( + of node: CustomAttributeSyntax, + attachedTo declaration: DeclSyntax, + in context: inout MacroExpansionContext + ) throws -> [AccessorDeclSyntax] { + guard let varDecl = declaration.as(VariableDeclSyntax.self), + let binding = varDecl.bindings.first, + let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier, + binding.accessor == nil + else { + return [] + } + + return [ + """ + + get { + _\(identifier).wrappedValue + } + """, + """ + + set { + _\(identifier).wrappedValue = newValue + } + """, + ] + } +} + +extension PropertyWrapper: PeerDeclarationMacro { + public static func expansion( + of node: CustomAttributeSyntax, + attachedTo declaration: DeclSyntax, + in context: inout MacroExpansionContext + ) throws -> [SwiftSyntax.DeclSyntax] { + guard let varDecl = declaration.as(VariableDeclSyntax.self), + let binding = varDecl.bindings.first, + let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier, + let type = binding.typeAnnotation?.type, + binding.accessor == nil + else { + return [] + } + + guard let wrapperTypeNameExpr = node.argumentList?.first?.expression, + let stringLiteral = wrapperTypeNameExpr.as(StringLiteralExprSyntax.self), + stringLiteral.segments.count == 1, + case let .stringSegment(wrapperTypeNameSegment)? = stringLiteral.segments.first + else { + return [] + } + + let storageType: TypeSyntax = "\(wrapperTypeNameSegment.content)<\(type)>" + let storageName = "_\(identifier)" + + return [ + """ + + private var \(raw: storageName): \(storageType) + + """ + ] + } +} + public struct AddCompletionHandler: PeerDeclarationMacro { public static func expansion( of node: CustomAttributeSyntax, @@ -395,6 +464,7 @@ public let testMacros: [String: Macro.Type] = [ "stringify": StringifyMacro.self, "myError": ErrorMacro.self, "bitwidthNumberedStructs": DefineBitwidthNumberedStructsMacro.self, + "wrapProperty": PropertyWrapper.self, "addCompletionHandler": AddCompletionHandler.self, "addBackingStorage": AddBackingStorage.self, ] @@ -515,6 +585,28 @@ final class MacroSystemTests: XCTestCase { ) } + func testPropertyWrapper() { + AssertMacroExpansion( + macros: testMacros, + """ + @wrapProperty("MyWrapperType") + var x: Int + """, + """ + + var x: Int { + get { + _x.wrappedValue + } + set { + _x.wrappedValue = newValue + } + } + private var _x: MyWrapperType + """ + ) + } + func testAddCompletionHandler() { AssertMacroExpansion( macros: testMacros,