-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 } ```
- Loading branch information
Showing
6 changed files
with
634 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
Sources/MemberwiseInitMacros/Macros/Support/MemberwiseInitFormatter.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
|
||
struct MemberwiseInitFormatter { | ||
static func formatInitializer( | ||
properties: [MemberProperty], | ||
accessLevel: AccessLevelModifier, | ||
deunderscoreParameters: Bool, | ||
optionalsDefaultNil: Bool? | ||
) -> InitializerDeclSyntax { | ||
let formattedParameters = formatParameters( | ||
properties: properties, | ||
deunderscoreParameters: deunderscoreParameters, | ||
optionalsDefaultNil: optionalsDefaultNil, | ||
accessLevel: accessLevel | ||
) | ||
|
||
let formattedInitSignature = "\n\(accessLevel) init(\(formattedParameters))" | ||
|
||
return try! InitializerDeclSyntax(SyntaxNodeString(stringLiteral: formattedInitSignature)) { | ||
CodeBlockItemListSyntax( | ||
properties.map { property in | ||
CodeBlockItemSyntax( | ||
stringLiteral: formatInitializerAssignmentStatement( | ||
for: property, | ||
considering: properties, | ||
deunderscoreParameters: deunderscoreParameters | ||
) | ||
) | ||
} | ||
) | ||
} | ||
} | ||
|
||
private static func formatParameters( | ||
properties: [MemberProperty], | ||
deunderscoreParameters: Bool, | ||
optionalsDefaultNil: Bool?, | ||
accessLevel: AccessLevelModifier | ||
) -> String { | ||
guard !properties.isEmpty else { return "" } | ||
|
||
return "\n" | ||
+ properties | ||
.map { property in | ||
formatParameter( | ||
for: property, | ||
considering: properties, | ||
deunderscoreParameters: deunderscoreParameters, | ||
optionalsDefaultNil: optionalsDefaultNil | ||
?? MemberwiseInitMacro.defaultOptionalsDefaultNil( | ||
for: property.keywordToken, | ||
initAccessLevel: accessLevel | ||
) | ||
) | ||
} | ||
.joined(separator: ",\n") + "\n" | ||
} | ||
|
||
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)" } | ||
?? (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)" | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
Sources/MemberwiseInitMacros/Macros/UncheckedMemberwiseInitMacro.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import SwiftCompilerPlugin | ||
import SwiftDiagnostics | ||
import SwiftSyntax | ||
import SwiftSyntaxBuilder | ||
import SwiftSyntaxMacroExpansion | ||
import SwiftSyntaxMacros | ||
|
||
public struct UncheckedMemberwiseInitMacro: MemberMacro { | ||
public static func expansion<D, C>( | ||
of node: AttributeSyntax, | ||
providingMembersOf decl: D, | ||
in context: C | ||
) throws -> [SwiftSyntax.DeclSyntax] | ||
where D: DeclGroupSyntax, C: MacroExpansionContext { | ||
guard [SwiftSyntax.SyntaxKind.classDecl, .structDecl, .actorDecl].contains(decl.kind) else { | ||
throw MacroExpansionErrorMessage( | ||
""" | ||
@_UncheckedMemberwiseInit can only be attached to a struct, class, or actor; \ | ||
not to \(decl.descriptiveDeclKind(withArticle: true)). | ||
""" | ||
) | ||
} | ||
|
||
let accessLevel = | ||
MemberwiseInitMacro.extractConfiguredAccessLevel(from: node) ?? .internal | ||
let optionalsDefaultNil: Bool? = | ||
MemberwiseInitMacro.extractLabeledBoolArgument("_optionalsDefaultNil", from: node) | ||
let deunderscoreParameters: Bool = | ||
MemberwiseInitMacro.extractLabeledBoolArgument("_deunderscoreParameters", from: node) ?? false | ||
|
||
let properties = try collectUncheckedMemberProperties( | ||
from: decl.memberBlock.members | ||
) | ||
|
||
return [ | ||
DeclSyntax( | ||
MemberwiseInitFormatter.formatInitializer( | ||
properties: properties, | ||
accessLevel: accessLevel, | ||
deunderscoreParameters: deunderscoreParameters, | ||
optionalsDefaultNil: optionalsDefaultNil | ||
) | ||
) | ||
] | ||
} | ||
|
||
private static func collectUncheckedMemberProperties( | ||
from memberBlockItemList: MemberBlockItemListSyntax | ||
) throws -> [MemberProperty] { | ||
memberBlockItemList.compactMap { member -> MemberProperty? in | ||
guard let variable = member.decl.as(VariableDeclSyntax.self), | ||
!variable.isComputedProperty, | ||
variable.modifiersExclude([.static, .lazy]), | ||
let binding = variable.bindings.first, | ||
let name = binding.pattern.as(IdentifierPatternSyntax.self)?.identifier.text, | ||
let type = binding.typeAnnotation?.type ?? binding.initializer?.value.inferredTypeSyntax | ||
else { return nil } | ||
|
||
let customSettings = MemberwiseInitMacro.extractVariableCustomSettings(from: variable) | ||
if customSettings?.ignore == true { | ||
return nil | ||
} | ||
|
||
return MemberProperty( | ||
accessLevel: variable.accessLevel, | ||
customSettings: customSettings, | ||
initializerValue: binding.initializer?.value, | ||
keywordToken: variable.bindingSpecifier.tokenKind, | ||
name: name, | ||
type: type.trimmed | ||
) | ||
} | ||
} | ||
} |
Oops, something went wrong.