diff --git a/Package.swift b/Package.swift index ed54793..09878a6 100644 --- a/Package.swift +++ b/Package.swift @@ -12,10 +12,6 @@ let package = Package( name: "PropertyWrapperMacro", targets: ["PropertyWrapperMacro"] ), - .library( - name: "PropertyWrapperMacro", - targets: ["PropertyWrapperMacro"] - ), ], dependencies: [ .package(url: "https://github.com/swiftlang/swift-syntax.git", "600.0.0" ..< "700.0.0"), @@ -24,6 +20,7 @@ let package = Package( .target( name: "PropertyWrapperMacro", dependencies: [ + "MacrofyModels", .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), @@ -42,17 +39,23 @@ let package = Package( .product(name: "SwiftDiagnostics", package: "swift-syntax"), ] ), + .target( + name: "MacrofyModels", + dependencies: [ + .product(name: "SwiftSyntax", package: "swift-syntax"), + ] + ), .target( name: "Macrofy", dependencies: [ "MacrofyMacro", + "MacrofyModels", "PropertyWrapperMacro", ] ), .macro( name: "ExampleMacros", dependencies: [ - "PropertyWrapperMacro", "Macrofy", .product(name: "SwiftSyntax", package: "swift-syntax"), .product(name: "SwiftSyntaxBuilder", package: "swift-syntax"), @@ -72,7 +75,6 @@ let package = Package( dependencies: [ "Macrofy", "MacrofyMacro", - "PropertyWrapperMacro", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] ), @@ -82,9 +84,18 @@ let package = Package( "Examples", "ExampleMacros", "PropertyWrapperMacro", + "MacrofyModels", .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), ] ), + .testTarget( + name: "MacrofyModelsTests", + dependencies: [ + "MacrofyModels", + .product(name: "SwiftSyntax", package: "swift-syntax"), + .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), + ] + ), ], swiftLanguageModes: [.v6] ) diff --git a/Sources/ExampleMacros/ExampleMacrosPlugin.swift b/Sources/ExampleMacros/ExampleMacrosPlugin.swift index 7269a2f..536c9bb 100644 --- a/Sources/ExampleMacros/ExampleMacrosPlugin.swift +++ b/Sources/ExampleMacros/ExampleMacrosPlugin.swift @@ -1,5 +1,5 @@ // -// MacrofyPlugin.swift +// ExampleMacrosPlugin.swift // Macrofy // // Created by Annalise Mariottini on 9/19/25. @@ -15,6 +15,7 @@ struct ExampleMacrosPlugin: CompilerPlugin { ExampleSettableMacro.self, ExampleWithProjectedMacro.self, ExampleWithSettableProjectedMacro.self, + ExampleWithGenericProjectedMacro.self, ExampleWithWrappedValueMacro.self, ] } diff --git a/Sources/ExampleMacros/ExamplePropertyWrapperMacros.swift b/Sources/ExampleMacros/ExamplePropertyWrapperMacros.swift index 93d3774..9cec65d 100644 --- a/Sources/ExampleMacros/ExamplePropertyWrapperMacros.swift +++ b/Sources/ExampleMacros/ExamplePropertyWrapperMacros.swift @@ -53,6 +53,17 @@ public final class ExampleWithSettableProjected: @un public lazy var projectedValue: Int = wrappedValue.hashValue } +@macrofy +@propertyWrapper +public final class ExampleWithGenericProjected: @unchecked Sendable { + public init(_ wrappedValue: Value) { + self.wrappedValue = wrappedValue + } + + public let wrappedValue: Value + public var projectedValue: Result { Result { wrappedValue } } +} + @macrofy @propertyWrapper public struct ExampleWithWrappedValue: Sendable { diff --git a/Sources/Examples/ExamplePropertyWrapperMacro.swift b/Sources/Examples/ExamplePropertyWrapperMacro.swift index 66547cd..b00e881 100644 --- a/Sources/Examples/ExamplePropertyWrapperMacro.swift +++ b/Sources/Examples/ExamplePropertyWrapperMacro.swift @@ -31,6 +31,12 @@ public macro ExampleWithSettableProjected( _ arguments: Any... ) = #externalMacro(module: "ExampleMacros", type: "ExampleWithSettableProjectedMacro") +@attached(peer, names: prefixed(_), prefixed(`$`)) +@attached(accessor, names: named(get), named(set)) +public macro ExampleWithGenericProjected( + _ arguments: Any... +) = #externalMacro(module: "ExampleMacros", type: "ExampleWithGenericProjectedMacro") + @attached(peer, names: prefixed(_), prefixed(`$`)) @attached(accessor, names: named(get), named(set)) public macro ExampleWithWrappedValue( diff --git a/Sources/Macrofy/Macrofy.swift b/Sources/Macrofy/Macrofy.swift index fdc004d..db2f473 100644 --- a/Sources/Macrofy/Macrofy.swift +++ b/Sources/Macrofy/Macrofy.swift @@ -6,6 +6,7 @@ // import Foundation +@_exported import MacrofyModels @_exported import PropertyWrapperMacro /// Automatically generates a property wrapper macro from a property wrapper type. diff --git a/Sources/MacrofyMacro/MacrofyMacro.swift b/Sources/MacrofyMacro/MacrofyMacro.swift index 2652e34..b28b623 100644 --- a/Sources/MacrofyMacro/MacrofyMacro.swift +++ b/Sources/MacrofyMacro/MacrofyMacro.swift @@ -50,14 +50,14 @@ import SwiftSyntaxMacros /// ``` public struct MacrofyMacro: PeerMacro { public static func expansion(of node: AttributeSyntax, - providingPeersOf declaration: some DeclSyntaxProtocol, + providingPeersOf declSyntax: some DeclSyntaxProtocol, in context: some MacroExpansionContext) throws -> [DeclSyntax] { func diagnose(_ diagnostic: MacrofyMacroDiagnostic) -> [DeclSyntax] { context.diagnose(Diagnostic(node: node, message: diagnostic)) return [] } - guard let declaration = Declaration(declaration) else { + guard let declaration = Declaration(declSyntax) else { return diagnose(.unsupportedDeclarationType) } @@ -72,50 +72,54 @@ public struct MacrofyMacro: PeerMacro { let projectedValue: (variableDecl: VariableDeclSyntax, binding: PatternBindingSyntax)? = members.firstVariable(withName: "projectedValue") let wrappedValueIsSettable = variableIsSettable(variableDecl: wrappedValue.variableDecl, binding: wrappedValue.binding) + let projectedValueIsSettable: Bool? + let projectedValueTypeResolver: ExprSyntax? if let projectedValue { projectedValueIsSettable = variableIsSettable(variableDecl: projectedValue.variableDecl, binding: projectedValue.binding) + projectedValueTypeResolver = self.projectedValueTypeResolver(of: node, wrappedValue: wrappedValue, projectedValue: projectedValue, declaration: declaration, in: context) } else { projectedValueIsSettable = nil + projectedValueTypeResolver = nil } let configMembers = try MemberBlockItemListSyntax { DeclSyntax(""" - + public init() { } - + """) - if let projectedValue { - if let projectedValueType = projectedValue.binding.typeAnnotation { - try FunctionDeclSyntax(""" - - public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "\(projectedValueType.type.trimmed)" } - - """) + if let projectedValueTypeResolver { + try FunctionDeclSyntax(""" + + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { + \(projectedValueTypeResolver) } + + """) // TODO: handle when projected type annotation is missing // TODO: handle projected type with generics } if declaration.isReferenceType { try VariableDeclSyntax(""" - + public let isReferenceType = true - + """) } if wrappedValueIsSettable { try VariableDeclSyntax(""" - + public let wrappedValueIsSettable = true - + """) } if projectedValueIsSettable == true { try VariableDeclSyntax(""" - + public let projectedValueIsSettable = true - + """) } } @@ -129,7 +133,7 @@ public struct MacrofyMacro: PeerMacro { public struct \(declaration.name.trimmed)Macro: PropertyWrapperMacro { \(config) } - """ + """, ] } @@ -143,7 +147,7 @@ public struct MacrofyMacro: PeerMacro { // mutable var return true } - guard case .accessors(let accessors) = accessorBlock.accessors else { + guard case let .accessors(accessors) = accessorBlock.accessors else { // var with only getter return false } @@ -152,6 +156,44 @@ public struct MacrofyMacro: PeerMacro { }) return containsSetter } + + private static func projectedValueTypeResolver(of node: AttributeSyntax, + wrappedValue: (variableDecl: VariableDeclSyntax, binding: PatternBindingSyntax), + projectedValue: (variableDecl: VariableDeclSyntax, binding: PatternBindingSyntax), + declaration: Declaration, + in context: some MacroExpansionContext) -> ExprSyntax? + { + func diagnose(_ diagnostic: MacrofyMacroDiagnostic) -> ExprSyntax? { + context.diagnose(Diagnostic(node: node, message: diagnostic)) + return nil + } + + guard let typeAnnotation = projectedValue.binding.typeAnnotation else { + return diagnose(.missingProjectedValueType) + } + lazy var typeAsString: ExprSyntax = "\"\(typeAnnotation.type.trimmed)\"" + + guard declaration.genericParameterClause != nil else { + return typeAsString + } + + // our property wrapper type has generics + // to determine the projected value type, we need to use the helper function to evaluate the types + // the helper function is defined in PropertyWrapperMacro.swift + return """ + projectedValueType( + of: node, + originalWrappedValue: #\""" + \(wrappedValue.variableDecl.trimmed) + \"""#, + originalProjectedValue: #\""" + \(projectedValue.variableDecl.trimmed) + \"""#, + providingAccessorsOf: declaration, + in: context + ) + """ + } } private enum Declaration { @@ -179,43 +221,56 @@ private enum Declaration { var declSyntax: any DeclSyntaxProtocol { switch self { - case .struct(let structDeclSyntax): + case let .struct(structDeclSyntax): return structDeclSyntax - case .class(let classDeclSyntax): + case let .class(classDeclSyntax): return classDeclSyntax - case .actor(let actorDeclSyntax): + case let .actor(actorDeclSyntax): return actorDeclSyntax - case .enum(let enumDeclSyntax): + case let .enum(enumDeclSyntax): return enumDeclSyntax } } var memberBlock: MemberBlockSyntax { switch self { - case .struct(let structDeclSyntax): + case let .struct(structDeclSyntax): return structDeclSyntax.memberBlock - case .class(let classDeclSyntax): + case let .class(classDeclSyntax): return classDeclSyntax.memberBlock - case .actor(let actorDeclSyntax): + case let .actor(actorDeclSyntax): return actorDeclSyntax.memberBlock - case .enum(let enumDeclSyntax): + case let .enum(enumDeclSyntax): return enumDeclSyntax.memberBlock } } var name: TokenSyntax { switch self { - case .struct(let structDeclSyntax): + case let .struct(structDeclSyntax): return structDeclSyntax.name - case .class(let classDeclSyntax): + case let .class(classDeclSyntax): return classDeclSyntax.name - case .actor(let actorDeclSyntax): + case let .actor(actorDeclSyntax): return actorDeclSyntax.name - case .enum(let enumDeclSyntax): + case let .enum(enumDeclSyntax): return enumDeclSyntax.name } } + var genericParameterClause: GenericParameterClauseSyntax? { + switch self { + case let .struct(structDeclSyntax): + return structDeclSyntax.genericParameterClause + case let .class(classDeclSyntax): + return classDeclSyntax.genericParameterClause + case let .actor(actorDeclSyntax): + return actorDeclSyntax.genericParameterClause + case let .enum(enumDeclSyntax): + return enumDeclSyntax.genericParameterClause + } + } + var isReferenceType: Bool { switch self { case .class, .actor: @@ -232,12 +287,18 @@ private extension MemberBlockItemListSyntax { $0.decl.as(VariableDeclSyntax.self) } .compactMap { variableDecl -> (VariableDeclSyntax, PatternBindingSyntax)? in - let wrappedValueBinding = variableDecl.bindings.first(where: { - $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == name - }) - guard let wrappedValueBinding else { return nil } - return (variableDecl, wrappedValueBinding) + let binding = variableDecl.firstPatternBinding(withName: name) + guard let binding else { return nil } + return (variableDecl, binding) } .first } } + +private extension VariableDeclSyntax { + func firstPatternBinding(withName name: String) -> PatternBindingSyntax? { + bindings.first(where: { + $0.pattern.as(IdentifierPatternSyntax.self)?.identifier.text == name + }) + } +} diff --git a/Sources/MacrofyMacro/MacrofyMacroDiagnostic.swift b/Sources/MacrofyMacro/MacrofyMacroDiagnostic.swift index d5886ee..6859bb0 100644 --- a/Sources/MacrofyMacro/MacrofyMacroDiagnostic.swift +++ b/Sources/MacrofyMacro/MacrofyMacroDiagnostic.swift @@ -13,11 +13,13 @@ enum MacrofyMacroDiagnostic: DiagnosticMessage { case unsupportedDeclarationType case missingWrappedValue + case missingProjectedValueType var rawValue: String { switch self { case .unsupportedDeclarationType: return "unsupported_declaration" case .missingWrappedValue: return "missing_wrapped_value" + case .missingProjectedValueType: return "missing_projected_value_type" } } @@ -25,6 +27,7 @@ enum MacrofyMacroDiagnostic: DiagnosticMessage { switch self { case .unsupportedDeclarationType: return "The \(Self.macroName) macro can only be used on a struct, class, actor, or enum." case .missingWrappedValue: return "A property wrapper must have a wrappedValue member." + case .missingProjectedValueType: return "A property wrapper with a projectedValue must have an explicit type" } } @@ -36,6 +39,7 @@ enum MacrofyMacroDiagnostic: DiagnosticMessage { switch self { case .unsupportedDeclarationType: return .error case .missingWrappedValue: return .error + case .missingProjectedValueType: return .error } } } diff --git a/Sources/MacrofyModels/TypeTree.swift b/Sources/MacrofyModels/TypeTree.swift new file mode 100644 index 0000000..1343367 --- /dev/null +++ b/Sources/MacrofyModels/TypeTree.swift @@ -0,0 +1,90 @@ +// +// TypeTree.swift +// Macrofy +// +// Created by Annalise Mariottini on 9/26/25. +// + +import SwiftSyntax + +public struct TypeTree { + public init(type: String, children: [TypeTree] = []) { + self.type = type + self.children = children + } + + public let type: String + public let children: [TypeTree] + + public func adding(children: [TypeTree]) -> Self { + TypeTree(type: type, children: children) + } +} + +public extension TypeTree { + init(_ identifierTypeSyntax: IdentifierTypeSyntax) { + let children: [TypeTree] + if let genericArgumentClause = identifierTypeSyntax.genericArgumentClause { + children = genericArgumentClause.arguments.compactMap { $0.argument.as(IdentifierTypeSyntax.self) }.map(Self.init) + } else { + children = [] + } + self.init(type: identifierTypeSyntax.name.text, children: children) + } +} + +extension TypeTree: Sequence { + public typealias Element = TypeTree + + public func makeIterator() -> Array.Iterator { + ([self] + children).makeIterator() + } +} + +extension TypeTree { + public var typeSyntax: TypeSyntax { + TypeSyntax(IdentifierTypeSyntax(name: .identifier(type), genericArgumentClause: genericArgumentClause)) + } + + private var genericArgumentClause: GenericArgumentClauseSyntax? { + guard !children.isEmpty else { + return nil + } + let childrenCount = children.count + return GenericArgumentClauseSyntax(arguments: GenericArgumentListSyntax(children.enumerated().map { + GenericArgumentSyntax(argument: .type(TypeSyntax(IdentifierTypeSyntax(name: .identifier($1.type)))), + trailingComma: $0 < childrenCount - 1 ? .commaToken() : nil) + })) + } +} + +extension TypeTree: CustomStringConvertible { + public var description: String { + typeSyntax.description + } +} + +public extension TypeTree { + func replacing(type: String, with newType: String) -> Self { + let typeTree: Self + if self.type == type { + typeTree = TypeTree(type: newType) + } else { + typeTree = self + } + let replacedChildren = children.map { $0.replacing(type: type, with: newType) } + return typeTree.adding(children: replacedChildren) + } +} + +extension TypeTree: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + guard lhs.type == rhs.type else { + return false + } + guard lhs.children.count == rhs.children.count else { + return false + } + return zip(lhs.children, rhs.children).allSatisfy { $0 == $1 } + } +} diff --git a/Sources/PropertyWrapperMacro/PropertyWrapperMacro.swift b/Sources/PropertyWrapperMacro/PropertyWrapperMacro.swift index 806835b..2569bce 100644 --- a/Sources/PropertyWrapperMacro/PropertyWrapperMacro.swift +++ b/Sources/PropertyWrapperMacro/PropertyWrapperMacro.swift @@ -6,11 +6,14 @@ // import Foundation +import MacrofyModels import SwiftDiagnostics import SwiftSyntax import SwiftSyntaxBuilder import SwiftSyntaxMacros +// MARK: Config + /// Configuration protocol for ``PropertyWrapperMacro`` implementations. /// /// This protocol defines the configuration interface for property wrapper macros, @@ -100,6 +103,8 @@ public struct DefaultPropertyWrapperMacroConfig: PropertyWrapperMacroConfig { public init() {} } +// MARK: Macro + /// Protocol for property wrapper macros. /// /// This protocol combines `AccessorMacro` and `PeerMacro` to provide complete @@ -215,7 +220,7 @@ public extension PropertyWrapperMacro { ) throws -> [DeclSyntax] { func diagnose(_ diagnostic: PropertyWrapperMacroDiagnostic) -> [DeclSyntax] { context.diagnose(Diagnostic(node: node, message: diagnostic)) - return ["get { fatalError() }"] + return [] } guard let variableDecl = declaration.as(VariableDeclSyntax.self) else { @@ -228,16 +233,16 @@ public extension PropertyWrapperMacro { return diagnose(.unexpectedTypeDeclaration) } - var callExprArgs: [ExprSyntax] = [] + var callExprArgs: [String] = [] if let initializer = binding.initializer { callExprArgs.append("wrappedValue: \(initializer.value)") } if let arguments = node.arguments { callExprArgs.append("\(arguments)") } - let callExpr: ExprSyntax = "(\(callExprArgs.joined(separator: ",")))" + let callExpr: ExprSyntax = "(\(raw: callExprArgs.joined(separator: ",")))" - let letOrVar: ExprSyntax = (!config.isReferenceType && (config.wrappedValueIsSettable || config.projectedValueIsSettable)) ? "var" : "let" + let letOrVar: TokenSyntax = (!config.isReferenceType && (config.wrappedValueIsSettable || config.projectedValueIsSettable)) ? "var" : "let" let propertyWrapperType = config.propertyWrapperType(of: node, providingAccessorsOf: declaration, in: context) var declSyntax: [DeclSyntax] = [ "private \(letOrVar) _\(identifier) = \(propertyWrapperType)\(callExpr)", @@ -257,11 +262,85 @@ public extension PropertyWrapperMacro { } } -private extension [ExprSyntax] { - func joined(separator: ExprSyntax = "") -> ExprSyntax { - self.reduce("") { - if "\($0)".isEmpty { return "\($1)" } - return "\($0)\(separator)\($1)" +// MARK: Projected Value Helpers + +public extension PropertyWrapperMacroConfig { + /// Resolves the projected value type for generic property wrappers by mapping generic type parameters to their concrete types. + /// + /// This function is used when the property wrapper contains generic parameters and the projected value type + /// needs to be resolved based on the actual concrete types used in the variable declaration. It performs + /// type mapping by comparing the original generic declarations with the annotated concrete types. + /// + /// ## Usage + /// + /// This function is primarily called by the `MacrofyMacro` when generating property wrapper macros that + /// contain generic projected values. For example, when transforming: + /// + /// ```swift + /// @propertyWrapper + /// struct MyWrapper { + /// let wrappedValue: Value + /// let projectedValue: Binding + /// } + /// ``` + /// + /// Used with a concrete type like `@MyWrapper var name: String`, this function will resolve + /// `Binding` to `Binding`. + /// + /// ## Type Mapping Process + /// + /// 1. Extracts type trees from the original wrapped and projected value declarations + /// 2. Creates a type tree from the concrete variable declaration + /// 3. Builds a mapping from generic types to concrete types by comparing type trees + /// 4. Applies the mapping to transform the projected value type + /// + /// - Parameters: + /// - node: The macro attribute syntax node for diagnostic reporting + /// - originalWrappedValue: The original wrapped value declaration from the property wrapper definition + /// - originalProjectedValue: The original projected value declaration from the property wrapper definition + /// - declaration: The concrete variable declaration being processed with specific type annotations + /// - context: The macro expansion context for diagnostic reporting and type resolution + /// - Returns: The resolved projected value type with generic parameters substituted, or `nil` if resolution fails + /// - Note: Returns `nil` and emits diagnostics for malformed declarations or unsupported type structures + func projectedValueType(of node: AttributeSyntax, + originalWrappedValue: DeclSyntax, + originalProjectedValue: DeclSyntax, + providingAccessorsOf declaration: some DeclSyntaxProtocol, + in context: some MacroExpansionContext) -> TypeSyntax? + { + func diagnose(_ diagnostic: PropertyWrapperMacroDiagnostic) -> TypeSyntax? { + context.diagnose(Diagnostic(node: node, message: diagnostic)) + return nil + } + + guard let originalWrappedValueTypeTree = originalWrappedValue.as(VariableDeclSyntax.self)?.bindings.first?.typeAnnotation?.type.as(IdentifierTypeSyntax.self).map(TypeTree.init) else { + return diagnose(.unexpectedWrappedValueType) + } + guard let originalProjectedValueTypeTree = originalProjectedValue.as(VariableDeclSyntax.self)?.bindings.first?.typeAnnotation?.type.as(IdentifierTypeSyntax.self).map(TypeTree.init) else { + return diagnose(.unexpectedProjectedValueType) + } + + guard let variableDecl = declaration.as(VariableDeclSyntax.self) else { + return diagnose(.unexpectedTypeDeclaration) + } + guard let typeAnnotation = variableDecl.bindings.first?.typeAnnotation else { + return diagnose(.unexpectedTypeDeclaration) + } + guard let identifierType = typeAnnotation.type.as(IdentifierTypeSyntax.self) else { + return diagnose(.unexpectedTypeDeclaration) + } + + let annotatedTypeTree = TypeTree(identifierType) + + var originalToAnnotatedTypeMap: [String: String] = [:] + for (originalType, annotatedType) in zip(originalWrappedValueTypeTree, annotatedTypeTree) where originalType.type != annotatedType.type { + originalToAnnotatedTypeMap[originalType.type] = annotatedType.type + } + + let projectedValueType = originalToAnnotatedTypeMap.reduce(originalProjectedValueTypeTree) { typeTree, type in + typeTree.replacing(type: type.key, with: type.value) } + + return projectedValueType.typeSyntax } } diff --git a/Sources/PropertyWrapperMacro/PropertyWrapperMacroDiagnostic.swift b/Sources/PropertyWrapperMacro/PropertyWrapperMacroDiagnostic.swift index bb63b62..bf32f1f 100644 --- a/Sources/PropertyWrapperMacro/PropertyWrapperMacroDiagnostic.swift +++ b/Sources/PropertyWrapperMacro/PropertyWrapperMacroDiagnostic.swift @@ -10,16 +10,22 @@ import SwiftDiagnostics enum PropertyWrapperMacroDiagnostic: DiagnosticMessage { case unexpectedTypeDeclaration + case unexpectedWrappedValueType + case unexpectedProjectedValueType var rawValue: String { switch self { case .unexpectedTypeDeclaration: return "unexpected-type-declaration" + case .unexpectedWrappedValueType: return "unexpected-wrapped-value-type" + case .unexpectedProjectedValueType: return "unexpected-projected-value-type" } } var message: String { switch self { case .unexpectedTypeDeclaration: return "Macro can only be used on a variable declaration" + case .unexpectedWrappedValueType: return "The wrappedValue of the original property wrapper must be a variable declaration with an explicit type" + case .unexpectedProjectedValueType: return "The projectedValue of the original property wrapper must be a variable declaration with an explicit type" } } @@ -30,6 +36,8 @@ enum PropertyWrapperMacroDiagnostic: DiagnosticMessage { var severity: SwiftDiagnostics.DiagnosticSeverity { switch self { case .unexpectedTypeDeclaration: return .error + case .unexpectedWrappedValueType: return .error + case .unexpectedProjectedValueType: return .error } } } diff --git a/Tests/ExampleMacrosTests/ExampleMacrosIntegrationTests.swift b/Tests/ExampleMacrosTests/ExampleMacrosIntegrationTests.swift index 601cb20..c56d500 100644 --- a/Tests/ExampleMacrosTests/ExampleMacrosIntegrationTests.swift +++ b/Tests/ExampleMacrosTests/ExampleMacrosIntegrationTests.swift @@ -9,8 +9,8 @@ import Foundation import Testing -import Examples import ExampleMacros +import Examples @Suite struct ExampleMacrosIntegrationTests { @@ -20,18 +20,21 @@ struct ExampleMacrosIntegrationTests { let e3 = Example3() let e4 = Example4() let e5 = Example5() + let e6 = Example6() #expect(e1.wrappedValue == randomID) #expect(e2.wrappedValue == randomID) #expect(e3.wrappedValue == randomID) #expect(e4.wrappedValue == randomID) #expect(e5.wrappedValue == randomID) + #expect(e6.wrappedValue == randomID) #expect(e1.propertyWrapper.wrappedValue == randomID) #expect(e2.propertyWrapper.wrappedValue == randomID) #expect(e3.propertyWrapper.wrappedValue == randomID) #expect(e4.propertyWrapper.wrappedValue == randomID) #expect(e5.propertyWrapper.wrappedValue == randomID) + #expect(e6.propertyWrapper.wrappedValue == randomID) #expect(e2.$wrappedValue == randomID.hashValue) @@ -42,6 +45,8 @@ struct ExampleMacrosIntegrationTests { let newProjectedValue = Int.random(in: 0 ..< 10000) e5.$wrappedValue = newProjectedValue #expect(e5.$wrappedValue == newProjectedValue) + + #expect(e6.$wrappedValue.get() == randomID) } } @@ -76,3 +81,9 @@ final class Example5: Sendable { var propertyWrapper: ExampleWithSettableProjected { _wrappedValue } } + +final class Example6: Sendable { + @ExampleWithGenericProjected(randomID) var wrappedValue: UUID + + var propertyWrapper: ExampleWithGenericProjected { _wrappedValue } +} diff --git a/Tests/ExampleMacrosTests/ExampleMacrosTests.swift b/Tests/ExampleMacrosTests/ExampleMacrosTests.swift index c001e72..6be8997 100644 --- a/Tests/ExampleMacrosTests/ExampleMacrosTests.swift +++ b/Tests/ExampleMacrosTests/ExampleMacrosTests.swift @@ -21,6 +21,7 @@ final class ExampleMacrosTests: XCTestCase { "ExampleSettable": ExampleSettableMacro.self, "ExampleWithProjected": ExampleWithProjectedMacro.self, "ExampleWithSettableProjected": ExampleWithSettableProjectedMacro.self, + "ExampleWithGenericProjected": ExampleWithGenericProjectedMacro.self, ] func testExamplePropertyWrapperMacro() async throws { @@ -237,4 +238,32 @@ final class ExampleMacrosTests: XCTestCase { assertMacroExpansion(original, expandedSource: expected, macros: testMacros) } + + func testExampleWithGenericProjectedPropertyWrapperMacro() async throws { + let original = """ + final class Outer { + @ExampleWithGenericProjected var inner: Inner + } + """ + let expected = """ + final class Outer { + var inner: Inner { + get { + _inner.wrappedValue + } + } + + private let _inner = ExampleWithGenericProjected() + + var $inner: Result { + get { + _inner.projectedValue + } + + } + } + """ + + assertMacroExpansion(original, expandedSource: expected, macros: testMacros) + } } diff --git a/Tests/MacrofyMacroTests/MacrofyMacroTests.swift b/Tests/MacrofyMacroTests/MacrofyMacroTests.swift index cadbbe0..90f45f7 100644 --- a/Tests/MacrofyMacroTests/MacrofyMacroTests.swift +++ b/Tests/MacrofyMacroTests/MacrofyMacroTests.swift @@ -33,7 +33,7 @@ final class MacrofyMacroTests: XCTestCase { struct MyPropertyWrapper { let wrappedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { @@ -58,12 +58,12 @@ final class MacrofyMacroTests: XCTestCase { class MyPropertyWrapper { let wrappedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public let isReferenceType = true } } @@ -85,12 +85,12 @@ final class MacrofyMacroTests: XCTestCase { final class MyPropertyWrapper { let wrappedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public let isReferenceType = true } } @@ -112,12 +112,12 @@ final class MacrofyMacroTests: XCTestCase { actor MyPropertyWrapper { let wrappedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public let isReferenceType = true } } @@ -139,7 +139,7 @@ final class MacrofyMacroTests: XCTestCase { enum MyPropertyWrapper { let wrappedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { @@ -166,7 +166,7 @@ final class MacrofyMacroTests: XCTestCase { struct MyPropertyWrapper { let wrappedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { @@ -191,7 +191,7 @@ final class MacrofyMacroTests: XCTestCase { struct MyPropertyWrapper { let wrappedValue: String = "hello" } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { @@ -216,7 +216,7 @@ final class MacrofyMacroTests: XCTestCase { struct MyPropertyWrapper { var wrappedValue: String { "hello" } } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { @@ -241,7 +241,7 @@ final class MacrofyMacroTests: XCTestCase { struct MyPropertyWrapper { var wrappedValue: String { get { "hello" } } } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { @@ -266,12 +266,12 @@ final class MacrofyMacroTests: XCTestCase { struct MyPropertyWrapper { var wrappedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public let wrappedValueIsSettable = true } } @@ -293,12 +293,12 @@ final class MacrofyMacroTests: XCTestCase { struct MyPropertyWrapper { var wrappedValue: String = "hello" } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public let wrappedValueIsSettable = true } } @@ -328,12 +328,12 @@ final class MacrofyMacroTests: XCTestCase { } var backingValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public let wrappedValueIsSettable = true } } @@ -359,12 +359,12 @@ final class MacrofyMacroTests: XCTestCase { let wrappedValue: String let projectedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "String" } @@ -390,12 +390,12 @@ final class MacrofyMacroTests: XCTestCase { let wrappedValue: String let projectedValue: String = "hello" } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "String" } @@ -421,12 +421,12 @@ final class MacrofyMacroTests: XCTestCase { let wrappedValue: String var projectedValue: String { "hello" } } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "String" } @@ -452,12 +452,12 @@ final class MacrofyMacroTests: XCTestCase { let wrappedValue: String var projectedValue: String { get { "hello" } } } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "String" } @@ -483,16 +483,16 @@ final class MacrofyMacroTests: XCTestCase { let wrappedValue: String var projectedValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "String" } - + public let projectedValueIsSettable = true } } @@ -516,16 +516,16 @@ final class MacrofyMacroTests: XCTestCase { let wrappedValue: String var projectedValue: String = "hello" } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "String" } - + public let projectedValueIsSettable = true } } @@ -557,16 +557,16 @@ final class MacrofyMacroTests: XCTestCase { } var backingValue: String } - + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { public struct Config: PropertyWrapperMacroConfig { public init() { } - + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { "String" } - + public let projectedValueIsSettable = true } } @@ -574,4 +574,86 @@ final class MacrofyMacroTests: XCTestCase { assertMacroExpansion(original, expandedSource: expected, macros: testMacros) } + + func testMacrofyMacro_projectedValue_genericValue() async throws { + let original = """ + @macrofy + @propertyWrapper + struct MyPropertyWrapper { + let wrappedValue: Value + let projectedValue: Binding + } + """ + let expected = """ + @propertyWrapper + struct MyPropertyWrapper { + let wrappedValue: Value + let projectedValue: Binding + } + + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { + public struct Config: PropertyWrapperMacroConfig { + public init() { + } + + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { + projectedValueType( + of: node, + originalWrappedValue: #\""" + let wrappedValue: Value + \"""#, + originalProjectedValue: #\""" + let projectedValue: Binding + \"""#, + providingAccessorsOf: declaration, + in: context + ) + } + } + } + """ + + assertMacroExpansion(original, expandedSource: expected, macros: testMacros) + } + + func testMacrofyMacro_projectedValue_nestedGenericValue() async throws { + let original = """ + @macrofy + @propertyWrapper + struct MyPropertyWrapper { + let wrappedValue: Value + let projectedValue: Binding> + } + """ + let expected = """ + @propertyWrapper + struct MyPropertyWrapper { + let wrappedValue: Value + let projectedValue: Binding> + } + + public struct MyPropertyWrapperMacro: PropertyWrapperMacro { + public struct Config: PropertyWrapperMacroConfig { + public init() { + } + + public func projectedValueType(of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext) -> TypeSyntax? { + projectedValueType( + of: node, + originalWrappedValue: #\""" + let wrappedValue: Value + \"""#, + originalProjectedValue: #\""" + let projectedValue: Binding> + \"""#, + providingAccessorsOf: declaration, + in: context + ) + } + } + } + """ + + assertMacroExpansion(original, expandedSource: expected, macros: testMacros) + } } diff --git a/Tests/MacrofyModelsTests/TypeTreeTests.swift b/Tests/MacrofyModelsTests/TypeTreeTests.swift new file mode 100644 index 0000000..2b0defe --- /dev/null +++ b/Tests/MacrofyModelsTests/TypeTreeTests.swift @@ -0,0 +1,113 @@ +// +// TypeTreeTests.swift +// Macrofy +// +// Created by Annalise Mariottini on 9/26/25. +// + +import MacrofyModels +import SwiftSyntax +import SwiftSyntaxMacros +import Testing + +@Suite +struct TypeTreeTests { + @Test + func createsTreeForType_noGenerics() { + let declSyntax: DeclSyntax = "var value: Value1" + let identifierType: IdentifierTypeSyntax = declSyntax.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(IdentifierTypeSyntax.self)! + let typeTree = TypeTree(identifierType) + #expect(typeTree == TypeTree(type: "Value1")) + } + + @Test + func createsTreeForType_nestedOnce_single() { + let declSyntax: DeclSyntax = "var value: Value1" + let identifierType: IdentifierTypeSyntax = declSyntax.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(IdentifierTypeSyntax.self)! + let typeTree = TypeTree(identifierType) + #expect(typeTree == TypeTree(type: "Value1", children: [ + TypeTree(type: "Value2"), + ])) + } + + @Test + func createsTreeForType_nestedOnce_multiple() { + let declSyntax: DeclSyntax = "var value: Value1" + let identifierType: IdentifierTypeSyntax = declSyntax.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(IdentifierTypeSyntax.self)! + let typeTree = TypeTree(identifierType) + #expect(typeTree == TypeTree(type: "Value1", children: [ + TypeTree(type: "Value21"), + TypeTree(type: "Value22"), + TypeTree(type: "Value23"), + ])) + } + + @Test + func createsTreeForType_nestedTwice_single() { + let declSyntax: DeclSyntax = "var value: Value1>" + let identifierType: IdentifierTypeSyntax = declSyntax.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(IdentifierTypeSyntax.self)! + let typeTree = TypeTree(identifierType) + #expect(typeTree == TypeTree(type: "Value1", children: [ + TypeTree(type: "Value2", children: [ + TypeTree(type: "Value3"), + ]), + ])) + } + + @Test + func createsTreeForType_nestedTwice_multiple() { + let declSyntax: DeclSyntax = "var value: Value1, Value22, Value23>" + let identifierType: IdentifierTypeSyntax = declSyntax.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(IdentifierTypeSyntax.self)! + let typeTree = TypeTree(identifierType) + #expect(typeTree == TypeTree(type: "Value1", children: [ + TypeTree(type: "Value21", children: [ + TypeTree(type: "Value31"), + TypeTree(type: "Value32"), + TypeTree(type: "Value33"), + ]), + TypeTree(type: "Value22", children: [ + TypeTree(type: "Value31"), + TypeTree(type: "Value32"), + ]), + TypeTree(type: "Value23"), + ])) + } + + @Test + func createsTreeForType_nestedThrice_single() { + let declSyntax: DeclSyntax = "var value: Value1>>" + let identifierType: IdentifierTypeSyntax = declSyntax.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(IdentifierTypeSyntax.self)! + let typeTree = TypeTree(identifierType) + #expect(typeTree == TypeTree(type: "Value1", children: [ + TypeTree(type: "Value2", children: [ + TypeTree(type: "Value3", children: [ + TypeTree(type: "Value4"), + ]), + ]), + ])) + } + + @Test + func createsTreeForType_nestedThrice_multiple() { + let declSyntax: DeclSyntax = "var value: Value1, Value32, Value33>, Value22>, Value23>" + let identifierType: IdentifierTypeSyntax = declSyntax.as(VariableDeclSyntax.self)!.bindings.first!.typeAnnotation!.type.as(IdentifierTypeSyntax.self)! + let typeTree = TypeTree(identifierType) + #expect(typeTree == TypeTree(type: "Value1", children: [ + TypeTree(type: "Value21", children: [ + TypeTree(type: "Value31", children: [ + TypeTree(type: "Value41"), + TypeTree(type: "Value42"), + ]), + TypeTree(type: "Value32"), + TypeTree(type: "Value33"), + ]), + TypeTree(type: "Value22", children: [ + TypeTree(type: "Value31"), + TypeTree(type: "Value32", children: [ + TypeTree(type: "Value41"), + ]), + ]), + TypeTree(type: "Value23"), + ])) + } +}