Skip to content

Commit b15e015

Browse files
committed
Add @_UncheckedMemberwiseInit macro (#36)
Introduce a new experimental macro for generating memberwise initializers with reduced safety checks compared to `@MemberwiseInit`. Features of `@_UncheckedMemberwiseInit`: - Include all properties in the initializer, regardless of access level - Include attributed properties by default (differs from `@MemberwiseInit`) - Allow exposure of lower access level members without per-member annotation - Has the same usage as `@MemberwiseInit` `@_UncheckedMemberwiseInit` provides a trade-off between ease of use and compile-time safety, suitable for scenarios where brevity is preferred over strict access control enforcement. Note that the underscore prefix indicates this is an experimental feature. Example: ```swift @_UnsafeMemberwiseInit(.public) public struct ViewModel { private let title: String } ``` Yields: ```swift public init(title: String) { self.title = title } ```
1 parent 1edc42e commit b15e015

File tree

6 files changed

+634
-39
lines changed

6 files changed

+634
-39
lines changed

Sources/MemberwiseInit/MemberwiseInit.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,27 @@ public macro MemberwiseInit(
3030
type: "MemberwiseInitMacro"
3131
)
3232

33+
@attached(member, names: named(init))
34+
public macro _UncheckedMemberwiseInit(
35+
_deunderscoreParameters: Bool? = nil,
36+
_optionalsDefaultNil: Bool? = nil
37+
) =
38+
#externalMacro(
39+
module: "MemberwiseInitMacros",
40+
type: "UncheckedMemberwiseInitMacro"
41+
)
42+
43+
@attached(member, names: named(init))
44+
public macro _UncheckedMemberwiseInit(
45+
_ accessLevel: AccessLevelConfig,
46+
_deunderscoreParameters: Bool? = nil,
47+
_optionalsDefaultNil: Bool? = nil
48+
) =
49+
#externalMacro(
50+
module: "MemberwiseInitMacros",
51+
type: "UncheckedMemberwiseInitMacro"
52+
)
53+
3354
// MARK: @Init macro
3455

3556
public enum IgnoreConfig {

Sources/MemberwiseInitMacros/MacroPlugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ struct MemberwiseInitPlugin: CompilerPlugin {
66
let providingMacros: [Macro.Type] = [
77
InitMacro.self,
88
MemberwiseInitMacro.self,
9+
UncheckedMemberwiseInitMacro.self,
910
]
1011
}

Sources/MemberwiseInitMacros/Macros/MemberwiseInitMacro.swift

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -47,44 +47,14 @@ public struct MemberwiseInitMacro: MemberMacro {
4747
)
4848
diagnostics.forEach { context.diagnose($0) }
4949

50-
func formatParameters() -> String {
51-
guard !properties.isEmpty else { return "" }
52-
53-
return "\n"
54-
+ properties
55-
.map { property in
56-
formatParameter(
57-
for: property,
58-
considering: properties,
59-
deunderscoreParameters: deunderscoreParameters,
60-
optionalsDefaultNil: optionalsDefaultNil
61-
?? defaultOptionalsDefaultNil(
62-
for: property.keywordToken,
63-
initAccessLevel: accessLevel
64-
)
65-
)
66-
}
67-
.joined(separator: ",\n")
68-
+ "\n"
69-
}
70-
71-
let formattedInitSignature = "\n\(accessLevel) init(\(formatParameters()))"
7250
return [
7351
DeclSyntax(
74-
try InitializerDeclSyntax(SyntaxNodeString(stringLiteral: formattedInitSignature)) {
75-
CodeBlockItemListSyntax(
76-
properties
77-
.map { property in
78-
CodeBlockItemSyntax(
79-
stringLiteral: formatInitializerAssignmentStatement(
80-
for: property,
81-
considering: properties,
82-
deunderscoreParameters: deunderscoreParameters
83-
)
84-
)
85-
}
86-
)
87-
}
52+
MemberwiseInitFormatter.formatInitializer(
53+
properties: properties,
54+
accessLevel: accessLevel,
55+
deunderscoreParameters: deunderscoreParameters,
56+
optionalsDefaultNil: optionalsDefaultNil
57+
)
8858
)
8959
]
9060
}
@@ -107,7 +77,7 @@ public struct MemberwiseInitMacro: MemberMacro {
10777
return nil
10878
}
10979

110-
private static func extractLabeledBoolArgument(
80+
static func extractLabeledBoolArgument(
11181
_ label: String,
11282
from node: AttributeSyntax
11383
) -> Bool? {
@@ -269,7 +239,7 @@ public struct MemberwiseInitMacro: MemberMacro {
269239
}
270240
}
271241

272-
private static func extractVariableCustomSettings(
242+
static func extractVariableCustomSettings(
273243
from variable: VariableDeclSyntax
274244
) -> VariableCustomSettings? {
275245
guard let customConfigurationAttribute = variable.customConfigurationAttribute else {
@@ -342,7 +312,7 @@ public struct MemberwiseInitMacro: MemberMacro {
342312
)
343313
}
344314

345-
private static func defaultOptionalsDefaultNil(
315+
static func defaultOptionalsDefaultNil(
346316
for bindingKeyword: TokenKind,
347317
initAccessLevel: AccessLevelModifier
348318
) -> Bool {
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxBuilder
3+
4+
struct MemberwiseInitFormatter {
5+
static func formatInitializer(
6+
properties: [MemberProperty],
7+
accessLevel: AccessLevelModifier,
8+
deunderscoreParameters: Bool,
9+
optionalsDefaultNil: Bool?
10+
) -> InitializerDeclSyntax {
11+
let formattedParameters = formatParameters(
12+
properties: properties,
13+
deunderscoreParameters: deunderscoreParameters,
14+
optionalsDefaultNil: optionalsDefaultNil,
15+
accessLevel: accessLevel
16+
)
17+
18+
let formattedInitSignature = "\n\(accessLevel) init(\(formattedParameters))"
19+
20+
return try! InitializerDeclSyntax(SyntaxNodeString(stringLiteral: formattedInitSignature)) {
21+
CodeBlockItemListSyntax(
22+
properties.map { property in
23+
CodeBlockItemSyntax(
24+
stringLiteral: formatInitializerAssignmentStatement(
25+
for: property,
26+
considering: properties,
27+
deunderscoreParameters: deunderscoreParameters
28+
)
29+
)
30+
}
31+
)
32+
}
33+
}
34+
35+
private static func formatParameters(
36+
properties: [MemberProperty],
37+
deunderscoreParameters: Bool,
38+
optionalsDefaultNil: Bool?,
39+
accessLevel: AccessLevelModifier
40+
) -> String {
41+
guard !properties.isEmpty else { return "" }
42+
43+
return "\n"
44+
+ properties
45+
.map { property in
46+
formatParameter(
47+
for: property,
48+
considering: properties,
49+
deunderscoreParameters: deunderscoreParameters,
50+
optionalsDefaultNil: optionalsDefaultNil
51+
?? MemberwiseInitMacro.defaultOptionalsDefaultNil(
52+
for: property.keywordToken,
53+
initAccessLevel: accessLevel
54+
)
55+
)
56+
}
57+
.joined(separator: ",\n") + "\n"
58+
}
59+
60+
private static func formatParameter(
61+
for property: MemberProperty,
62+
considering allProperties: [MemberProperty],
63+
deunderscoreParameters: Bool,
64+
optionalsDefaultNil: Bool
65+
) -> String {
66+
let defaultValue =
67+
property.initializerValue.map { " = \($0.description)" }
68+
?? property.customSettings?.defaultValue.map { " = \($0)" }
69+
?? (optionalsDefaultNil && property.type.isOptionalType ? " = nil" : "")
70+
71+
let escaping =
72+
(property.customSettings?.forceEscaping ?? false || property.type.isFunctionType)
73+
? "@escaping " : ""
74+
75+
let label = property.initParameterLabel(
76+
considering: allProperties, deunderscoreParameters: deunderscoreParameters)
77+
78+
let parameterName = property.initParameterName(
79+
considering: allProperties, deunderscoreParameters: deunderscoreParameters)
80+
81+
return "\(label)\(parameterName): \(escaping)\(property.type.description)\(defaultValue)"
82+
}
83+
84+
private static func formatInitializerAssignmentStatement(
85+
for property: MemberProperty,
86+
considering allProperties: [MemberProperty],
87+
deunderscoreParameters: Bool
88+
) -> String {
89+
let assignee =
90+
switch property.customSettings?.assignee {
91+
case .none:
92+
"self.\(property.name)"
93+
case .wrapper:
94+
"self._\(property.name)"
95+
case let .raw(assignee):
96+
assignee
97+
}
98+
99+
let parameterName = property.initParameterName(
100+
considering: allProperties,
101+
deunderscoreParameters: deunderscoreParameters
102+
)
103+
return "\(assignee) = \(parameterName)"
104+
}
105+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import SwiftCompilerPlugin
2+
import SwiftDiagnostics
3+
import SwiftSyntax
4+
import SwiftSyntaxBuilder
5+
import SwiftSyntaxMacroExpansion
6+
import SwiftSyntaxMacros
7+
8+
public struct UncheckedMemberwiseInitMacro: MemberMacro {
9+
public static func expansion<D, C>(
10+
of node: AttributeSyntax,
11+
providingMembersOf decl: D,
12+
in context: C
13+
) throws -> [SwiftSyntax.DeclSyntax]
14+
where D: DeclGroupSyntax, C: MacroExpansionContext {
15+
guard [SwiftSyntax.SyntaxKind.classDecl, .structDecl, .actorDecl].contains(decl.kind) else {
16+
throw MacroExpansionErrorMessage(
17+
"""
18+
@_UncheckedMemberwiseInit can only be attached to a struct, class, or actor; \
19+
not to \(decl.descriptiveDeclKind(withArticle: true)).
20+
"""
21+
)
22+
}
23+
24+
let accessLevel =
25+
MemberwiseInitMacro.extractConfiguredAccessLevel(from: node) ?? .internal
26+
let optionalsDefaultNil: Bool? =
27+
MemberwiseInitMacro.extractLabeledBoolArgument("_optionalsDefaultNil", from: node)
28+
let deunderscoreParameters: Bool =
29+
MemberwiseInitMacro.extractLabeledBoolArgument("_deunderscoreParameters", from: node) ?? false
30+
31+
let properties = try collectUncheckedMemberProperties(
32+
from: decl.memberBlock.members
33+
)
34+
35+
return [
36+
DeclSyntax(
37+
MemberwiseInitFormatter.formatInitializer(
38+
properties: properties,
39+
accessLevel: accessLevel,
40+
deunderscoreParameters: deunderscoreParameters,
41+
optionalsDefaultNil: optionalsDefaultNil
42+
)
43+
)
44+
]
45+
}
46+
47+
private static func collectUncheckedMemberProperties(
48+
from memberBlockItemList: MemberBlockItemListSyntax
49+
) throws -> [MemberProperty] {
50+
memberBlockItemList.compactMap { member -> MemberProperty? in
51+
guard let variable = member.decl.as(VariableDeclSyntax.self),
52+
!variable.isComputedProperty,
53+
variable.modifiersExclude([.static, .lazy]),
54+
let binding = variable.bindings.first,
55+
let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text,
56+
let type = binding.typeAnnotation?.type ?? binding.initializer?.value.inferredTypeSyntax
57+
else { return nil }
58+
59+
let customSettings = MemberwiseInitMacro.extractVariableCustomSettings(from: variable)
60+
if customSettings?.ignore == true {
61+
return nil
62+
}
63+
64+
return MemberProperty(
65+
accessLevel: variable.accessLevel,
66+
customSettings: customSettings,
67+
initializerValue: binding.initializer?.value,
68+
keywordToken: variable.bindingSpecifier.tokenKind,
69+
name: name,
70+
type: type.trimmed
71+
)
72+
}
73+
}
74+
}

0 commit comments

Comments
 (0)