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

Extend the Default attribute to support multiple bindings declarations with parameter packs. #75

Open
wants to merge 1 commit 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
30 changes: 30 additions & 0 deletions Sources/MetaCodable/Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,33 @@
@available(swift 5.9)
public macro Default<T>(_ default: T) =
#externalMacro(module: "MacroPlugin", type: "Default")

/// Provides a `default` 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
/// @Default("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, `10` will be used as `field2`'s value.
///
/// - Parameter defaults: The default values 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 Default<each T>(_ defaults: repeat each T) =
#externalMacro(module: "MacroPlugin", type: "Default")
136 changes: 133 additions & 3 deletions Sources/PluginCore/Attributes/Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ package struct Default: PropertyAttribute {

/// The default value expression provided.
var expr: ExprSyntax {
return node.arguments!
.as(LabeledExprListSyntax.self)!.first!.expression
exprs.first!
}

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

/// Creates a new instance with the provided node.
Expand Down Expand Up @@ -47,7 +53,7 @@ package struct Default: PropertyAttribute {
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
expect(syntaxes: VariableDeclSyntax.self)
DefaultAttributeDeclaration<Self>(self)
attachedToNonStaticVariable()
cantDuplicate()
cantBeCombined(with: IgnoreCoding.self)
Expand Down Expand Up @@ -77,6 +83,38 @@ where
}
}

extension Registration
where
Decl == PropertyDeclSyntax, Var: PropertyVariable,
Var.Initialization == RequiredInitialization
{
/// Update registration with default value 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.
func addDefaultValueIfExists() -> Registration<Decl, Key, AnyPropertyVariable<AnyRequiredVariableInitialization>> {
guard let attr = Default(from: self.decl)
else { return self.updating(with: self.variable.any) }

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

if i < attr.exprs.count {
let newVar = self.variable.with(default: attr.exprs[i])
return self.updating(with: newVar.any)
}

return self.updating(with: self.variable.any)
}
}

fileprivate extension PropertyVariable
where Initialization == RequiredInitialization {
/// Update variable data with the default value expression provided.
Expand All @@ -90,3 +128,95 @@ where Initialization == RequiredInitialization {
return .init(base: self, options: .init(expr: expr))
}
}

@_implementationOnly import SwiftDiagnostics
@_implementationOnly import SwiftSyntaxMacros
/// A diagnostic producer type that can validate the ``Default`` attribut's number of parameters.
///
/// - Note: This producer also validates passed syntax is of variable
/// declaration type. No need to pass additional diagnostic producer
/// to validate this.
fileprivate struct DefaultAttributeDeclaration<Attr: PropertyAttribute>: DiagnosticProducer {
/// The attribute for which
/// validation performed.
///
/// Uses this attribute name
/// in generated diagnostic
/// messages.
let attr: Attr

/// Underlying producer that validates passed syntax is variable
/// declaration.
///
/// This diagnostic producer is used first to check if passed declaration is
/// variable declaration. If validation failed, then further validation by
/// this type is terminated.
let base: InvalidDeclaration<Attr>

/// Creates a grouped variable declaration validation instance
/// with provided attribute.
///
/// Underlying variable declaration validation instance is created
/// and used first. Post the success of base validation this type
/// performs validation.
///
/// - Parameter attr: The attribute for which
/// validation performed.
/// - Returns: Newly created diagnostic producer.
init(_ attr: Attr) {
self.attr = attr
self.base = .init(attr, expect: [VariableDeclSyntax.self])
}

/// Validates and produces diagnostics for the passed syntax
/// in the macro expansion context provided.
///
/// Check whether the number of parameters of the application's ``Default`` attribute corresponds to the number of declared variables.
///
/// - Parameters:
/// - syntax: The syntax to validate and produce diagnostics for.
/// - context: The macro expansion context diagnostics produced in.
///
/// - Returns: True if syntax fails validation, false otherwise.
@discardableResult
func produce(
for syntax: some SyntaxProtocol,
in context: some MacroExpansionContext
) -> Bool {
guard !base.produce(for: syntax, in: context) else { return true }
let decl = syntax.as(VariableDeclSyntax.self)!
let bindingsCount = decl.bindings.count

let attributeArgumentsCount = self.attr.node.arguments?.as(LabeledExprListSyntax.self)?.count ?? 0

guard bindingsCount != attributeArgumentsCount
else { return false }

var msg: String
if bindingsCount - attributeArgumentsCount < 0 {
msg = "@\(attr.name) expect \(bindingsCount) default \(bindingsCount > 1 ? "values" : "value") but found \(attributeArgumentsCount) !"
} else if bindingsCount - attributeArgumentsCount == 1 {
msg = "@\(attr.name) missing default value for variable "
} else {
msg = "@\(attr.name) missing default values for variables "
}

for (i, binding) in decl.bindings.enumerated() where binding.pattern.is(IdentifierPatternSyntax.self) {
if i >= attributeArgumentsCount {
msg += "'\(binding.pattern.trimmed.description)'"
if i < decl.bindings.count - 1 {
msg += ", "
}
}
}

let message = attr.diagnostic(
message:
msg,
id: attr.misuseMessageID,
severity: .error
)
attr.diagnose(message: message, in: context)
return true
}
}
76 changes: 76 additions & 0 deletions Tests/MetaCodableTests/Attributes/DefaultTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,81 @@ final class DefaultTests: XCTestCase {
]
)
}

func testMissingDefaultValue() throws {
assertMacroExpansion(
"""
struct SomeCodable {
@Default()
let one: String
}
""",
expandedSource:
"""
struct SomeCodable {
let one: String
}
""",
diagnostics: [
.init(
id: Default.misuseID,
message:
"@Default missing default value for variable 'one'",
line: 2, column: 5,
fixIts: [
.init(message: "Remove @Default attribute")
]
),
]
)
}

func testMissingDefaultValues() throws {
assertMacroExpansion(
"""
struct SomeCodable {
@Default("hello")
let one: String, two: Int
}
""",
expandedSource:
"""
struct SomeCodable {
let one: String, two: Int
}
""",
diagnostics: [
.multiBinding(line: 2, column: 5)
]
)
}

func testTooManyDefaultValueParameters() throws {
assertMacroExpansion(
"""
struct SomeCodable {
@Default("hello", 10)
let one: String
}
""",
expandedSource:
"""
struct SomeCodable {
let one: String
}
""",
diagnostics: [
.init(
id: Default.misuseID,
message:
"@Default expect 1 default value but found 2 !",
line: 2, column: 5,
fixIts: [
.init(message: "Remove @Default attribute")
]
),
]
)
}
}
#endif