From c1e4d33f8b17c9409c533fe400f38399ad199e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Galen=20O=E2=80=99Hanlon?= Date: Tue, 4 Mar 2025 12:42:24 -0800 Subject: [PATCH 1/6] Update copyright years to include 2023-2025 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index b1b5908..b38bfef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Galen O’Hanlon +Copyright (c) 2023-2025 Galen O’Hanlon Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From c7139ea38236069dcfaa559fdfe329457b58d20f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Galen=20O=E2=80=99Hanlon?= Date: Tue, 4 Mar 2025 12:46:19 -0800 Subject: [PATCH 2/6] Remove unused functions The formatting functions were moved to MemberwiseInitFormatter but that commit missed removing them from MemberwiseInitMacro. --- .../Macros/MemberwiseInitMacro.swift | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift index ff92961..d2e71d0 100644 --- a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift @@ -324,50 +324,4 @@ public struct MemberwiseInitMacro: MemberMacro { false } } - - private static func formatParameter( - for property: MemberProperty, - considering allProperties: [MemberProperty], - deunderscoreParameters: Bool, - optionalsDefaultNil: Bool - ) -> String { - let defaultValue = - property.initializerValue.map { " = \($0.description)" } - ?? property.customSettings?.defaultValue.map { " = \($0.description)" } - ?? (optionalsDefaultNil && property.type.isOptionalType ? " = nil" : "") - - let escaping = - (property.customSettings?.forceEscaping ?? false || property.type.isFunctionType) - ? "@escaping " : "" - - let label = property.initParameterLabel( - considering: allProperties, deunderscoreParameters: deunderscoreParameters) - - let parameterName = property.initParameterName( - considering: allProperties, deunderscoreParameters: deunderscoreParameters) - - return "\(label)\(parameterName): \(escaping)\(property.type.description)\(defaultValue)" - } - - private static func formatInitializerAssignmentStatement( - for property: MemberProperty, - considering allProperties: [MemberProperty], - deunderscoreParameters: Bool - ) -> String { - let assignee = - switch property.customSettings?.assignee { - case .none: - "self.\(property.name)" - case .wrapper: - "self._\(property.name)" - case let .raw(assignee): - assignee - } - - let parameterName = property.initParameterName( - considering: allProperties, - deunderscoreParameters: deunderscoreParameters - ) - return "\(assignee) = \(parameterName)" - } } From 80c3c9b63b0ca0812fc8135274466857bf67dc92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Galen=20O=E2=80=99Hanlon?= Date: Tue, 4 Mar 2025 12:43:12 -0800 Subject: [PATCH 3/6] Improve clean target with better error handling and file reporting --- Makefile | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 58b5181..320b0b6 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,17 @@ test: test-swift test-swift: swift test --parallel +# Remove build artifacts while tolerating SourceKit-locked files clean: - rm -rf .build + @echo "Cleaning build artifacts..." + @rm -rf .build/* 2>&1 | grep -v "Permission denied" || true + @echo "Checking remaining build artifacts..." + @if [ -d .build ]; then \ + ls -R .build 2>/dev/null || echo "Unable to list some directories"; \ + else \ + echo "Build directory completely removed"; \ + fi + @echo "Build artifacts cleaned (ensure that any remaining files listed above won't affect new builds, e.g. SourceKit files)" test-swift-syntax-versions: @for version in \ From 31c0ba8cce196992ab06361cc3f760f6cd700b5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Galen=20O=E2=80=99Hanlon?= Date: Tue, 4 Mar 2025 13:08:33 -0800 Subject: [PATCH 4/6] Bump swift-snapshot-testing to 1.18.1 --- Package.resolved | 22 ++++++++++++++++++++-- Package.swift | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Package.resolved b/Package.resolved index fa739e3..80436bf 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,12 +1,21 @@ { "pins" : [ + { + "identity" : "swift-custom-dump", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-custom-dump", + "state" : { + "revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1", + "version" : "1.3.3" + } + }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "7b0bbbae90c41f848f90ac7b4df6c4f50068256d", - "version" : "1.17.5" + "revision" : "b2d4cb30735f4fbc3a01963a9c658336dd21e9ba", + "version" : "1.18.1" } }, { @@ -17,6 +26,15 @@ "revision" : "0687f71944021d616d34d922343dcef086855920", "version" : "600.0.1" } + }, + { + "identity" : "xctest-dynamic-overlay", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state" : { + "revision" : "39de59b2d47f7ef3ca88a039dff3084688fe27f4", + "version" : "1.5.2" + } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 71e98a1..7914132 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.1"), + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.18.1"), //.conditionalPackage(url: "https://github.com/swiftlang/swift-syntax", envVar: "SWIFT_SYNTAX_VERSION", default: "509.0.0..<510.0.0") //.conditionalPackage(url: "https://github.com/swiftlang/swift-syntax", envVar: "SWIFT_SYNTAX_VERSION", default: "510.0.0..<511.0.0") //.conditionalPackage(url: "https://github.com/swiftlang/swift-syntax", envVar: "SWIFT_SYNTAX_VERSION", default: "511.0.0..<601.0.0-prerelease") From 771802b2ea57a2d6aa8cfc9dc7688bcb31848e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Galen=20O=E2=80=99Hanlon?= Date: Tue, 4 Mar 2025 13:33:30 -0800 Subject: [PATCH 5/6] Fix `@Init(type:)` and `@InitWrapper(type:)` on Swift 6.1 This addresses an incompatibility with Swift 6.1 (included in Xcode 16.3 beta) where the macro would fail when used with type parameters. In Swift 6.1, metatype syntax requires the .self suffix when used in this context. Before this fix: - Code like `@InitRaw(type: String?)` would fail with type errors in Swift 6.1 - This was a previously warned deprecation that is now enforced in Swift 6.1 The implementation now: - Correctly handles both Swift 5 style type references (without `.self`) - Supports Swift 6.1 style type references (with `.self` suffix) - Maintains backward compatibility while being future-proof Fixes #47 --- README.md | 8 +- Sources/MemberwiseInitClient/main.swift | 4 +- .../Macros/MemberwiseInitMacro.swift | 20 ++- .../CustomInitRawTests.swift | 59 +++---- .../CustomInitWrapperTests.swift | 11 +- Tests/MemberwiseInitTests/ReadmeTests.swift | 147 ++++++++++++------ 6 files changed, 162 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index 1ef02a5..62e0657 100644 --- a/README.md +++ b/README.md @@ -135,13 +135,13 @@ Attach to the property declarations of a struct that `@MemberwiseInit` is provid ### `@InitWrapper(type:)` -* `@InitWrapper(type: Binding)` +* `@InitWrapper(type: Binding.self)`
Apply this attribute to properties that are wrapped by a property wrapper and require direct initialization using the property wrapper’s type. ```swift @MemberwiseInit struct CounterView: View { - @InitWrapper(type: Binding) + @InitWrapper(type: Binding.self) @Binding var isOn: Bool var body: some View { … } @@ -150,7 +150,7 @@ Attach to the property declarations of a struct that `@MemberwiseInit` is provid > **Note** > The above `@InitWrapper` is functionally equivalent to the following `@InitRaw` configuration:
- > `@InitRaw(assignee: "self._isOn", type: Binding)`. + > `@InitRaw(assignee: "self._isOn", type: Binding.self)`. ### Etcetera @@ -476,7 +476,7 @@ import SwiftUI @MemberwiseInit struct CounterView: View { - @InitWrapper(type: Binding) + @InitWrapper(type: Binding.self) @Binding var count: Int var body: some View { … } diff --git a/Sources/MemberwiseInitClient/main.swift b/Sources/MemberwiseInitClient/main.swift index 4c715ce..65f1327 100644 --- a/Sources/MemberwiseInitClient/main.swift +++ b/Sources/MemberwiseInitClient/main.swift @@ -148,7 +148,7 @@ public struct Usage { // Some property wrappers require initialization of the property wrapper // itself, hence `@InitWrapper`. - @InitWrapper(type: Logged) + @InitWrapper(type: Logged.self) @Logged public var nameWithWrapper: String @@ -158,7 +158,7 @@ public struct Usage { assignee: "self._nameWithWrapperRaw", escaping: false, label: "_", - type: Logged + type: Logged.self ) @Logged var nameWithWrapperRaw: String diff --git a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift index d2e71d0..b60c1a4 100644 --- a/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift +++ b/Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift @@ -289,16 +289,26 @@ public struct MemberwiseInitMacro: MemberMacro { .expression .trimmedStringLiteral - let configuredType = + let typeExpr = customConfiguration? .firstWhereLabel("type")? .expression - .trimmedDescription - // TODO: Is it possible for invalid type syntax to be provided for an `Any.Type` parameter? - // NB: All expressions satisfying the `Any.Type` parameter type are parsable to TypeSyntax. + let typeString: String? = typeExpr.flatMap { expr -> String? in + // For Swift 6 style with .self suffix + if let memberAccess = expr.as(MemberAccessExprSyntax.self), + memberAccess.declName.baseName.text == "self", + let baseExpr = memberAccess.base + { + return baseExpr.trimmedDescription + } + // For Swift 5 style without .self suffix + return expr.trimmedDescription + } + + // Create TypeSyntax from the string let configuredTypeSyntax = - configuredType.map(TypeSyntax.init(stringLiteral:)) + typeString.map(TypeSyntax.init(stringLiteral:)) return VariableCustomSettings( accessLevel: configuredAccessLevel, diff --git a/Tests/MemberwiseInitTests/CustomInitRawTests.swift b/Tests/MemberwiseInitTests/CustomInitRawTests.swift index ba6f22e..dc5f149 100644 --- a/Tests/MemberwiseInitTests/CustomInitRawTests.swift +++ b/Tests/MemberwiseInitTests/CustomInitRawTests.swift @@ -5,9 +5,8 @@ import XCTest final class CustomInitRawTests: XCTestCase { override func invokeTest() { - // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8 withMacroTesting( - //indentationWidth: .spaces(2), + indentationWidth: .spaces(2), macros: [ "MemberwiseInit": MemberwiseInitMacro.self, "InitRaw": InitMacro.self, @@ -117,7 +116,7 @@ final class CustomInitRawTests: XCTestCase { """ @MemberwiseInit struct S { - @InitRaw(type: Q) var v: T + @InitRaw(type: Q.self) var v: T } """ } expansion: { @@ -140,7 +139,7 @@ final class CustomInitRawTests: XCTestCase { """ @MemberwiseInit struct S { - @InitRaw(type: Q) var v: T + @InitRaw(type: Q.self) var v: T } """ } expansion: { @@ -163,7 +162,7 @@ final class CustomInitRawTests: XCTestCase { """ @MemberwiseInit(.public) public struct S { - @InitRaw(.public, assignee: "self.foo", default: nil, escaping: true, label: "_", type: Q?) + @InitRaw(.public, assignee: "self.foo", default: nil, escaping: true, label: "_", type: Q?.self) var initRaw: T } """ @@ -182,25 +181,33 @@ final class CustomInitRawTests: XCTestCase { } } - // TODO: Add fix-it diagnostic when provided type is a Metatype - // func testTypeAsMetatype_FailsWithDiagnostic() { - // assertMacro(record: true) { - // """ - // @MemberwiseInit - // struct S { - // @Init(type: Q.self) var v: T - // } - // """ - // } diagnostics: { - // """ - // @MemberwiseInit - // struct S { - // @Init(type: Q.self) var v: T - // ┬───────────────── - // ╰─ 🛑 Invalid use of metatype 'Q.self'. Expected a type, not its metatype. - // ✏️ Remove '.self'; type is expected, not a metatype. - // } - // """ - // } - // } + // NB: In Swift 5.9, you could use `type: Q` without `.self` + // In Swift 6, you must use `type: Q.self` when referencing types as values + // + // @MemberwiseInit doesn't produce warnings/fix-its for the Swift 5.9 syntax because: + // 1. On Swift 6, the compiler already produces errors with fix-its + // 2. Adding our own diagnostics would create redundant, noisy warnings alongside compiler errors + // 3. Both syntax forms produce the correct output with proper parameter types + func testTypeReferenceCompatibility() { + assertMacro { + """ + @MemberwiseInit + struct S { + @Init(type: Q) var v: T + } + """ + } expansion: { + """ + struct S { + @Init(type: Q) var v: T + + internal init( + v: Q + ) { + self.v = v + } + } + """ + } + } } diff --git a/Tests/MemberwiseInitTests/CustomInitWrapperTests.swift b/Tests/MemberwiseInitTests/CustomInitWrapperTests.swift index 695080a..38e9849 100644 --- a/Tests/MemberwiseInitTests/CustomInitWrapperTests.swift +++ b/Tests/MemberwiseInitTests/CustomInitWrapperTests.swift @@ -5,9 +5,8 @@ import XCTest final class CustomInitWrapperTests: XCTestCase { override func invokeTest() { - // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8 withMacroTesting( - //indentationWidth: .spaces(2), + indentationWidth: .spaces(2), macros: [ "MemberwiseInit": MemberwiseInitMacro.self, "Init": InitMacro.self, @@ -23,7 +22,7 @@ final class CustomInitWrapperTests: XCTestCase { """ @MemberwiseInit struct S { - @InitWrapper(type: Q) + @InitWrapper(type: Q.self) var v: T } """ @@ -47,7 +46,7 @@ final class CustomInitWrapperTests: XCTestCase { """ @MemberwiseInit struct S { - @InitWrapper(escaping: true, type: Q) + @InitWrapper(escaping: true, type: Q.self) var v: T } """ @@ -71,7 +70,7 @@ final class CustomInitWrapperTests: XCTestCase { """ @MemberwiseInit struct S { - @InitWrapper(label: "_", type: Q) + @InitWrapper(label: "_", type: Q.self) var v: T } """ @@ -95,7 +94,7 @@ final class CustomInitWrapperTests: XCTestCase { """ @MemberwiseInit(.public) public struct S { - @InitWrapper(.public, default: Q(), escaping: true, label: "_", type: Q) + @InitWrapper(.public, default: Q(), escaping: true, label: "_", type: Q.self) var v: T } """ diff --git a/Tests/MemberwiseInitTests/ReadmeTests.swift b/Tests/MemberwiseInitTests/ReadmeTests.swift index f5eb473..8b57ff0 100644 --- a/Tests/MemberwiseInitTests/ReadmeTests.swift +++ b/Tests/MemberwiseInitTests/ReadmeTests.swift @@ -5,12 +5,12 @@ import XCTest final class ReadmeTests: XCTestCase { override func invokeTest() { - // NB: Waiting for swift-macro-testing PR to support explicit indentationWidth: https://github.com/pointfreeco/swift-macro-testing/pull/8 withMacroTesting( - //indentationWidth: .spaces(2), + indentationWidth: .spaces(2), macros: [ "MemberwiseInit": MemberwiseInitMacro.self, "Init": InitMacro.self, + "InitWrapper": InitMacro.self, "_UncheckedMemberwiseInit": UncheckedMemberwiseInitMacro.self, ] ) { @@ -162,32 +162,60 @@ final class ReadmeTests: XCTestCase { } func testBinding() { - assertMacro { - """ - @MemberwiseInit - struct CounterView: View { - @InitWrapper(type: Binding) - @Binding var isOn: Bool + #if canImport(SwiftSyntax600) + assertMacro { + """ + @MemberwiseInit + struct CounterView: View { + @InitWrapper(type: Binding.self) + @Binding var isOn: Bool - var body: some View { EmptyView() } + var body: some View { EmptyView() } + } + """ + } expansion: { + """ + struct CounterView: View { + @Binding var isOn: Bool + + var body: some View { EmptyView() } + + internal init( + isOn: Binding + ) { + self._isOn = isOn + } + } + """ } - """ - } expansion: { - """ - struct CounterView: View { - @InitWrapper(type: Binding) - @Binding var isOn: Bool + #else + assertMacro { + """ + @MemberwiseInit + struct CounterView: View { + @InitWrapper(type: Binding.self) + @Binding var isOn: Bool - var body: some View { EmptyView() } + var body: some View { EmptyView() } + } + """ + } expansion: { + """ + struct CounterView: View { + @Binding + var isOn: Bool - internal init( - isOn: Binding - ) { - self._isOn = isOn + var body: some View { EmptyView() } + + internal init( + isOn: Binding + ) { + self._isOn = isOn + } } + """ } - """ - } + #endif } func testLabelessParmeters() { @@ -448,35 +476,66 @@ final class ReadmeTests: XCTestCase { } func testSupportForPropertyWrappers() { - assertMacro { - """ - import SwiftUI + #if canImport(SwiftSyntax600) + assertMacro { + """ + import SwiftUI - @MemberwiseInit - struct CounterView: View { - @InitWrapper(type: Binding) - @Binding var count: Int + @MemberwiseInit + struct CounterView: View { + @InitWrapper(type: Binding.self) + @Binding var count: Int - var body: some View { EmptyView() } + var body: some View { EmptyView() } + } + """ + } expansion: { + """ + import SwiftUI + struct CounterView: View { + @Binding var count: Int + + var body: some View { EmptyView() } + + internal init( + count: Binding + ) { + self._count = count + } + } + """ } - """ - } expansion: { - """ - import SwiftUI - struct CounterView: View { - @InitWrapper(type: Binding) - @Binding var count: Int + #else + assertMacro { + """ + import SwiftUI - var body: some View { EmptyView() } + @MemberwiseInit + struct CounterView: View { + @InitWrapper(type: Binding.self) + @Binding var count: Int - internal init( - count: Binding - ) { - self._count = count + var body: some View { EmptyView() } } + """ + } expansion: { + """ + import SwiftUI + struct CounterView: View { + @Binding + var count: Int + + var body: some View { EmptyView() } + + internal init( + count: Binding + ) { + self._count = count + } + } + """ } - """ - } + #endif } func testAutomaticEscapingForClosureTypes() { From 028f600eb4b96f1ed4bbd1669fd6626527ed43fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Galen=20O=E2=80=99Hanlon?= Date: Tue, 4 Mar 2025 14:20:14 -0800 Subject: [PATCH 6/6] Update CI to test Swift 6.0.2 on Linux This works around an (erroneous) compiler build error on swift-issue-reporting. See: https://github.com/pointfreeco/swift-issue-reporting/issues/150 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c06221c..ce21c7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: - name: Install Swift uses: swift-actions/setup-swift@v2 with: - swift-version: "5.10" + swift-version: "6.0.2" - uses: actions/checkout@v4 - name: Run tests run: swift test