-
Notifications
You must be signed in to change notification settings - Fork 9
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)
A new experimental macro for generating memberwise initializers without the safety checks present in the standard `MemberwiseInit` macro. It simplifies usage by allowing exposure of lower access level members in initializers without per-member annotation, at the cost of losing some compile-time checks.
- Loading branch information
Showing
6 changed files
with
442 additions
and
38 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)" | ||
} | ||
} |
72 changes: 72 additions & 0 deletions
72
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,72 @@ | ||
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 configuredAccessLevel = | ||
MemberwiseInitMacro.extractConfiguredAccessLevel(from: node) ?? .internal | ||
let optionalsDefaultNil: Bool? = | ||
MemberwiseInitMacro.extractLabeledBoolArgument("_optionalsDefaultNil", from: node) | ||
let deunderscoreParameters: Bool = | ||
MemberwiseInitMacro.extractLabeledBoolArgument("_deunderscoreParameters", from: node) ?? false | ||
|
||
let accessLevel = configuredAccessLevel | ||
let properties = try collectUncheckedMemberProperties( | ||
from: decl.memberBlock.members, | ||
targetAccessLevel: accessLevel | ||
) | ||
|
||
return [ | ||
DeclSyntax( | ||
MemberwiseInitFormatter.formatInitializer( | ||
properties: properties, | ||
accessLevel: accessLevel, | ||
deunderscoreParameters: deunderscoreParameters, | ||
optionalsDefaultNil: optionalsDefaultNil | ||
) | ||
) | ||
] | ||
} | ||
|
||
private static func collectUncheckedMemberProperties( | ||
from memberBlockItemList: MemberBlockItemListSyntax, | ||
targetAccessLevel: AccessLevelModifier | ||
) 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 } | ||
|
||
return MemberProperty( | ||
accessLevel: variable.accessLevel, | ||
customSettings: nil, | ||
initializerValue: binding.initializer?.value, | ||
keywordToken: variable.bindingSpecifier.tokenKind, | ||
name: name, | ||
type: type.trimmed | ||
) | ||
} | ||
} | ||
} |
Oops, something went wrong.