Skip to content
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ The project uses **swift-testing framework** (not XCTest):
#expect(value.isApproximatelyEqual(to: expectedValue))
```

**Exception: Macro Tests**
- Macro tests must use **XCTest** framework (not swift-testing)
- Import with `import XCTest`
- Use `final class TestName: XCTestCase` with `func testName()` methods
- Use XCTest assertions like `XCTAssertEqual()`, `XCTAssertTrue()`, etc.
- For macro expansion testing, use `assertMacroExpansion` from `SwiftSyntaxMacrosTestSupport`

### Compatibility Tests

When writing tests in `OpenSwiftUICompatibilityTests`:
Expand Down
25 changes: 25 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// swift-tools-version: 6.1
// The swift-tools-version declares the minimum version of Swift required to build this package.

import CompilerPluginSupport
import Foundation
import PackageDescription

Expand Down Expand Up @@ -240,6 +241,26 @@ let openSwiftUISPITestTarget = Target.testTarget(
swiftSettings: sharedSwiftSettings
)

// MARK: - OpenSwiftUIMacros Target

let openSwiftUIMacrosTarget = Target.macro(
name: "OpenSwiftUIMacros",
dependencies: [
.product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"),
],
swiftSettings: sharedSwiftSettings
)

let openSwiftUIMacrosTestTarget = Target.testTarget(
name: "OpenSwiftUIMacrosTests",
dependencies: [
"OpenSwiftUIMacros",
.product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
],
swiftSettings: sharedSwiftSettings
)

// MARK: - OpenSwiftUICore Target

// NOTE:
Expand All @@ -249,6 +270,7 @@ let openSwiftUICoreTarget = Target.target(
name: "OpenSwiftUICore",
dependencies: [
"OpenSwiftUI_SPI",
"OpenSwiftUIMacros",
.product(name: "OpenCoreGraphicsShims", package: "OpenCoreGraphics"),
.product(name: "OpenQuartzCoreShims", package: "OpenCoreGraphics"),
.product(name: "OpenAttributeGraphShims", package: "OpenAttributeGraph"),
Expand Down Expand Up @@ -435,9 +457,11 @@ let package = Package(
products: products,
dependencies: [
.package(url: "https://github.com/apple/swift-numerics", from: "1.0.3"),
.package(url: "https://github.com/swiftlang/swift-syntax.git", from: "601.0.0"),
],
targets: [
openSwiftUISPITarget,
openSwiftUIMacrosTarget,
openSwiftUICoreTarget,
cOpenSwiftUITarget,
openSwiftUITarget,
Expand All @@ -454,6 +478,7 @@ if renderGTKCondition {
if !compatibilityTestCondition {
package.targets += [
openSwiftUISPITestTarget,
openSwiftUIMacrosTestTarget,
openSwiftUICoreTestTarget,
openSwiftUITestTarget,
openSwiftUIBridgeTestTarget,
Expand Down
11 changes: 11 additions & 0 deletions Sources/OpenSwiftUICore/Macro/EntryMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// EntryMacro.swift
// OpenSwiftUICore

@attached(accessor) @attached(peer, names: prefixed(__Key_)) public macro Entry() = #externalMacro(
module: "OpenSwiftUIMacros", type: "EntryMacro"
)

@attached(accessor) public macro __EntryDefaultValue() = #externalMacro(
module: "OpenSwiftUIMacros", type: "EntryDefaultValueMacro"
)
31 changes: 31 additions & 0 deletions Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// EntryDefaultValueMacro.swift
// OpenSwiftUIMacros

package import SwiftSyntax
package import SwiftSyntaxMacros

package struct EntryDefaultValueMacro: AccessorMacro {
package static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let initializer = binding.initializer else {
throw MacroExpansionErrorMessage("@__EntryDefaultValue can only be applied to stored properties with initial values")
}

let defaultValue = initializer.value

return [
AccessorDeclSyntax(
accessorSpecifier: .keyword(.get),
body: CodeBlockSyntax {
"\(defaultValue)"
}
)
]
}
}
123 changes: 123 additions & 0 deletions Sources/OpenSwiftUIMacros/EntryMacro.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
//
// EntryMacro.swift
// OpenSwiftUIMacros

package import SwiftSyntax
package import SwiftSyntaxMacros

package struct EntryMacro: AccessorMacro, PeerMacro {
package static func expansion(
of node: AttributeSyntax,
providingAccessorsOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [AccessorDeclSyntax] {
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw MacroExpansionErrorMessage("@Entry can only be applied to stored properties")
}

// Check that we have either a type annotation OR an initializer (for type inference)
let hasType = binding.typeAnnotation?.type != nil
let hasInitializer = binding.initializer != nil

guard hasType || hasInitializer else {
throw MacroExpansionErrorMessage("@Entry requires either a type annotation or an initial value")
}


let identifierText = identifier.text
let keyName = "__Key_\(identifierText)"

return [
AccessorDeclSyntax(
accessorSpecifier: .keyword(.get),
body: CodeBlockSyntax {
"self[\(raw: keyName).self]"
}
),
AccessorDeclSyntax(
accessorSpecifier: .keyword(.set),
body: CodeBlockSyntax {
"self[\(raw: keyName).self] = newValue"
}
)
]
}

package static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard let varDecl = declaration.as(VariableDeclSyntax.self),
let binding = varDecl.bindings.first,
let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
throw MacroExpansionErrorMessage("@Entry requires a property with valid identifier")
}

let identifierText = identifier.text
let keyName = "__Key_\(identifierText)"

// Determine default value
let defaultValue: ExprSyntax

if let initializer = binding.initializer {
// Has initializer - use the initializer value
defaultValue = initializer.value
} else if let explicitType = binding.typeAnnotation?.type {
// Only type annotation, no initializer
// Check if it's optional type - if so, default to nil
let typeString = explicitType.description
if typeString.contains("?") {
defaultValue = ExprSyntax(NilLiteralExprSyntax())
} else {
throw MacroExpansionErrorMessage("@Entry requires an initial value for non-optional types")
}
} else {
throw MacroExpansionErrorMessage("@Entry requires either a type annotation or an initial value")
}

let keyStruct = StructDeclSyntax(
modifiers: [DeclModifierSyntax(name: .keyword(.private))],
name: .identifier(keyName),
inheritanceClause: InheritanceClauseSyntax {
InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("OpenSwiftUICore.EnvironmentKey")))
}
) {
// Create the variable declaration
let patternBinding = PatternBindingSyntax(
pattern: IdentifierPatternSyntax(identifier: .identifier("defaultValue")),
typeAnnotation: binding.typeAnnotation,
initializer: InitializerClauseSyntax(
equal: .equalToken(),
value: defaultValue
)
)

VariableDeclSyntax(
attributes: AttributeListSyntax([
AttributeListSyntax.Element(
AttributeSyntax(
atSign: .atSignToken(),
attributeName: IdentifierTypeSyntax(name: .identifier("__EntryDefaultValue"))
)
)
]),
modifiers: [DeclModifierSyntax(name: .keyword(.static))],
bindingSpecifier: .keyword(.var),
bindings: PatternBindingListSyntax([patternBinding])
)
}

return [DeclSyntax(keyStruct)]
}
}

package struct MacroExpansionErrorMessage: Error, CustomStringConvertible {
package let description: String

package init(_ description: String) {
self.description = description
}
}
16 changes: 16 additions & 0 deletions Sources/OpenSwiftUIMacros/OpenSwiftUIMacrosPlugin.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
//
// OpenSwiftUIMacrosPlugin.swift
// OpenSwiftUIMacros

import SwiftCompilerPlugin
import SwiftSyntax
import SwiftSyntaxBuilder
import SwiftSyntaxMacros

@main
struct OpenSwiftUIMacrosPlugin: CompilerPlugin {
let providingMacros: [Macro.Type] = [
EntryMacro.self,
EntryDefaultValueMacro.self,
]
}
Loading