Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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"),
Expand All @@ -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"),
Expand All @@ -72,7 +75,6 @@ let package = Package(
dependencies: [
"Macrofy",
"MacrofyMacro",
"PropertyWrapperMacro",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
]
),
Expand All @@ -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]
)
3 changes: 2 additions & 1 deletion Sources/ExampleMacros/ExampleMacrosPlugin.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// MacrofyPlugin.swift
// ExampleMacrosPlugin.swift
// Macrofy
//
// Created by Annalise Mariottini on 9/19/25.
Expand All @@ -15,6 +15,7 @@ struct ExampleMacrosPlugin: CompilerPlugin {
ExampleSettableMacro.self,
ExampleWithProjectedMacro.self,
ExampleWithSettableProjectedMacro.self,
ExampleWithGenericProjectedMacro.self,
ExampleWithWrappedValueMacro.self,
]
}
11 changes: 11 additions & 0 deletions Sources/ExampleMacros/ExamplePropertyWrapperMacros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ public final class ExampleWithSettableProjected<Value: Hashable & Sendable>: @un
public lazy var projectedValue: Int = wrappedValue.hashValue
}

@macrofy
@propertyWrapper
public final class ExampleWithGenericProjected<Value: Hashable & Sendable>: @unchecked Sendable {
public init(_ wrappedValue: Value) {
self.wrappedValue = wrappedValue
}

public let wrappedValue: Value
public var projectedValue: Result<Value, Never> { Result { wrappedValue } }
}

@macrofy
@propertyWrapper
public struct ExampleWithWrappedValue<Value: Hashable & Sendable>: Sendable {
Expand Down
6 changes: 6 additions & 0 deletions Sources/Examples/ExamplePropertyWrapperMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions Sources/Macrofy/Macrofy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
//

import Foundation
@_exported import MacrofyModels
@_exported import PropertyWrapperMacro

/// Automatically generates a property wrapper macro from a property wrapper type.
Expand Down
133 changes: 97 additions & 36 deletions Sources/MacrofyMacro/MacrofyMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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

""")
}
}
Expand All @@ -129,7 +133,7 @@ public struct MacrofyMacro: PeerMacro {
public struct \(declaration.name.trimmed)Macro: PropertyWrapperMacro {
\(config)
}
"""
""",
]
}

Expand All @@ -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
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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:
Expand All @@ -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
})
}
}
4 changes: 4 additions & 0 deletions Sources/MacrofyMacro/MacrofyMacroDiagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ 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"
}
}

var message: String {
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"
}
}

Expand All @@ -36,6 +39,7 @@ enum MacrofyMacroDiagnostic: DiagnosticMessage {
switch self {
case .unsupportedDeclarationType: return .error
case .missingWrappedValue: return .error
case .missingProjectedValueType: return .error
}
}
}
Loading