Skip to content
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

[Feature Request] 1. Default value by InitializerClauseSyntax 2. Add GroupedDefault for multiple bindings 3. Some builtin type infer #70

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
34 changes: 34 additions & 0 deletions Sources/MacroPlugin/Definitions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,40 @@ struct Default: PeerMacro {
}
}

/// Attribute type for `GroupedDefault` macro-attribute.
///
/// This type can validate`GroupedDefault` macro-attribute
/// usage and extract data for `Codable` macro to
/// generate implementation.
struct GroupedDefault: PeerMacro {
/// Provide metadata to `Codable` macro for final expansion
/// and verify proper usage of this macro.
///
/// This macro doesn't perform any expansion rather `Codable` macro
/// uses when performing expansion.
///
/// This macro verifies that macro usage condition is met by attached
/// declaration by using the `validate` implementation provided.
///
/// - Parameters:
/// - node: The attribute describing this macro.
/// - declaration: The declaration this macro attribute is attached to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: No declaration is returned, only attached declaration is
/// analyzed.
static func expansion(
of node: AttributeSyntax,
providingPeersOf declaration: some DeclSyntaxProtocol,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
return try PluginCore.GroupedDefault.expansion(
of: node, providingPeersOf: declaration, in: context
)
}
}


/// Attribute type for `CodedAs` macro-attribute.
///
/// This type can validate`CodedAs` macro-attribute
Expand Down
1 change: 1 addition & 0 deletions Sources/MacroPlugin/Plugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct MetaCodablePlugin: CompilerPlugin {
MemberInit.self,
CodingKeys.self,
IgnoreCodingInitialized.self,
GroupedDefault.self,
Inherits.self,
]
}
29 changes: 29 additions & 0 deletions Sources/MetaCodable/GroupedDefault.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/// Provides a `GroupedDefault` value to be used when decoding fails and
/// when not initialized explicitly in memberwise initializer(s).
///
/// If the value is missing or has incorrect data type, the default value
/// will be used instead of throwing error and terminating decoding.
/// i.e. for a field declared as:
/// ```swift
/// @GroupedDefault("some", 10)
/// let field: String, field2: Int
/// ```
/// if empty json provided or type at `CodingKey` is different
/// ```json
/// { "field": 5 } // or {}
/// ```
/// the default value provided in this case `some` will be used as
/// `field`'s value.
///
/// - Parameter defaults: The defaults value to use.
///
/// - Note: This macro on its own only validates if attached declaration
/// is a variable declaration. ``Codable()`` macro uses this macro
/// when generating final implementations.
///
/// - Important: The field type must confirm to `Codable` and
/// default value type `T` must be the same as field type.
@attached(peer)
@available(swift 5.9)
public macro GroupedDefault<each T>(_ defaults: repeat each T) =
#externalMacro(module: "MacroPlugin", type: "GroupedDefault")
2 changes: 1 addition & 1 deletion Sources/PluginCore/Attributes/Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ package struct Default: PropertyAttribute {
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
expect(syntaxes: VariableDeclSyntax.self)
attachedToUngroupedVariable()
attachedToNonStaticVariable()
cantDuplicate()
cantBeCombined(with: IgnoreCoding.self)
Expand Down
118 changes: 118 additions & 0 deletions Sources/PluginCore/Attributes/GroupedDefault.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
@_implementationOnly import SwiftSyntax
@_implementationOnly import SwiftSyntaxMacros

/// Attribute type for `GroupedDefault` macro-attribute.
///
/// This type can validate`GroupedDefault` macro-attribute
/// usage and extract data for `Codable` macro to
/// generate implementation.
package struct GroupedDefault: PropertyAttribute {
/// The node syntax provided
/// during initialization.
let node: AttributeSyntax

/// The default value expressions provided.
var exprs: [ExprSyntax] {
node.arguments!.as(LabeledExprListSyntax.self)!.map {
$0.expression
}
}

/// Creates a new instance with the provided node.
///
/// The initializer fails to create new instance if the name
/// of the provided node is different than this attribute.
///
/// - Parameter node: The attribute syntax to create with.
/// - Returns: Newly created attribute instance.
init?(from node: AttributeSyntax) {
guard
node.attributeName.as(IdentifierTypeSyntax.self)!
.name.text == Self.name
else { return nil }
self.node = node
}

/// Builds diagnoser that can validate this macro
/// attached declaration.
///
/// The following conditions are checked by the
/// built diagnoser:
/// * Attached declaration is a variable declaration.
/// * Attached declaration is not a static variable
/// declaration
/// * Macro usage is not duplicated for the same
/// declaration.
/// * This attribute isn't used combined with
/// `IgnoreCoding` attribute.
///
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
attachedToGroupedVariable()
attachedToNonStaticVariable()
cantDuplicate()
cantBeCombined(with: IgnoreCoding.self)
}
}
}

extension Registration
where
Decl == PropertyDeclSyntax, Var: PropertyVariable & InitializableVariable, Var.Initialization == AnyRequiredVariableInitialization, Var == AnyPropertyVariable<AnyRequiredVariableInitialization>
{
/// Update registration with binding initializer value.
///
/// New registration is updated with default expression data that will be
/// used for decoding failure and memberwise initializer(s), if provided.
///
/// - Returns: Newly built registration with default expression data or self.
func addDefaultValueIfInitializerExists() -> Self {
guard Default(from: self.decl) == nil, GroupedDefault(from: self.decl) == nil, let value = decl.binding.initializer?.value, let variable = self.variable.base as? AnyPropertyVariable<RequiredInitialization> else {
return self
}

let newVar = variable.with(default: value)
return self.updating(with: newVar.any)
}

/// Update registration with pattern binding default values if exists.
///
/// New registration is updated with default expression data that will be
/// used for decoding failure and memberwise initializer(s), if provided.
///
/// - Returns: Newly built registration with default expression data or self.
func addGroupedDefaultIfExists() -> Self {
guard let defaults = GroupedDefault(from: self.decl) else {
return self
}

var i: Int = 0
for (index, binding) in self.decl.decl.bindings.enumerated() {
if binding.pattern.description == self.decl.binding.pattern.description {
i = index
break
}
}

guard let variable = self.variable.base as? AnyPropertyVariable<RequiredInitialization>
else { return self }

let newVar = variable.with(default: defaults.exprs[i])
return self.updating(with: newVar.any)
}
}

fileprivate extension PropertyVariable
where Initialization == RequiredInitialization {
/// Update variable data with the default value expression provided.
///
/// `DefaultValueVariable` is created with this variable as base
/// and default expression provided.
///
/// - Parameter expr: The default expression to add.
/// - Returns: Created variable data with default expression.
func with(default expr: ExprSyntax) -> DefaultValueVariable<Self> {
return .init(base: self, options: .init(expr: expr))
}
}
23 changes: 19 additions & 4 deletions Sources/PluginCore/Diagnostics/GroupedVariableDeclaration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ struct GroupedVariableDeclaration<Attr: PropertyAttribute>: DiagnosticProducer {
/// in generated diagnostic
/// messages.
let attr: Attr
/// The attribute is attatch to multiple bindings variable.
let isAttach: Bool
/// Underlying producer that validates passed syntax is variable
/// declaration.
///
Expand All @@ -36,9 +38,11 @@ struct GroupedVariableDeclaration<Attr: PropertyAttribute>: DiagnosticProducer {
///
/// - Parameter attr: The attribute for which
/// validation performed.
/// - Parameter isAttach: can attach to multiple bindings.
/// - Returns: Newly created diagnostic producer.
init(_ attr: Attr) {
init(_ attr: Attr, isAttach: Bool) {
self.attr = attr
self.isAttach = isAttach
self.base = .init(attr, expect: [VariableDeclSyntax.self])
}

Expand All @@ -60,11 +64,11 @@ struct GroupedVariableDeclaration<Attr: PropertyAttribute>: DiagnosticProducer {
in context: some MacroExpansionContext
) -> Bool {
guard !base.produce(for: syntax, in: context) else { return true }
guard syntax.as(VariableDeclSyntax.self)!.bindings.count > 1
guard (!isAttach && syntax.as(VariableDeclSyntax.self)!.bindings.count > 1) || (isAttach && syntax.as(VariableDeclSyntax.self)!.bindings.count == 1)
else { return false }
let message = attr.diagnostic(
message:
"@\(attr.name) can't be used with grouped variables declaration",
isAttach ? "@\(attr.name) can't be used with single variables declaration" : "@\(attr.name) can't be used with grouped variables declaration",
id: attr.misuseMessageID,
severity: .error
)
Expand All @@ -82,6 +86,17 @@ extension PropertyAttribute {
///
/// - Returns: Grouped variable declaration validation diagnostic producer.
func attachedToUngroupedVariable() -> GroupedVariableDeclaration<Self> {
return .init(self)
return .init(self, isAttach: false)
}

/// Indicates attribute must be attached to multiple bindings variable declaration.
///
/// The created diagnostic producer produces error diagnostic,
/// if attribute is attached to grouped variable and non-variable
/// declarations.
///
/// - Returns: Grouped variable declaration validation diagnostic producer.
func attachedToGroupedVariable() -> GroupedVariableDeclaration<Self> {
return .init(self, isAttach: true)
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,3 @@ package protocol RequiredVariableInitialization: VariableInitialization {
/// generating initializer.
var code: CodeBlockItemSyntax { get }
}

extension RequiredVariableInitialization {
/// Converts initialization to optional from required initialization.
///
/// Wraps current instance in `OptionalInitialization`.
var optionalize: OptionalInitialization<Self> {
return .init(base: self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,7 @@ where
in context: some MacroExpansionContext
) -> AnyInitialization {
return if options.`init` {
if options.initialized {
base.initializing(in: context).optionalize.any
} else {
base.initializing(in: context).any
}
base.initializing(in: context).any
} else {
IgnoredInitialization().any
}
Expand Down
Loading