Skip to content
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
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Informed by explicit developer cues, MemberwiseInit can more often automatically
* [Infer type from property initialization expressions](#infer-type-from-property-initialization-expressions)
* [Explicitly ignore properties](#explicitly-ignore-properties)
* [Attributed properties are ignored by default, but includable](#attributed-properties-are-ignored-by-default-but-includable)
* [Support for property wrappers](#support-for-property-wrappers)
* [Automatic `@escaping` for closure types (usually)](#automatic-escaping-for-closure-types-usually)
* [Experimental: Deunderscore parameter names](#experimental-deunderscore-parameter-names)
* [Experimental: Defaulting optionals to nil](#experimental-defaulting-optionals-to-nil)
Expand Down Expand Up @@ -99,7 +100,7 @@ To use MemberwiseInit:

## Quick reference

MemberwiseInit includes two autocomplete-friendly macros:
MemberwiseInit includes two macros:

### `@MemberwiseInit`

Expand Down Expand Up @@ -145,6 +146,20 @@ Attach to member property declarations of a struct, actor, or class that `@Membe
* `@Init(.public, label: String)`
<br> Custom labels can be combined with all other behaviors.

* `@Init(assignee: String)`
<br> Override the target property to which the initializer argument should be assigned. By default, a property named “property” has the assignee `self.property`, as demonstrated in `self.property = property`.

* `@Init(type: Any.Type)`
<br> Override the type of the argument in the initializer.

* `@Init(assignee: String, type: Any.Type)`
<br> Combine `assignee` and `type` to support usage with property wrappers:

```swift
@Init(assignee: "self._isOn", type: Binding<Bool>)
@Binding var isOn = true
```

## Features and limitations

### Custom `init` parameter labels
Expand Down Expand Up @@ -381,6 +396,22 @@ From here, you have two alternatives:
}
```

### Support for property wrappers

Combine `@Init(assignee:)` and `@Init(type)` to support property wrappers. For example, here’s a simple usage with SwiftUI’s `@Binding`:

```swift
import SwiftUI

@MemberwiseInit
struct CounterView: View {
@Init(assignee: “self._count”, type: Binding<Int>)
@Binding var count = 0

var body: some View { … }
}
```

### Automatic `@escaping` for closure types (usually)

MemberwiseInit automatically marks closures in initializer parameters as `@escaping`. If using a typealias for a closure, explicitly annotate the property with `@Init(.escaping)`.
Expand Down
33 changes: 12 additions & 21 deletions Sources/MemberwiseInit/MemberwiseInit.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public enum AccessLevelConfig {

@attached(member, names: named(init))
public macro MemberwiseInit(
_ accessLevel: AccessLevelConfig = .internal,
_deunderscoreParameters: Bool = false,
_ accessLevel: AccessLevelConfig? = nil,
_deunderscoreParameters: Bool? = nil,
_optionalsDefaultNil: Bool? = nil
) =
#externalMacro(
Expand All @@ -30,16 +30,13 @@ public enum IgnoreConfig {
case ignore
}

@attached(peer)
public macro Init() =
#externalMacro(
module: "MemberwiseInitMacros",
type: "InitMacro"
)

@attached(peer)
public macro Init(
_ ignore: IgnoreConfig
_ accessLevel: AccessLevelConfig? = nil,
assignee: String? = nil,
escaping: Bool? = nil,
label: String? = nil,
type: Any.Type? = nil
) =
#externalMacro(
module: "MemberwiseInitMacros",
Expand All @@ -48,34 +45,28 @@ public macro Init(

@attached(peer)
public macro Init(
label: String
_ ignore: IgnoreConfig
) =
#externalMacro(
module: "MemberwiseInitMacros",
type: "InitMacro"
)

@attached(peer)
public macro Init(
_ accessLevel: AccessLevelConfig,
_ escaping: EscapingConfig,
label: String? = nil
) =
#externalMacro(
module: "MemberwiseInitMacros",
type: "InitMacro"
)
// MARK: - Deprecated

// Deprecated; remove in 1.0
@attached(peer)
public macro Init(
_ accessLevel: AccessLevelConfig,
_ escaping: EscapingConfig,
label: String? = nil
) =
#externalMacro(
module: "MemberwiseInitMacros",
type: "InitMacro"
)

// Deprecated; remove in 1.0
@attached(peer)
public macro Init(
_ escaping: EscapingConfig,
Expand Down
45 changes: 32 additions & 13 deletions Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,16 +152,17 @@ public struct MemberwiseInitMacro: MemberMacro {
}

let type =
binding.typeAnnotation?.type
customSettings?.type
?? binding.typeAnnotation?.type
?? binding.initializer?.value.inferredTypeSyntax
?? acc.typeFromTrailingBinding

acc.bindings.append(
PropertyBinding(
accessLevel: variable.accessLevel,
adoptedType: type,
binding: binding,
customSettings: customSettings,
effectiveType: type,
keywordToken: variable.bindingSpecifier.tokenKind
)
)
Expand All @@ -181,9 +182,7 @@ public struct MemberwiseInitMacro: MemberMacro {
if propertyBinding.isComputedProperty || propertyBinding.isPreinitializedLet {
return (properties, diagnostics)
}
if propertyBinding.isPreinitializedVarWithoutType,
propertyBinding.initializer?.inferredTypeSyntax == nil
{
if propertyBinding.isPreinitializedVarWithoutType {
return (
properties,
diagnostics + [propertyBinding.diagnostic(message: .missingTypeForVarProperty)]
Expand Down Expand Up @@ -280,24 +279,36 @@ public struct MemberwiseInitMacro: MemberMacro {

let configuredIgnore = configuredValues.contains("ignore")
let configuredForceEscaping = configuredValues.contains("escaping")

let configuredAccessLevel =
configuredValues
.compactMap(AccessLevelModifier.init(rawValue:))
.first

let configuredLabel =
memberConfiguration
.first(where: { $0.label?.text == "label" })?
.expression
.as(StringLiteralExprSyntax.self)?
.segments
.trimmedDescription
.trimmingCharacters(in: .whitespacesAndNewlines)
.trimmedStringLiteral

let configuredType =
memberConfiguration
.first(where: { $0.label?.text == "type" })?
.expression

let configuredAssignee =
memberConfiguration
.first(where: { $0.label?.text == "assignee" })?
.expression
.trimmedStringLiteral

return PropertyCustomSettings(
accessLevel: configuredAccessLevel,
assignee: configuredAssignee,
forceEscaping: configuredForceEscaping,
ignore: configuredIgnore,
label: configuredLabel,
type: configuredType.map { TypeSyntax(stringLiteral: $0.trimmedDescription) },
_syntaxNode: memberConfiguration
)
}
Expand Down Expand Up @@ -340,15 +351,22 @@ public struct MemberwiseInitMacro: MemberMacro {
considering allProperties: [MemberProperty],
deunderscoreParameters: Bool
) -> String {
"self.\(property.name) = \(property.initParameterName(considering: allProperties, deunderscoreParameters: deunderscoreParameters))"
let assignee = property.customSettings?.assignee ?? "self.\(property.name)"
let parameterName = property.initParameterName(
considering: allProperties,
deunderscoreParameters: deunderscoreParameters
)
return "\(assignee) = \(parameterName)"
}
}

private struct PropertyCustomSettings: Equatable {
let accessLevel: AccessLevelModifier?
let assignee: String?
let forceEscaping: Bool
let ignore: Bool
let label: String?
let type: TypeSyntax?
let _syntaxNode: LabeledExprListSyntax

func diagnostic(message: MemberwiseInitMacroDiagnostic) -> Diagnostic {
Expand Down Expand Up @@ -380,14 +398,14 @@ private struct PropertyBinding {
// Or, store `binding` and add a bunch of computed properties.
init(
accessLevel: AccessLevelModifier,
adoptedType: TypeSyntax?,
binding: PatternBindingSyntax,
customSettings: PropertyCustomSettings?,
effectiveType: TypeSyntax?,
keywordToken: TokenKind
) {
self.accessLevel = accessLevel
self.customSettings = customSettings
self.effectiveType = binding.typeAnnotation?.type ?? adoptedType
self.effectiveType = effectiveType
self.initializer = binding.initializer?.trimmed.value
self.isComputedProperty = binding.isComputedProperty
self.isTuplePattern = binding.pattern.isTuplePattern
Expand All @@ -398,8 +416,9 @@ private struct PropertyBinding {

var isPreinitializedVarWithoutType: Bool {
self.initializer != nil
&& self.effectiveType == nil
&& self.keywordToken == .keyword(.var)
&& self.effectiveType == nil
&& self.initializer?.inferredTypeSyntax == nil
}

var isPreinitializedLet: Bool {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,12 @@ extension PatternSyntax {
self.as(TuplePatternSyntax.self) != nil
}
}

extension ExprSyntax {
var trimmedStringLiteral: String? {
self.as(StringLiteralExprSyntax.self)?
.segments
.trimmedDescription
.trimmingCharacters(in: .whitespacesAndNewlines)
}
}
95 changes: 95 additions & 0 deletions Tests/MemberwiseInitTests/MemberwiseInitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,101 @@ final class MemberwiseInitTests: XCTestCase {
}
}

// MARK: - Test custom type

func testCustomType() {
assertMacro {
"""
@MemberwiseInit
struct S {
@Init(type: Q) var type: T
}
"""
} expansion: {
"""
struct S {
var type: T

internal init(
type: Q
) {
self.type = type
}
}
"""
}
}

func testCustomType_GenericExpression() {
assertMacro {
"""
@MemberwiseInit
struct S {
@Init(type: Q<R>) var type: T
}
"""
} expansion: {
"""
struct S {
var type: T

internal init(
type: Q<R>
) {
self.type = type
}
}
"""
}
}

// TODO: Add fix-it diagnostic when provided type is a Metatype
// func testCustomType_MetatypeFailsWithDiagnostic() {
// assertMacro(record: true) {
// """
// @MemberwiseInit
// struct S {
// @Init(type: Q.self) var type: T
// }
// """
// } diagnostics: {
// """
// @MemberwiseInit
// struct S {
// @Init(type: Q.self) var type: T
// ┬─────────────────
// ╰─ 🛑 Invalid use of metatype 'Q.self'. Expected a type, not its metatype.
// ╰─ 🛑 Remove '.self'; type is expected, not a metatype.
// }
// """
// }
// }

// MARK: - Test custom assign

func testCustomAssign() {
assertMacro {
"""
@MemberwiseInit
struct S {
@Init(assignee: "self._type") var type: T
}
"""
} expansion: {
"""
struct S {
var type: T

internal init(
type: T
) {
self._type = type
}
}
"""
}
}

// MARK: - Test simple usage

// NB: Redundant to AccessLevelTests but handy to have here, too.
Expand Down