Skip to content

Commit

Permalink
feat: added class support
Browse files Browse the repository at this point in the history
  • Loading branch information
soumyamahunt committed Jan 9, 2024
1 parent 495cea4 commit 4bfeac3
Show file tree
Hide file tree
Showing 90 changed files with 3,654 additions and 2,288 deletions.
2 changes: 2 additions & 0 deletions .github/config/spellcheck-wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ HelperCoder
HelperCoders
JSON
LossySequenceCoder
Codable
MetaCodable
Midbin
README
Expand Down Expand Up @@ -48,3 +49,4 @@ variadic
vscode
watchOS
www
typealias
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ You can even create your own by conforming to `HelperCoder`.
</details>

See the full documentation for [`MetaCodable`](https://swiftpackageindex.com/SwiftyLab/MetaCodable/main/documentation/metacodable) and [`HelperCoders`](https://swiftpackageindex.com/SwiftyLab/MetaCodable/main/documentation/helpercoders), for API details and advanced use cases.
Also, [see the limitations](Sources/MetaCodable/MetaCodable.docc/Limitations.md).

## Contributing

Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,10 @@
@_implementationOnly import SwiftSyntax

/// An extension that manages fetching attributes
/// attached to declarations.
extension SyntaxProtocol {
/// Provides all the attributes attached to this declaration of
/// the provided type.
///
/// All the attribute syntaxes are checked and those matching
/// the provided type are returned.
///
/// - Parameter type: The macro-attribute type to search.
/// - Returns: All the attributes of provided type.
func attributes<A: Attribute>(for type: A.Type) -> [A] {
guard
case .choices(let choices) = DeclSyntax.structure
else { return [] }

let declSyntaxChoice = choices.first { choice in
if case .node(let type) = choice {
return type is AttributableDeclSyntax.Type
&& self.is(type)
} else {
return false
}
}

guard
let declSyntaxChoice,
case .node(let declSyntaxType) = declSyntaxChoice,
let declaration = self.as(declSyntaxType),
let declaration = declaration as? AttributableDeclSyntax
else { return [] }

return declaration.attributes.compactMap { attribute in
guard case .attribute(let attribute) = attribute else { return nil }
return type.init(from: attribute)
}
}
}

/// A declaration syntax type that supports macro-attribute.
///
/// This type can check whether an `AttributeSyntax`
/// is for this attribute and perform validation of this attribute usage.
protocol AttributableDeclSyntax: DeclSyntaxProtocol {
protocol AttributableDeclSyntax {
/// The list of attributes attached to this declaration.
var attributes: AttributeListSyntax { get }
}
Expand Down
73 changes: 70 additions & 3 deletions Sources/CodableMacroPlugin/Attributes/Attribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,58 @@ extension Attribute {
///
/// This attribute can must be removed or its usage condition
/// must be satisfied.
var misuseMessageID: MessageID { .messageID("\(id)-misuse") }
var misuseMessageID: MessageID { messageID("\(id)-misuse") }
/// Message id for unnecessary usage of this attribute.
///
/// This attribute can be omitted in such scenario and the
/// final result will still be the same.
var unusedMessageID: MessageID { .messageID("\(id)-unused") }
var unusedMessageID: MessageID { messageID("\(id)-unused") }

/// Creates a new message id in current package domain.
///
/// - Parameters id: The message id.
/// - Returns: Created message id.
func messageID(_ id: String) -> MessageID {
return .init(
domain: "com.SwiftyLab.MetaCodable",
id: id
)
}

/// Provides all the attributes of current type attached to
/// the provided declaration.
///
/// All the attribute syntaxes are checked and those matching
/// the current type are returned.
///
/// - Parameter syntax: The declaration to search in.
/// - Returns: All the attributes of current type.
static func attributes(attachedTo syntax: some SyntaxProtocol) -> [Self] {
guard
case .choices(let choices) = DeclSyntax.structure
else { return [] }

let declSyntaxChoice = choices.first { choice in
if case .node(let type) = choice {
return type is AttributableDeclSyntax.Type
&& syntax.is(type)
} else {
return false
}
}

guard
let declSyntaxChoice,
case .node(let declSyntaxType) = declSyntaxChoice,
let declaration = syntax.as(declSyntaxType),
let declaration = declaration as? AttributableDeclSyntax
else { return [] }

return declaration.attributes.compactMap { attribute in
guard case let .attribute(attribute) = attribute else { return nil }
return Self.init(from: attribute)
}
}

/// Checks whether this attribute is applied more than once to
/// provided declaration.
Expand All @@ -65,6 +111,27 @@ extension Attribute {
/// is attached to.
/// - Returns: Whether this attribute is applied more than once.
func isDuplicated(in declaration: some SyntaxProtocol) -> Bool {
return declaration.attributes(for: Self.self).count > 1
return Self.attributes(attachedTo: declaration).count > 1
}

/// Creates a new diagnostic message instance at current attribute node
/// with provided message, id and severity.
///
/// - Parameters:
/// - message: The message to be shown.
/// - messageID: The id associated with diagnostic.
/// - severity: The severity of diagnostic.
///
/// - Returns: The newly created diagnostic message instance.
func diagnostic(
message: String, id: MessageID,
severity: DiagnosticSeverity
) -> MetaCodableMessage {
return .init(
macro: self.node,
message: message,
messageID: id,
severity: severity
)
}
}
140 changes: 102 additions & 38 deletions Sources/CodableMacroPlugin/Attributes/Codable/Codable+Expansion.swift
Original file line number Diff line number Diff line change
@@ -1,20 +1,95 @@
@_implementationOnly import Foundation
@_implementationOnly import SwiftSyntax
@_implementationOnly import SwiftSyntaxMacros

extension Codable: ExtensionMacro {
extension Codable: MemberMacro, ExtensionMacro {
/// Expand to produce extensions with `Codable` implementation
/// members for attached struct.
/// members for attached `class`.
///
/// Conformance for both `Decodable` and `Encodable` is generated regardless
/// of whether class already conforms to any. Class or its super class
/// shouldn't conform to `Decodable` or `Encodable`
///
/// The `AttributeExpander` instance provides declarations based on
/// whether declaration is supported.
///
/// - Parameters:
/// - node: The custom attribute describing this attached macro.
/// - declaration: The declaration this macro attribute is attached to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Declarations of `CodingKeys` type, `Decodable`
/// conformance with `init(from:)` implementation and `Encodable`
/// conformance with `encode(to:)` implementation depending on already
/// declared conformances of type.
///
/// - Note: For types other than `class` types no declarations generated.
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let defaultProtocols: [TypeSyntax] = [
.init(stringLiteral: TypeCodingLocation.Method.decode.protocol),
.init(stringLiteral: TypeCodingLocation.Method.encode.protocol),
]
return try Self.expansion(
of: node, providingMembersOf: declaration,
conformingTo: defaultProtocols, in: context
)
}

/// Expand to produce extensions with `Codable` implementation
/// members for attached `class`.
///
/// Depending on whether attached type already conforms to `Decodable`
/// or `Encodable`, `Decodable` or `Encodable` conformance
/// implementation is skipped. Entire macro expansion is skipped if attached
/// type already conforms to both `Decodable` and`Encodable`.
///
/// The `AttributeExpander` instance provides declarations based on
/// whether declaration is supported.
///
/// - Parameters:
/// - node: The custom attribute describing this attached macro.
/// - declaration: The declaration this macro attribute is attached to.
/// - protocols: The list of protocols to add conformances to. These will
/// always be protocols that `type` does not already state a conformance
/// to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Declarations of `CodingKeys` type, `Decodable`
/// conformance with `init(from:)` implementation and `Encodable`
/// conformance with `encode(to:)` implementation depending on already
/// declared conformances of type.
///
/// - Note: For types other than `class` types no declarations generated.
static func expansion(
of node: AttributeSyntax,
providingMembersOf declaration: some DeclGroupSyntax,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
guard
let exp = AttributeExpander(for: declaration, in: context),
let decl = declaration.as(ClassDeclSyntax.self),
case let type = IdentifierTypeSyntax(name: decl.name)
else { return [] }
let exts = exp.codableExpansion(for: type, to: protocols, in: context)
return exts.flatMap { `extension` in
`extension`.memberBlock.members.map { DeclSyntax($0.decl) }
}
}

/// Expand to produce extensions with `Codable` implementation
/// members for attached `struct` or `class`.
///
/// Depending on whether attached type already conforms to `Decodable`
/// or `Encodable` extension for `Decodable` or `Encodable` conformance
/// implementation is skipped.Entire macro expansion is skipped if attached type
/// already conforms to both `Decodable` and`Encodable`.
/// implementation is skipped. Entire macro expansion is skipped if attached
/// type already conforms to both `Decodable` and`Encodable`.
///
/// For all the variable declarations in the attached type registration is
/// done via `Registrar` instance with optional `PeerAttribute`
/// metadata. The `Registrar` instance provides declarations based on
/// all the registrations.
/// The `AttributeExpander` instance provides declarations based on
/// whether declaration is supported.
///
/// - Parameters:
/// - node: The custom attribute describing this attached macro.
Expand All @@ -25,44 +100,33 @@ extension Codable: ExtensionMacro {
/// to.
/// - context: The context in which to perform the macro expansion.
///
/// - Returns: Extensions with `CodingKeys` type, `Decodable`
/// - Returns: Extensions with `CodingKeys` type, `Decodable`
/// conformance with `init(from:)` implementation and `Encodable`
/// conformance with `encode(to:)` implementation depending on already
/// declared conformances of type.
///
/// - Note: For `class` types only conformance is generated,
/// member expansion generates the actual implementation.
static func expansion(
of node: AttributeSyntax,
attachedTo declaration: some DeclGroupSyntax,
providingExtensionsOf type: some TypeSyntaxProtocol,
conformingTo protocols: [TypeSyntax],
in context: some MacroExpansionContext
) throws -> [ExtensionDeclSyntax] {
let registrar = registrar(for: declaration, node: node, in: context)
guard let registrar else { return [] }
return registrar.codableExpansion(for: type, to: protocols, in: context)
guard
let self = Self(from: node),
!self.diagnoser().produce(for: declaration, in: context),
let exp = AttributeExpander(for: declaration, in: context)
else { return [] }
var exts = exp.codableExpansion(for: type, to: protocols, in: context)
if declaration.is(ClassDeclSyntax.self) {
for (index, var `extension`) in exts.enumerated() {
`extension`.memberBlock = .init(members: [])
exts[index] = `extension`
}
exts.removeAll { $0.inheritanceClause == nil }
}
return exts
}
}

/// An extension that converts field token syntax
/// to equivalent key token.
extension TokenSyntax {
/// Convert field token syntax
/// to equivalent key token
/// string by trimming \`s`.
var asKey: String {
self.text.trimmingCharacters(in: .swiftVariableExtra)
}

/// Convert field token syntax
/// to equivalent key token
/// by trimming \`s`.
var raw: TokenSyntax { .identifier(self.asKey) }
}

/// An extension that manages
/// custom character sets
/// for macro expansion.
extension CharacterSet {
/// Character set that contains extra characters in swift variable names
/// not applicable for key construction.
static let swiftVariableExtra: Self = .init(arrayLiteral: "`")
}
8 changes: 4 additions & 4 deletions Sources/CodableMacroPlugin/Attributes/Codable/Codable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/// methods.
/// * If attached declaration already conforms to `Codable` this macro expansion
/// is skipped.
struct Codable: RegistrationAttribute {
struct Codable: Attribute {
/// The node syntax provided
/// during initialization.
let node: AttributeSyntax
Expand All @@ -40,13 +40,13 @@ struct Codable: RegistrationAttribute {
/// attached declaration.
///
/// Builds diagnoser that validates attached declaration
/// is `struct` declaration and macro usage is not
/// duplicated for the same declaration.
/// is `struct` or `class` 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)
expect(syntaxes: StructDeclSyntax.self, ClassDeclSyntax.self)
cantDuplicate()
}
}
Expand Down

0 comments on commit 4bfeac3

Please sign in to comment.