Skip to content

Commit

Permalink
feat: added initialized variable ignore option (#17)
Browse files Browse the repository at this point in the history
* feat: added initialized variable ignore option

* refactor: use separate macro attribute instead of adding options
  • Loading branch information
soumyamahunt committed Sep 20, 2023
1 parent 94855a0 commit 6cd519e
Show file tree
Hide file tree
Showing 52 changed files with 1,306 additions and 575 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Supercharge `Swift`'s `Codable` implementations with macros.
- Allows to provide default value in case of decoding failures with ``Default(_:)``.
- Generates member-wise initializer(s) considering the above default value syntax as well.
- Allows to create custom decoding/encoding strategies with ``HelperCoder`` and using them with ``CodedBy(_:)``. i.e. ``LossySequenceCoder`` etc.
- Allows to ignore specific properties from decoding/encoding with ``IgnoreCoding()``, ``IgnoreDecoding()`` and ``@IgnoreEncoding()``.
- Allows to ignore all initialized properties of a type from decoding/encoding with ``IgnoreCodingInitialized()`` unless explicitly asked to decode/encode by attaching any coding attributes, i.e. ``CodedIn(_:)``, ``CodedAt(_:)``,
``CodedBy(_:)``, ``Default(_:)`` etc.

## Requirements

Expand Down
Original file line number Diff line number Diff line change
@@ -1,56 +1,7 @@
import Foundation
import SwiftSyntax
import SwiftDiagnostics
import SwiftSyntaxMacros

/// Attribute type for `Codable` macro-attribute.
///
/// Describes a macro that validates `Codable` macro usage
/// and generates `Codable` conformances and implementations.
///
/// This macro performs two different kinds of expansion:
/// * Conformance macro expansion, to confirm to `Decodable`
/// and `Encodable` protocols.
/// * Member macro expansion, to generate custom `CodingKey` type
/// for the attached struct declaration named `CodingKeys` and use
/// this type for `Codable` implementation of both `init(from:)`
/// and `encode(to:)` methods by using `CodedPropertyMacro`
/// declarations. Additionally member-wise initializer(s) also generated.
struct Codable: Attribute {
/// The node syntax provided
/// during initialization.
let node: AttributeSyntax
/// 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(SimpleTypeIdentifierSyntax.self)!
.description == Self.name
else { return nil }
self.node = node
}

/// Builds diagnoser that can validate this macro
/// attached declaration.
///
/// Builds diagnoser that validates attached declaration
/// is `struct` declaration and macro usage is not
/// duplicated for the same declaration.
///
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
expect(syntax: StructDeclSyntax.self)
cantDuplicate()
}
}
}

extension Codable: ConformanceMacro, MemberMacro {
/// Expand to produce `Codable` conformance.
///
Expand Down Expand Up @@ -94,8 +45,8 @@ extension Codable: ConformanceMacro, MemberMacro {
) throws -> [DeclSyntax] {
// validate proper use
guard
!Self(from: node)!.diagnoser()
.produce(for: declaration, in: context)
let self = Self(from: node),
!self.diagnoser().produce(for: declaration, in: context)
else { return [] }

let options = Registrar.Options(modifiers: declaration.modifiers)
Expand All @@ -106,23 +57,23 @@ extension Codable: ConformanceMacro, MemberMacro {
guard let decl = member.decl.as(VariableDeclSyntax.self)
else { return }

// builder
let builder = IgnoreCodingInitialized(from: declaration)
|> KeyPathRegistrationBuilder(
provider: CodedAt(from: decl)
?? CodedIn(from: decl)
?? CodedIn()
)
|> HelperCodingRegistrationBuilder()
|> DefaultCodingRegistrationBuilder()
|> InitializationRegistrationBuilder()
|> IgnoreCodingBuilder()

// build
let registrations = decl.registrations(node: node, in: context) {
ExhaustiveRegistrationBuilder(
optional: CodedAt(from: decl),
fallback: CodedIn(from: decl) ?? CodedIn()
)
OptionalRegistrationBuilder(base: CodedBy(from: decl))
OptionalRegistrationBuilder(base: Default(from: decl))
InitializationRegistrationBuilder<AnyVariable>()
ConditionalCodingBuilder<
InitializationVariable<AnyVariable<RequiredInitialization>>
>()
}
let regs = decl.registrations(for: self, in: context, with: builder)

// register
for registration in registrations
where registration.variable.canBeRegistered {
for registration in regs {
registrar.add(registration: registration, context: context)
}
}
Expand Down
50 changes: 50 additions & 0 deletions Sources/CodableMacroPlugin/Attributes/Codable/Codable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import SwiftSyntax

/// Attribute type for `Codable` macro-attribute.
///
/// Describes a macro that validates `Codable` macro usage
/// and generates `Codable` conformances and implementations.
///
/// This macro performs two different kinds of expansion:
/// * Conformance macro expansion, to confirm to `Decodable`
/// and `Encodable` protocols.
/// * Member macro expansion, to generate custom `CodingKey` type
/// for the attached struct declaration named `CodingKeys` and use
/// this type for `Codable` implementation of both `init(from:)`
/// and `encode(to:)` methods by using `CodedPropertyMacro`
/// declarations. Additionally member-wise initializer(s) also generated.
struct Codable: Attribute {
/// The node syntax provided
/// during initialization.
let node: AttributeSyntax

/// 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(SimpleTypeIdentifierSyntax.self)!
.description == Self.name
else { return nil }
self.node = node
}

/// Builds diagnoser that can validate this macro
/// attached declaration.
///
/// Builds diagnoser that validates attached declaration
/// is `struct` declaration and macro usage is not
/// duplicated for the same declaration.
///
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
expect(syntax: StructDeclSyntax.self)
cantDuplicate()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import SwiftSyntax

/// Attribute type for `IgnoreCodingInitialized` macro-attribute.
///
/// This type can validate`IgnoreCodingInitialized` macro-attribute
/// usage and extract data for `Codable` macro to generate implementation.
///
/// Attaching this macro to type declaration indicates all the initialized
/// properties for the said type will be ignored from decoding and
/// encoding unless explicitly asked with attached coding attributes,
/// i.e. `CodedIn`, `CodedAt` etc.
struct IgnoreCodingInitialized: PeerAttribute {
/// The node syntax provided
/// during initialization.
let node: AttributeSyntax

/// 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(SimpleTypeIdentifierSyntax.self)!
.description == Self.name
else { return nil }
self.node = node
}

/// Create a new instance from provided attached
/// grouped declaration.
///
/// This initialization will fail if this attribute not attached
/// to provided variable declaration
///
/// - Parameter decl: The attached grouped declaration.
/// - Returns: Newly created attribute instance.
init?(from decl: some DeclGroupSyntax) {
guard let decl = decl as? AttributableDeclSyntax else { return nil }
self.init(from: decl)
}

/// Builds diagnoser that can validate this macro
/// attached declaration.
///
/// Builds diagnoser that validates attached declaration
/// has `Codable` macro attached and macro usage
/// is not duplicated for the same declaration.
///
/// - Returns: The built diagnoser instance.
func diagnoser() -> DiagnosticProducer {
return AggregatedDiagnosticProducer {
mustBeCombined(with: Codable.self)
shouldNotDuplicate()
}
}
}
7 changes: 7 additions & 0 deletions Sources/CodableMacroPlugin/Attributes/CodedBy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ struct CodedBy: PropertyAttribute {
/// during initialization.
let node: AttributeSyntax

/// The helper coding instance
/// expression provided.
var expr: ExprSyntax {
return node.argument!
.as(TupleExprElementListSyntax.self)!.first!.expression
}

/// Creates a new instance with the provided node.
///
/// The initializer fails to create new instance if the name
Expand Down
6 changes: 6 additions & 0 deletions Sources/CodableMacroPlugin/Attributes/Default.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ struct Default: PropertyAttribute {
/// during initialization.
let node: AttributeSyntax

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

/// Creates a new instance with the provided node.
///
/// The initializer fails to create new instance if the name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import SwiftSyntax

/// A special `PropertyAttribute` that updates
/// `CodingKey` path data.
///
/// Attaching attribute of this type to variables indicates
/// explicit decoding/encoding of those variables.
protocol KeyPathProvider: PropertyAttribute {
/// Indicates whether `CodingKey` path
/// data is provided to this instance.
///
/// If data is provided explicitly by attaching
/// the macro then `true`, otherwise `false`
/// This can be used to indicate variable is
/// explicitly asked to decode/encode.
var provided: Bool { get }

/// Updates `CodingKey` path using the provided path.
///
/// The `CodingKey` path may override current data
/// or just update current `CodingKey` path.
///
/// - Parameter path: Current `CodingKey` path.
/// - Returns: Updated `CodingKey` path.
func keyPath(withExisting path: [String]) -> [String]
}

extension KeyPathProvider {
/// Returns `CodingKey` path
/// provided in this attribute.
///
/// The path components are provided
/// as variadic arguments without any labels.
///
/// - Important: The path components must be string literals
/// with single segment (i.e no interpolation,
/// no string combinations).
var providedPath: [String] {
guard let exprs = node.argument?.as(TupleExprElementListSyntax.self)
else { return [] }

let path: [String] = exprs.compactMap { expr in
guard expr.label == nil else { return nil }
return expr.expression.as(StringLiteralExprSyntax.self)?
.segments.first?.as(StringSegmentSyntax.self)?
.content.text
}
return path
}
}

extension CodedAt: KeyPathProvider {
/// Indicates whether `CodingKey` path
/// data is provided to this instance.
///
/// Always `true` for this type.
var provided: Bool { true }

/// Updates `CodingKey` path using the provided path.
///
/// The `CodingKey` path overrides current `CodingKey` path data.
///
/// - Parameter path: Current `CodingKey` path.
/// - Returns: Updated `CodingKey` path.
func keyPath(withExisting path: [String]) -> [String] { providedPath }
}

extension CodedIn: KeyPathProvider {
/// Indicates whether `CodingKey` path
/// data is provided to this instance.
///
/// If attribute is initialized with syntax node,
/// then `true`, otherwise `false`.
var provided: Bool { !inDefaultMode }

/// Updates `CodingKey` path using the provided path.
///
/// The `CodingKey` path is updated by prepending
/// provided path to current `CodingKey` path.
///
/// - Parameter path: Current `CodingKey` path.
/// - Returns: Updated `CodingKey` path.
func keyPath(withExisting path: [String]) -> [String] {
var finalPath = providedPath
finalPath.append(contentsOf: path)
return finalPath
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,29 @@ import SwiftSyntax
import SwiftDiagnostics
import SwiftSyntaxMacros

/// An `Attribute` type that generates "peer" declarations that
/// sit alongside the attached declaration.
///
/// 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 `diagnoser().produce(syntax:in:)`
/// implementation. If verification fails, then this macro generates
/// diagnostic to remove it.
protocol PeerAttribute: Attribute, PeerMacro { }

/// An `Attribute` type that can be applied to property declaration.
///
/// This macro doesn't perform any expansion rather `CodableMacro`
/// 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 `diagnoser().produce(syntax:in:)` implementation.
/// If verification fails, then this macro generates diagnostic to remove it.
protocol PropertyAttribute: Attribute, PeerMacro {}
protocol PropertyAttribute: PeerAttribute {}

extension PropertyAttribute {
extension PeerAttribute {
/// Create a new instance from provided attached
/// variable declaration.
///
Expand All @@ -21,8 +33,8 @@ extension PropertyAttribute {
///
/// - Parameter declaration: The attached variable
/// declaration.
/// - Returns: Created registration builder attribute.
init?(from declaration: VariableDeclSyntax) {
/// - Returns: Newly created attribute instance.
init?(from declaration: some AttributableDeclSyntax) {
let attribute = declaration.attributes?.first { attribute in
guard case .attribute(let attribute) = attribute
else { return false }
Expand All @@ -32,10 +44,10 @@ extension PropertyAttribute {
self.init(from: attribute)
}

/// Provide metadata to `CodableMacro` for final expansion
/// Provide metadata to `Codable` macro for final expansion
/// and verify proper usage of this macro.
///
/// This macro doesn't perform any expansion rather `CodableMacro`
/// 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
Expand Down

0 comments on commit 6cd519e

Please sign in to comment.