From 7a574172d7866adfcc7a1bdcfab7d596ac019c06 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 27 Sep 2025 12:24:59 +0800 Subject: [PATCH 01/10] Add OpenSwiftUIMacros macro target and Entry macros Introduce a new OpenSwiftUIMacros executable target and accompanying test target, and register them in Package.swift. Add a Macro folder in OpenSwiftUI with EntryMacro and __EntryDefaultValue macro declarations that reference the new OpenSwiftUIMacros module. Implement the macros in Sources/OpenSwiftUIMacros (EntryMacro, EntryDefaultValueMacro, and plugin entry point) to generate accessor and peer key types for @Entry and to provide a getter-only expansion for @__EntryDefaultValue. Add comprehensive tests (OpenSwiftUIMacrosTests) to verify macro expansions for string and integer defaults. --- Package.swift | 24 ++++ Sources/OpenSwiftUI/Macro/EntryMacro.swift | 14 +++ .../EntryDefaultValueMacro.swift | 35 ++++++ Sources/OpenSwiftUIMacros/EntryMacro.swift | 104 ++++++++++++++++++ .../OpenSwiftUIMacros/OpenSwiftUIMacros.swift | 19 ++++ .../EntryMacroTests.swift | 95 ++++++++++++++++ 6 files changed, 291 insertions(+) create mode 100644 Sources/OpenSwiftUI/Macro/EntryMacro.swift create mode 100644 Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift create mode 100644 Sources/OpenSwiftUIMacros/EntryMacro.swift create mode 100644 Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift create mode 100644 Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift diff --git a/Package.swift b/Package.swift index fe72e6c16..475aa7689 100644 --- a/Package.swift +++ b/Package.swift @@ -240,6 +240,26 @@ let openSwiftUISPITestTarget = Target.testTarget( swiftSettings: sharedSwiftSettings ) +// MARK: - OpenSwiftUIMacros Target + +let openSwiftUIMacrosTarget = Target.executableTarget( + 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: @@ -249,6 +269,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"), @@ -435,9 +456,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, @@ -454,6 +477,7 @@ if renderGTKCondition { if !compatibilityTestCondition { package.targets += [ openSwiftUISPITestTarget, + openSwiftUIMacrosTestTarget, openSwiftUICoreTestTarget, openSwiftUITestTarget, openSwiftUIBridgeTestTarget, diff --git a/Sources/OpenSwiftUI/Macro/EntryMacro.swift b/Sources/OpenSwiftUI/Macro/EntryMacro.swift new file mode 100644 index 000000000..d3d4ce29d --- /dev/null +++ b/Sources/OpenSwiftUI/Macro/EntryMacro.swift @@ -0,0 +1,14 @@ +// +// EntryMacro.swift +// OpenSwiftUI +// +// Created by OpenSwiftUI on [Date]. +// + +@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" +) \ No newline at end of file diff --git a/Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift b/Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift new file mode 100644 index 000000000..e611d0ec6 --- /dev/null +++ b/Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift @@ -0,0 +1,35 @@ +// +// EntryDefaultValueMacro.swift +// OpenSwiftUI +// +// Created by OpenSwiftUI on [Date]. +// + +@_exported import SwiftSyntax +@_exported import SwiftSyntaxBuilder +@_exported import SwiftSyntaxMacros + +public struct EntryDefaultValueMacro: AccessorMacro { + public 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)" + } + ) + ] + } +} \ No newline at end of file diff --git a/Sources/OpenSwiftUIMacros/EntryMacro.swift b/Sources/OpenSwiftUIMacros/EntryMacro.swift new file mode 100644 index 000000000..2fab2c8f3 --- /dev/null +++ b/Sources/OpenSwiftUIMacros/EntryMacro.swift @@ -0,0 +1,104 @@ +// +// EntryMacro.swift +// OpenSwiftUI +// +// Created by OpenSwiftUI on [Date]. +// + +@_exported import SwiftSyntax +@_exported import SwiftSyntaxBuilder +@_exported import SwiftSyntaxMacros + +public struct EntryMacro: AccessorMacro, PeerMacro { + public 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, + let _ = binding.typeAnnotation?.type else { + throw MacroExpansionErrorMessage("@Entry can only be applied to stored properties") + } + + 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" + } + ) + ] + } + + public 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, + let type = binding.typeAnnotation?.type, + let initializer = binding.initializer else { + throw MacroExpansionErrorMessage("@Entry requires a property with type annotation and initial value") + } + + let identifierText = identifier.text + let keyName = "__Key_\(identifierText)" + let defaultValue = initializer.value + + let keyStruct = StructDeclSyntax( + modifiers: [DeclModifierSyntax(name: .keyword(.private))], + name: .identifier(keyName), + inheritanceClause: InheritanceClauseSyntax { + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("SwiftUICore.EnvironmentKey"))) + } + ) { + VariableDeclSyntax( + attributes: AttributeListSyntax([ + AttributeListSyntax.Element( + AttributeSyntax( + atSign: .atSignToken(), + attributeName: IdentifierTypeSyntax(name: .identifier("__EntryDefaultValue")) + ) + ) + ]), + modifiers: [DeclModifierSyntax(name: .keyword(.static))], + bindingSpecifier: .keyword(.var) + ) { + PatternBindingSyntax( + pattern: IdentifierPatternSyntax(identifier: .identifier("defaultValue")), + typeAnnotation: TypeAnnotationSyntax( + colon: .colonToken(), + type: type + ), + initializer: InitializerClauseSyntax( + equal: .equalToken(), + value: defaultValue + ) + ) + } + } + + return [DeclSyntax(keyStruct)] + } +} + +struct MacroExpansionErrorMessage: Error, CustomStringConvertible { + let description: String + + init(_ description: String) { + self.description = description + } +} \ No newline at end of file diff --git a/Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift b/Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift new file mode 100644 index 000000000..2b35b8385 --- /dev/null +++ b/Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift @@ -0,0 +1,19 @@ +// +// OpenSwiftUIMacros.swift +// OpenSwiftUI +// +// Created by OpenSwiftUI on [Date]. +// + +@_exported import SwiftCompilerPlugin +@_exported import SwiftSyntax +@_exported import SwiftSyntaxBuilder +@_exported import SwiftSyntaxMacros + +@main +struct OpenSwiftUIMacrosPlugin: CompilerPlugin { + let providingMacros: [Macro.Type] = [ + EntryMacro.self, + EntryDefaultValueMacro.self, + ] +} \ No newline at end of file diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift new file mode 100644 index 000000000..eb7c084be --- /dev/null +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -0,0 +1,95 @@ +// +// EntryMacroTests.swift +// OpenSwiftUI +// +// Created by OpenSwiftUI on [Date]. +// + +import SwiftSyntaxMacros +import SwiftSyntaxMacrosTestSupport +import Testing + +#if canImport(OpenSwiftUIMacros) +import OpenSwiftUIMacros + +@Test func entryMacroExpansion() { + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var myCustomValue: String = "Default value" + } + """, + expandedSource: + """ + extension EnvironmentValues { + var myCustomValue: String { + get { + self[__Key_myCustomValue.self] + } + set { + self[__Key_myCustomValue.self] = newValue + } + } + } + + private struct __Key_myCustomValue: SwiftUICore.EnvironmentKey { + @__EntryDefaultValue + static var defaultValue: String = "Default value" + } + """, + macros: testMacros + ) +} + +@Test func entryMacroExpansionWithIntType() { + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var intValue: Int = 42 + } + """, + expandedSource: + """ + extension EnvironmentValues { + var intValue: Int { + get { + self[__Key_intValue.self] + } + set { + self[__Key_intValue.self] = newValue + } + } + } + + private struct __Key_intValue: SwiftUICore.EnvironmentKey { + @__EntryDefaultValue + static var defaultValue: Int = 42 + } + """, + macros: testMacros + ) +} + +@Test func entryDefaultValueMacroExpansion() { + assertMacroExpansion( + """ + @__EntryDefaultValue + static var defaultValue: String = "Default value" + """, + expandedSource: + """ + static var defaultValue: String { + get { + "Default value" + } + } + """, + macros: testMacros + ) +} + +private let testMacros: [String: Macro.Type] = [ + "Entry": EntryMacro.self, + "__EntryDefaultValue": EntryDefaultValueMacro.self, +] +#endif \ No newline at end of file From 43051e3d7aa3aca9fb7ffa2de0722345e2967882 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 27 Sep 2025 16:50:26 +0800 Subject: [PATCH 02/10] Audit implementation --- .../Macro/EntryMacro.swift | 5 +- .../EntryDefaultValueMacro.swift | 16 +- Sources/OpenSwiftUIMacros/EntryMacro.swift | 26 ++- .../OpenSwiftUIMacros/OpenSwiftUIMacros.swift | 19 --- .../OpenSwiftUIMacrosPlugin.swift | 16 ++ .../EntryMacroTests.swift | 150 +++++++++--------- 6 files changed, 109 insertions(+), 123 deletions(-) rename Sources/{OpenSwiftUI => OpenSwiftUICore}/Macro/EntryMacro.swift (84%) delete mode 100644 Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift create mode 100644 Sources/OpenSwiftUIMacros/OpenSwiftUIMacrosPlugin.swift diff --git a/Sources/OpenSwiftUI/Macro/EntryMacro.swift b/Sources/OpenSwiftUICore/Macro/EntryMacro.swift similarity index 84% rename from Sources/OpenSwiftUI/Macro/EntryMacro.swift rename to Sources/OpenSwiftUICore/Macro/EntryMacro.swift index d3d4ce29d..e6ac46713 100644 --- a/Sources/OpenSwiftUI/Macro/EntryMacro.swift +++ b/Sources/OpenSwiftUICore/Macro/EntryMacro.swift @@ -1,9 +1,6 @@ // // EntryMacro.swift -// OpenSwiftUI -// -// Created by OpenSwiftUI on [Date]. -// +// OpenSwiftUICore @attached(accessor) @attached(peer, names: prefixed(__Key_)) public macro Entry() = #externalMacro( module: "OpenSwiftUIMacros", type: "EntryMacro" diff --git a/Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift b/Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift index e611d0ec6..09168ca1c 100644 --- a/Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift +++ b/Sources/OpenSwiftUIMacros/EntryDefaultValueMacro.swift @@ -1,16 +1,12 @@ // // EntryDefaultValueMacro.swift -// OpenSwiftUI -// -// Created by OpenSwiftUI on [Date]. -// +// OpenSwiftUIMacros -@_exported import SwiftSyntax -@_exported import SwiftSyntaxBuilder -@_exported import SwiftSyntaxMacros +package import SwiftSyntax +package import SwiftSyntaxMacros -public struct EntryDefaultValueMacro: AccessorMacro { - public static func expansion( +package struct EntryDefaultValueMacro: AccessorMacro { + package static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext @@ -32,4 +28,4 @@ public struct EntryDefaultValueMacro: AccessorMacro { ) ] } -} \ No newline at end of file +} diff --git a/Sources/OpenSwiftUIMacros/EntryMacro.swift b/Sources/OpenSwiftUIMacros/EntryMacro.swift index 2fab2c8f3..3fd59b40a 100644 --- a/Sources/OpenSwiftUIMacros/EntryMacro.swift +++ b/Sources/OpenSwiftUIMacros/EntryMacro.swift @@ -1,16 +1,12 @@ // // EntryMacro.swift -// OpenSwiftUI -// -// Created by OpenSwiftUI on [Date]. -// +// OpenSwiftUIMacros -@_exported import SwiftSyntax -@_exported import SwiftSyntaxBuilder -@_exported import SwiftSyntaxMacros +package import SwiftSyntax +package import SwiftSyntaxMacros -public struct EntryMacro: AccessorMacro, PeerMacro { - public static func expansion( +package struct EntryMacro: AccessorMacro, PeerMacro { + package static func expansion( of node: AttributeSyntax, providingAccessorsOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext @@ -41,7 +37,7 @@ public struct EntryMacro: AccessorMacro, PeerMacro { ] } - public static func expansion( + package static func expansion( of node: AttributeSyntax, providingPeersOf declaration: some DeclSyntaxProtocol, in context: some MacroExpansionContext @@ -62,7 +58,7 @@ public struct EntryMacro: AccessorMacro, PeerMacro { modifiers: [DeclModifierSyntax(name: .keyword(.private))], name: .identifier(keyName), inheritanceClause: InheritanceClauseSyntax { - InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("SwiftUICore.EnvironmentKey"))) + InheritedTypeSyntax(type: IdentifierTypeSyntax(name: .identifier("OpenSwiftUICore.EnvironmentKey"))) } ) { VariableDeclSyntax( @@ -95,10 +91,10 @@ public struct EntryMacro: AccessorMacro, PeerMacro { } } -struct MacroExpansionErrorMessage: Error, CustomStringConvertible { - let description: String +package struct MacroExpansionErrorMessage: Error, CustomStringConvertible { + package let description: String - init(_ description: String) { + package init(_ description: String) { self.description = description } -} \ No newline at end of file +} diff --git a/Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift b/Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift deleted file mode 100644 index 2b35b8385..000000000 --- a/Sources/OpenSwiftUIMacros/OpenSwiftUIMacros.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// OpenSwiftUIMacros.swift -// OpenSwiftUI -// -// Created by OpenSwiftUI on [Date]. -// - -@_exported import SwiftCompilerPlugin -@_exported import SwiftSyntax -@_exported import SwiftSyntaxBuilder -@_exported import SwiftSyntaxMacros - -@main -struct OpenSwiftUIMacrosPlugin: CompilerPlugin { - let providingMacros: [Macro.Type] = [ - EntryMacro.self, - EntryDefaultValueMacro.self, - ] -} \ No newline at end of file diff --git a/Sources/OpenSwiftUIMacros/OpenSwiftUIMacrosPlugin.swift b/Sources/OpenSwiftUIMacros/OpenSwiftUIMacrosPlugin.swift new file mode 100644 index 000000000..b54e2f4d9 --- /dev/null +++ b/Sources/OpenSwiftUIMacros/OpenSwiftUIMacrosPlugin.swift @@ -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, + ] +} diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift index eb7c084be..e45b6c5f1 100644 --- a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -1,95 +1,95 @@ // // EntryMacroTests.swift -// OpenSwiftUI -// -// Created by OpenSwiftUI on [Date]. -// +// OpenSwiftUIMacrosTests import SwiftSyntaxMacros import SwiftSyntaxMacrosTestSupport -import Testing +import XCTest #if canImport(OpenSwiftUIMacros) import OpenSwiftUIMacros -@Test func entryMacroExpansion() { - assertMacroExpansion( - """ - extension EnvironmentValues { - @Entry var myCustomValue: String = "Default value" - } - """, - expandedSource: - """ - extension EnvironmentValues { - var myCustomValue: String { - get { - self[__Key_myCustomValue.self] - } - set { - self[__Key_myCustomValue.self] = newValue +private let testMacros: [String: Macro.Type] = [ + "Entry": EntryMacro.self, + "__EntryDefaultValue": EntryDefaultValueMacro.self, +] + +final class EntryMacroTests: XCTestCase { + func testEntryMacroExpansion() { + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var myCustomValue: String = "Default value" + } + """, + expandedSource: + """ + extension EnvironmentValues { + var myCustomValue: String { + get { + self[__Key_myCustomValue.self] + } + set { + self[__Key_myCustomValue.self] = newValue + } } } - } - private struct __Key_myCustomValue: SwiftUICore.EnvironmentKey { - @__EntryDefaultValue - static var defaultValue: String = "Default value" - } - """, - macros: testMacros - ) -} + private struct __Key_myCustomValue: OpenSwiftUICore.EnvironmentKey { + @__EntryDefaultValue + static var defaultValue: String = "Default value" + } + """, + macros: testMacros + ) + } -@Test func entryMacroExpansionWithIntType() { - assertMacroExpansion( - """ - extension EnvironmentValues { - @Entry var intValue: Int = 42 - } - """, - expandedSource: - """ - extension EnvironmentValues { - var intValue: Int { - get { - self[__Key_intValue.self] - } - set { - self[__Key_intValue.self] = newValue + func testEntryMacroExpansionWithIntType() { + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var intValue: Int = 42 + } + """, + expandedSource: + """ + extension EnvironmentValues { + var intValue: Int { + get { + self[__Key_intValue.self] + } + set { + self[__Key_intValue.self] = newValue + } } } - } - private struct __Key_intValue: SwiftUICore.EnvironmentKey { - @__EntryDefaultValue - static var defaultValue: Int = 42 - } - """, - macros: testMacros - ) -} + private struct __Key_intValue: OpenSwiftUICore.EnvironmentKey { + @__EntryDefaultValue + static var defaultValue: Int = 42 + } + """, + macros: testMacros + ) + } -@Test func entryDefaultValueMacroExpansion() { - assertMacroExpansion( - """ - @__EntryDefaultValue - static var defaultValue: String = "Default value" - """, - expandedSource: - """ - static var defaultValue: String { - get { - "Default value" + func testEntryDefaultValueMacroExpansion() { + assertMacroExpansion( + """ + @__EntryDefaultValue + static var defaultValue: String = "Default value" + """, + expandedSource: + """ + static var defaultValue: String { + get { + "Default value" + } } - } - """, - macros: testMacros - ) + """, + macros: testMacros + ) + } } -private let testMacros: [String: Macro.Type] = [ - "Entry": EntryMacro.self, - "__EntryDefaultValue": EntryDefaultValueMacro.self, -] -#endif \ No newline at end of file +#endif From 6af05a8cf954f2680b9b31f944f44c72a12a8ae7 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 27 Sep 2025 13:04:46 +0800 Subject: [PATCH 03/10] Fix Package.swift --- Package.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index 475aa7689..4fa94d44a 100644 --- a/Package.swift +++ b/Package.swift @@ -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 @@ -242,7 +243,7 @@ let openSwiftUISPITestTarget = Target.testTarget( // MARK: - OpenSwiftUIMacros Target -let openSwiftUIMacrosTarget = Target.executableTarget( +let openSwiftUIMacrosTarget = Target.macro( name: "OpenSwiftUIMacros", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), From f3c601953ecb1bb59cc02557c4215633e8897c90 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 27 Sep 2025 16:50:16 +0800 Subject: [PATCH 04/10] Update CLAUDE.md --- CLAUDE.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index cd04bfde3..bff8a3dac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`: From 70b25ed1f3c48f8db6a89316ae32824feaccefb3 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 27 Sep 2025 17:01:21 +0800 Subject: [PATCH 05/10] Place EnvironmentKey types inside EnvironmentValues extension The generated EnvironmentKey structs were emitted outside the EnvironmentValues extension, which did not match expected structure. Move the private key types into the extension and adjust their defaultValue to computed properties (with getter) to satisfy the macro/test expectations. This ensures myCustomValue and intValue keys are scoped correctly and tests reflect the updated shape. --- .../EntryMacroTests.swift | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift index e45b6c5f1..08a9631e0 100644 --- a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -33,11 +33,14 @@ final class EntryMacroTests: XCTestCase { self[__Key_myCustomValue.self] = newValue } } - } - private struct __Key_myCustomValue: OpenSwiftUICore.EnvironmentKey { - @__EntryDefaultValue - static var defaultValue: String = "Default value" + private struct __Key_myCustomValue: OpenSwiftUICore.EnvironmentKey { + static var defaultValue: String { + get { + "Default value" + } + } + } } """, macros: testMacros @@ -62,11 +65,14 @@ final class EntryMacroTests: XCTestCase { self[__Key_intValue.self] = newValue } } - } - private struct __Key_intValue: OpenSwiftUICore.EnvironmentKey { - @__EntryDefaultValue - static var defaultValue: Int = 42 + private struct __Key_intValue: OpenSwiftUICore.EnvironmentKey { + static var defaultValue: Int { + get { + 42 + } + } + } } """, macros: testMacros From fa51ece50da46bc3723fc065b69de0a2ced30c30 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 29 Sep 2025 22:59:26 +0800 Subject: [PATCH 06/10] Support inferred and optional types in @Entry macro Add tests for two new cases: inferred type ("@Entry var inferType = 0") and optional type ("@Entry var optionalType: Int?"). Update the macro implementation to accept bindings that have either a type annotation or an initializer, infer simple literal types from initializers (Int, Double, String, Bool), and provide a default nil value for optional type annotations. This fixes failing tests by allowing @Entry to work with type inference and Optional default nil semantics, and by returning clear errors for unsupported cases. --- Sources/OpenSwiftUIMacros/EntryMacro.swift | 63 ++++++++++++++++-- .../EntryMacroTests.swift | 64 +++++++++++++++++++ 2 files changed, 120 insertions(+), 7 deletions(-) diff --git a/Sources/OpenSwiftUIMacros/EntryMacro.swift b/Sources/OpenSwiftUIMacros/EntryMacro.swift index 3fd59b40a..3a224672a 100644 --- a/Sources/OpenSwiftUIMacros/EntryMacro.swift +++ b/Sources/OpenSwiftUIMacros/EntryMacro.swift @@ -13,11 +13,18 @@ package struct EntryMacro: AccessorMacro, PeerMacro { ) throws -> [AccessorDeclSyntax] { guard let varDecl = declaration.as(VariableDeclSyntax.self), let binding = varDecl.bindings.first, - let identifier = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier, - let _ = binding.typeAnnotation?.type else { + 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)" @@ -44,15 +51,57 @@ package struct EntryMacro: AccessorMacro, PeerMacro { ) throws -> [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, - let initializer = binding.initializer else { - throw MacroExpansionErrorMessage("@Entry requires a property with type annotation and initial value") + 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)" - let defaultValue = initializer.value + + // Determine type and default value + let type: TypeSyntax + let defaultValue: ExprSyntax + + if let explicitType = binding.typeAnnotation?.type { + // Case 1: Explicit type annotation + type = explicitType + if let initializer = binding.initializer { + // Has both type and initializer - use the initializer value + defaultValue = initializer.value + } else { + // 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 if let initializer = binding.initializer { + // Case 2: Type inference from initializer + // We need to infer the type from the initializer + // For now, we'll use a simple approach and let Swift infer it + let initValue = initializer.value + + // Try to infer basic types + if initValue.is(IntegerLiteralExprSyntax.self) { + type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Int"))) + } else if initValue.is(FloatLiteralExprSyntax.self) { + type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Double"))) + } else if initValue.is(StringLiteralExprSyntax.self) { + type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("String"))) + } else if initValue.is(BooleanLiteralExprSyntax.self) { + type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Bool"))) + } else { + // For complex expressions, we'll use the initializer expression type + // This is a simplified approach - in practice Swift's type inference is more complex + throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions") + } + defaultValue = initValue + } else { + throw MacroExpansionErrorMessage("@Entry requires either a type annotation or an initial value") + } let keyStruct = StructDeclSyntax( modifiers: [DeclModifierSyntax(name: .keyword(.private))], diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift index 08a9631e0..572113543 100644 --- a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -96,6 +96,70 @@ final class EntryMacroTests: XCTestCase { macros: testMacros ) } + + func testEntryMacroWithInferredType() { + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var inferType = 0 + } + """, + expandedSource: + """ + extension EnvironmentValues { + var inferType { + get { + self[__Key_inferType.self] + } + set { + self[__Key_inferType.self] = newValue + } + } + + private struct __Key_inferType: OpenSwiftUICore.EnvironmentKey { + static var defaultValue: Int { + get { + 0 + } + } + } + } + """, + macros: testMacros + ) + } + + func testEntryMacroWithOptionalType() { + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var optionalType: Int? + } + """, + expandedSource: + """ + extension EnvironmentValues { + var optionalType: Int? { + get { + self[__Key_optionalType.self] + } + set { + self[__Key_optionalType.self] = newValue + } + } + + private struct __Key_optionalType: OpenSwiftUICore.EnvironmentKey { + static var defaultValue: Int? { + get { + nil + } + } + } + } + """, + macros: testMacros + ) + } } #endif From 904fb6ca2fc89b769dc82365d78f83ed14bc68a5 Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 29 Sep 2025 23:17:09 +0800 Subject: [PATCH 07/10] Infer custom types from simple initializer calls in @Entry macro Add support for type inference when using @Entry with simple initializer calls like CustomType(). The macro previously only inferred basic literals (String/Bool) and rejected other expressions; this caused tests like adding a CustomType struct with @Entry var inferCustomType = CustomType() to fail. Now the macro recognizes FunctionCallExpr with a DeclReferenceExpr callee and uses the referenced identifier as the inferred type. Also add a unit test (testEntryMacroWithCustomTypeInference) that verifies expansion for the CustomType example, and update the error message for complex expressions to instruct users to provide explicit types when necessary. --- Sources/OpenSwiftUIMacros/EntryMacro.swift | 11 ++++-- .../EntryMacroTests.swift | 36 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUIMacros/EntryMacro.swift b/Sources/OpenSwiftUIMacros/EntryMacro.swift index 3a224672a..4e77b4ce3 100644 --- a/Sources/OpenSwiftUIMacros/EntryMacro.swift +++ b/Sources/OpenSwiftUIMacros/EntryMacro.swift @@ -93,10 +93,15 @@ package struct EntryMacro: AccessorMacro, PeerMacro { type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("String"))) } else if initValue.is(BooleanLiteralExprSyntax.self) { type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Bool"))) + } else if let functionCall = initValue.as(FunctionCallExprSyntax.self), + let identifierExpr = functionCall.calledExpression.as(DeclReferenceExprSyntax.self) { + // Handle function calls like CustomType() + let typeName = identifierExpr.baseName.text + type = TypeSyntax(IdentifierTypeSyntax(name: .identifier(typeName))) } else { - // For complex expressions, we'll use the initializer expression type - // This is a simplified approach - in practice Swift's type inference is more complex - throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions") + // For other complex expressions, we cannot easily infer the type at compile time + // We could potentially use a more sophisticated approach, but for now we require explicit types + throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions. Use: @Entry var name: CustomType = CustomType()") } defaultValue = initValue } else { diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift index 572113543..cb1b8ed3c 100644 --- a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -160,6 +160,42 @@ final class EntryMacroTests: XCTestCase { macros: testMacros ) } + + func testEntryMacroWithCustomTypeInference() { + assertMacroExpansion( + """ + struct CustomType {} + + extension EnvironmentValues { + @Entry var inferCustomType = CustomType() + } + """, + expandedSource: + """ + struct CustomType {} + + extension EnvironmentValues { + var inferCustomType { + get { + self[__Key_inferCustomType.self] + } + set { + self[__Key_inferCustomType.self] = newValue + } + } + + private struct __Key_inferCustomType: OpenSwiftUICore.EnvironmentKey { + static var defaultValue: CustomType { + get { + CustomType() + } + } + } + } + """, + macros: testMacros + ) + } } #endif From 191a7626d20f7985715193a8542473f4296f893f Mon Sep 17 00:00:00 2001 From: Kyle Date: Mon, 29 Sep 2025 23:25:51 +0800 Subject: [PATCH 08/10] Handle member-function call inference and add tests Improve @Entry macro handling for function-call initializers by validating and heuristically inferring types for different call forms. Add a new test to assert behavior for member function calls (A.b()) and emit a clear diagnostic when the macro cannot reliably infer a return type. This change also tightens error messages for complex expressions and ensures optional/member call cases either get a sensible heuristic or require an explicit type annotation so tests reflect the macro's limitations. --- Sources/OpenSwiftUIMacros/EntryMacro.swift | 52 ++++++++++++++++--- .../EntryMacroTests.swift | 21 ++++++++ 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/Sources/OpenSwiftUIMacros/EntryMacro.swift b/Sources/OpenSwiftUIMacros/EntryMacro.swift index 4e77b4ce3..d16aba1e5 100644 --- a/Sources/OpenSwiftUIMacros/EntryMacro.swift +++ b/Sources/OpenSwiftUIMacros/EntryMacro.swift @@ -25,6 +25,20 @@ package struct EntryMacro: AccessorMacro, PeerMacro { throw MacroExpansionErrorMessage("@Entry requires either a type annotation or an initial value") } + // Validate type inference for unsupported cases + if !hasType, let initializer = binding.initializer { + let initValue = initializer.value + + // Check if this is a member function call that we cannot handle + if let functionCall = initValue.as(FunctionCallExprSyntax.self), + let memberAccess = functionCall.calledExpression.as(MemberAccessExprSyntax.self) { + let memberName = memberAccess.declName.baseName.text + if memberName.first?.isUppercase != true { + throw MacroExpansionErrorMessage("@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()") + } + } + } + let identifierText = identifier.text let keyName = "__Key_\(identifierText)" @@ -93,15 +107,39 @@ package struct EntryMacro: AccessorMacro, PeerMacro { type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("String"))) } else if initValue.is(BooleanLiteralExprSyntax.self) { type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Bool"))) - } else if let functionCall = initValue.as(FunctionCallExprSyntax.self), - let identifierExpr = functionCall.calledExpression.as(DeclReferenceExprSyntax.self) { - // Handle function calls like CustomType() - let typeName = identifierExpr.baseName.text - type = TypeSyntax(IdentifierTypeSyntax(name: .identifier(typeName))) + } else if let functionCall = initValue.as(FunctionCallExprSyntax.self) { + // Handle different types of function calls + if let identifierExpr = functionCall.calledExpression.as(DeclReferenceExprSyntax.self) { + // Simple function calls like CustomType() + let typeName = identifierExpr.baseName.text + type = TypeSyntax(IdentifierTypeSyntax(name: .identifier(typeName))) + } else if let memberAccess = functionCall.calledExpression.as(MemberAccessExprSyntax.self) { + // Member access function calls like A.b() + // We cannot determine the return type without type checking, but we can try + // to extract it from context or require explicit annotation + + // For now, we'll look for a pattern where the function name suggests the return type + // This is heuristic-based and limited, but covers some common cases + let memberName = memberAccess.declName.baseName.text + + // Try some heuristics: if function name matches a type (like A.c() -> C) + // This is a simplified approach for demonstration + if memberName.first?.isUppercase == true { + // Assume function name starting with uppercase is a type name + let capitalizedName = String(memberName.prefix(1).uppercased() + memberName.dropFirst()) + type = TypeSyntax(IdentifierTypeSyntax(name: .identifier(capitalizedName))) + } else { + // For member access calls, we need explicit type annotation + // because we cannot reliably infer the return type + throw MacroExpansionErrorMessage("@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()") + } + } else { + // Other complex function call expressions + throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions. Use: @Entry var name: CustomType = CustomType()") + } } else { // For other complex expressions, we cannot easily infer the type at compile time - // We could potentially use a more sophisticated approach, but for now we require explicit types - throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions. Use: @Entry var name: CustomType = CustomType()") + throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions") } defaultValue = initValue } else { diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift index cb1b8ed3c..e094d5d54 100644 --- a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -196,6 +196,27 @@ final class EntryMacroTests: XCTestCase { macros: testMacros ) } + + func testEntryMacroWithMemberFunctionCallInference() { + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var p = A.b() + } + """, + expandedSource: + """ + extension EnvironmentValues { + var p = A.b() + } + """, + diagnostics: [ + DiagnosticSpec(message: "@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()", line: 2, column: 5), + DiagnosticSpec(message: "@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()", line: 2, column: 5) + ], + macros: testMacros + ) + } } #endif From 660153c6a822e67ca341e585b07dbbf92403345e Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 30 Sep 2025 00:08:53 +0800 Subject: [PATCH 09/10] Improve @Entry macro: stop incorrect type heuristics and preserve default value Refine Entry macro expansion to stop attempting fragile, case-by-case type inference from initializers (especially member function calls). Instead: always preserve the initializer expression as the defaultValue when present, handle the "only type annotation" case by producing nil for optional types or emitting an error for non-optional types, and emit the generated PatternBinding correctly. This fixes an incorrect emitted type like "static var defaultValue: B = A.B()" and updates tests to assert the expanded form (ignore @__EntryDefaultValue for now) and add a positive test for A.B() returning C. --- .../OpenSwiftUICore/Macro/EntryMacro.swift | 2 +- Sources/OpenSwiftUIMacros/EntryMacro.swift | 117 ++++-------------- .../EntryMacroTests.swift | 35 ++++-- 3 files changed, 52 insertions(+), 102 deletions(-) diff --git a/Sources/OpenSwiftUICore/Macro/EntryMacro.swift b/Sources/OpenSwiftUICore/Macro/EntryMacro.swift index e6ac46713..1b30c57af 100644 --- a/Sources/OpenSwiftUICore/Macro/EntryMacro.swift +++ b/Sources/OpenSwiftUICore/Macro/EntryMacro.swift @@ -8,4 +8,4 @@ @attached(accessor) public macro __EntryDefaultValue() = #externalMacro( module: "OpenSwiftUIMacros", type: "EntryDefaultValueMacro" -) \ No newline at end of file +) diff --git a/Sources/OpenSwiftUIMacros/EntryMacro.swift b/Sources/OpenSwiftUIMacros/EntryMacro.swift index d16aba1e5..34f8803e9 100644 --- a/Sources/OpenSwiftUIMacros/EntryMacro.swift +++ b/Sources/OpenSwiftUIMacros/EntryMacro.swift @@ -25,19 +25,6 @@ package struct EntryMacro: AccessorMacro, PeerMacro { throw MacroExpansionErrorMessage("@Entry requires either a type annotation or an initial value") } - // Validate type inference for unsupported cases - if !hasType, let initializer = binding.initializer { - let initValue = initializer.value - - // Check if this is a member function call that we cannot handle - if let functionCall = initValue.as(FunctionCallExprSyntax.self), - let memberAccess = functionCall.calledExpression.as(MemberAccessExprSyntax.self) { - let memberName = memberAccess.declName.baseName.text - if memberName.first?.isUppercase != true { - throw MacroExpansionErrorMessage("@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()") - } - } - } let identifierText = identifier.text let keyName = "__Key_\(identifierText)" @@ -72,76 +59,21 @@ package struct EntryMacro: AccessorMacro, PeerMacro { let identifierText = identifier.text let keyName = "__Key_\(identifierText)" - // Determine type and default value - let type: TypeSyntax + // Determine default value let defaultValue: ExprSyntax - if let explicitType = binding.typeAnnotation?.type { - // Case 1: Explicit type annotation - type = explicitType - if let initializer = binding.initializer { - // Has both type and initializer - use the initializer value - defaultValue = initializer.value - } else { - // 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 if let initializer = binding.initializer { - // Case 2: Type inference from initializer - // We need to infer the type from the initializer - // For now, we'll use a simple approach and let Swift infer it - let initValue = initializer.value - - // Try to infer basic types - if initValue.is(IntegerLiteralExprSyntax.self) { - type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Int"))) - } else if initValue.is(FloatLiteralExprSyntax.self) { - type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Double"))) - } else if initValue.is(StringLiteralExprSyntax.self) { - type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("String"))) - } else if initValue.is(BooleanLiteralExprSyntax.self) { - type = TypeSyntax(IdentifierTypeSyntax(name: .identifier("Bool"))) - } else if let functionCall = initValue.as(FunctionCallExprSyntax.self) { - // Handle different types of function calls - if let identifierExpr = functionCall.calledExpression.as(DeclReferenceExprSyntax.self) { - // Simple function calls like CustomType() - let typeName = identifierExpr.baseName.text - type = TypeSyntax(IdentifierTypeSyntax(name: .identifier(typeName))) - } else if let memberAccess = functionCall.calledExpression.as(MemberAccessExprSyntax.self) { - // Member access function calls like A.b() - // We cannot determine the return type without type checking, but we can try - // to extract it from context or require explicit annotation - - // For now, we'll look for a pattern where the function name suggests the return type - // This is heuristic-based and limited, but covers some common cases - let memberName = memberAccess.declName.baseName.text - - // Try some heuristics: if function name matches a type (like A.c() -> C) - // This is a simplified approach for demonstration - if memberName.first?.isUppercase == true { - // Assume function name starting with uppercase is a type name - let capitalizedName = String(memberName.prefix(1).uppercased() + memberName.dropFirst()) - type = TypeSyntax(IdentifierTypeSyntax(name: .identifier(capitalizedName))) - } else { - // For member access calls, we need explicit type annotation - // because we cannot reliably infer the return type - throw MacroExpansionErrorMessage("@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()") - } - } else { - // Other complex function call expressions - throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions. Use: @Entry var name: CustomType = CustomType()") - } + 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 { - // For other complex expressions, we cannot easily infer the type at compile time - throw MacroExpansionErrorMessage("@Entry with type inference requires explicit type for complex expressions") + throw MacroExpansionErrorMessage("@Entry requires an initial value for non-optional types") } - defaultValue = initValue } else { throw MacroExpansionErrorMessage("@Entry requires either a type annotation or an initial value") } @@ -153,6 +85,16 @@ package struct EntryMacro: AccessorMacro, PeerMacro { 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( @@ -163,20 +105,9 @@ package struct EntryMacro: AccessorMacro, PeerMacro { ) ]), modifiers: [DeclModifierSyntax(name: .keyword(.static))], - bindingSpecifier: .keyword(.var) - ) { - PatternBindingSyntax( - pattern: IdentifierPatternSyntax(identifier: .identifier("defaultValue")), - typeAnnotation: TypeAnnotationSyntax( - colon: .colonToken(), - type: type - ), - initializer: InitializerClauseSyntax( - equal: .equalToken(), - value: defaultValue - ) - ) - } + bindingSpecifier: .keyword(.var), + bindings: PatternBindingListSyntax([patternBinding]) + ) } return [DeclSyntax(keyStruct)] diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift index e094d5d54..c750475ee 100644 --- a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -117,7 +117,7 @@ final class EntryMacroTests: XCTestCase { } private struct __Key_inferType: OpenSwiftUICore.EnvironmentKey { - static var defaultValue: Int { + static var defaultValue { get { 0 } @@ -185,7 +185,7 @@ final class EntryMacroTests: XCTestCase { } private struct __Key_inferCustomType: OpenSwiftUICore.EnvironmentKey { - static var defaultValue: CustomType { + static var defaultValue { get { CustomType() } @@ -198,24 +198,43 @@ final class EntryMacroTests: XCTestCase { } func testEntryMacroWithMemberFunctionCallInference() { + // Test the correct case where function name starts with uppercase (should succeed) assertMacroExpansion( """ extension EnvironmentValues { - @Entry var p = A.b() + @Entry var custom = A.B() + } + enum A { + static func B() -> C { .init() } } """, expandedSource: """ extension EnvironmentValues { - var p = A.b() + var custom { + get { + self[__Key_custom.self] + } + set { + self[__Key_custom.self] = newValue + } + } + + private struct __Key_custom: OpenSwiftUICore.EnvironmentKey { + static var defaultValue { + get { + A.B() + } + } + } + } + enum A { + static func B() -> C { .init() } } """, - diagnostics: [ - DiagnosticSpec(message: "@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()", line: 2, column: 5), - DiagnosticSpec(message: "@Entry with member function calls requires explicit type annotation. Use: @Entry var p: ReturnType = A.b()", line: 2, column: 5) - ], macros: testMacros ) + } } From 230b4e1de2ccddf20a806fc219808ee559e4aa9f Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 30 Sep 2025 00:11:52 +0800 Subject: [PATCH 10/10] Add additional Entry macro tests and test macros map Add a dedicated testEntryMacros map and several new assertMacroExpansion cases to follow the testEntryMacroExpansion example. These additions cover a basic @Entry expansion with a custom default value, and a case ensuring member function call inference works when assigning A.B() as a default. This change was needed to extend test coverage for EntryMacro type inference and annotation generation and to isolate the Entry macro mapping for these tests. --- .../EntryMacroTests.swift | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift index c750475ee..7fce36cd0 100644 --- a/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift +++ b/Tests/OpenSwiftUIMacrosTests/EntryMacroTests.swift @@ -14,6 +14,10 @@ private let testMacros: [String: Macro.Type] = [ "__EntryDefaultValue": EntryDefaultValueMacro.self, ] +private let testEntryMacros: [String: Macro.Type] = [ + "Entry": EntryMacro.self, +] + final class EntryMacroTests: XCTestCase { func testEntryMacroExpansion() { assertMacroExpansion( @@ -45,6 +49,31 @@ final class EntryMacroTests: XCTestCase { """, macros: testMacros ) + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var myCustomValue: String = "Default value" + } + """, + expandedSource: + """ + extension EnvironmentValues { + var myCustomValue: String { + get { + self[__Key_myCustomValue.self] + } + set { + self[__Key_myCustomValue.self] = newValue + } + } + + private struct __Key_myCustomValue: OpenSwiftUICore.EnvironmentKey { + @__EntryDefaultValue static var defaultValue: String = "Default value" + } + } + """, + macros: testEntryMacros + ) } func testEntryMacroExpansionWithIntType() { @@ -198,7 +227,6 @@ final class EntryMacroTests: XCTestCase { } func testEntryMacroWithMemberFunctionCallInference() { - // Test the correct case where function name starts with uppercase (should succeed) assertMacroExpansion( """ extension EnvironmentValues { @@ -234,7 +262,37 @@ final class EntryMacroTests: XCTestCase { """, macros: testMacros ) + assertMacroExpansion( + """ + extension EnvironmentValues { + @Entry var custom = A.B() + } + enum A { + static func B() -> C { .init() } + } + """, + expandedSource: + """ + extension EnvironmentValues { + var custom { + get { + self[__Key_custom.self] + } + set { + self[__Key_custom.self] = newValue + } + } + private struct __Key_custom: OpenSwiftUICore.EnvironmentKey { + @__EntryDefaultValue static var defaultValue = A.B() + } + } + enum A { + static func B() -> C { .init() } + } + """, + macros: testEntryMacros + ) } }