Skip to content

Support for Swift 6.1's stricter type syntax #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -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
Expand Down
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
22 changes: 20 additions & 2 deletions Package.resolved
Original file line number Diff line number Diff line change
@@ -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"
}
},
{
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ Attach to the property declarations of a struct that `@MemberwiseInit` is provid

### `@InitWrapper(type:)`

* `@InitWrapper(type: Binding<String>)`
* `@InitWrapper(type: Binding<String>.self)`
<br> 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<Bool>)
@InitWrapper(type: Binding<Bool>.self)
@Binding var isOn: Bool

var body: some View { … }
Expand All @@ -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:<br>
> `@InitRaw(assignee: "self._isOn", type: Binding<Bool>)`.
> `@InitRaw(assignee: "self._isOn", type: Binding<Bool>.self)`.

### Etcetera

Expand Down Expand Up @@ -476,7 +476,7 @@ import SwiftUI

@MemberwiseInit
struct CounterView: View {
@InitWrapper(type: Binding<Int>)
@InitWrapper(type: Binding<Int>.self)
@Binding var count: Int

var body: some View { … }
Expand Down
4 changes: 2 additions & 2 deletions Sources/MemberwiseInitClient/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ public struct Usage<T> {

// Some property wrappers require initialization of the property wrapper
// itself, hence `@InitWrapper`.
@InitWrapper(type: Logged<String>)
@InitWrapper(type: Logged<String>.self)
@Logged
public var nameWithWrapper: String

Expand All @@ -158,7 +158,7 @@ public struct Usage<T> {
assignee: "self._nameWithWrapperRaw",
escaping: false,
label: "_",
type: Logged<String>
type: Logged<String>.self
)
@Logged
var nameWithWrapperRaw: String
Expand Down
66 changes: 15 additions & 51 deletions Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)"
}
}
59 changes: 33 additions & 26 deletions Tests/MemberwiseInitTests/CustomInitRawTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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: {
Expand All @@ -140,7 +139,7 @@ final class CustomInitRawTests: XCTestCase {
"""
@MemberwiseInit
struct S {
@InitRaw(type: Q<R>) var v: T
@InitRaw(type: Q<R>.self) var v: T
}
"""
} expansion: {
Expand All @@ -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<T>?)
@InitRaw(.public, assignee: "self.foo", default: nil, escaping: true, label: "_", type: Q<T>?.self)
var initRaw: T
}
"""
Expand All @@ -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
}
}
"""
}
}
}
11 changes: 5 additions & 6 deletions Tests/MemberwiseInitTests/CustomInitWrapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -23,7 +22,7 @@ final class CustomInitWrapperTests: XCTestCase {
"""
@MemberwiseInit
struct S {
@InitWrapper(type: Q<T>)
@InitWrapper(type: Q<T>.self)
var v: T
}
"""
Expand All @@ -47,7 +46,7 @@ final class CustomInitWrapperTests: XCTestCase {
"""
@MemberwiseInit
struct S {
@InitWrapper(escaping: true, type: Q<T>)
@InitWrapper(escaping: true, type: Q<T>.self)
var v: T
}
"""
Expand All @@ -71,7 +70,7 @@ final class CustomInitWrapperTests: XCTestCase {
"""
@MemberwiseInit
struct S {
@InitWrapper(label: "_", type: Q<T>)
@InitWrapper(label: "_", type: Q<T>.self)
var v: T
}
"""
Expand All @@ -95,7 +94,7 @@ final class CustomInitWrapperTests: XCTestCase {
"""
@MemberwiseInit(.public)
public struct S {
@InitWrapper(.public, default: Q<T>(), escaping: true, label: "_", type: Q<T>)
@InitWrapper(.public, default: Q<T>(), escaping: true, label: "_", type: Q<T>.self)
var v: T
}
"""
Expand Down
Loading