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 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 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 \ 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") 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 ff92961..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, @@ -324,50 +334,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)" - } } 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() {