Skip to content

Add Wrappers for Class, Actor, Extension #17

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

Merged
merged 8 commits into from
May 30, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions Sources/MacroToolkit/DeclGroup/Actor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftSyntax
/// Wraps an `actor` declaration.
public struct Actor: DeclGroupProtocol {
/// The underlying syntax node for the `actor` declaration.
public var rawValue: ActorDeclSyntax
public var _syntax: ActorDeclSyntax

/// The identifier (name) of the `actor`.
public var identifier: String {
Expand All @@ -14,6 +14,6 @@ public struct Actor: DeclGroupProtocol {
///
/// - Parameter syntax: The syntax node representing the `actor` declaration.
public init(_ syntax: ActorDeclSyntax) {
rawValue = syntax
_syntax = syntax
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import SwiftSyntax

/// An enumeration representing different types of declaration groups (e.g., `struct`, `class`, `enum`, `actor`, `extension`).
/// This enum conforms to `DeclGroupProtocol` and wraps specific declaration group types.
public enum DeclGroup: DeclGroupProtocol {
/// An enum that encapsulates various types of declaration groups (`struct`, `class`, `enum`, `actor`, `extension`)
/// and provides a unified interface for interacting with them. This enum conforms to `AnyDeclGroupProtocol`,
/// allowing access to common properties of declaration groups.
public enum AnyDeclGroup: AnyDeclGroupProtocol {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any particular reason for renaming this to AnyDeclGroup? I can see a few upsides and would be happy to discuss, but best to apply this convention to the other root-level enums as well if we decide it's a good idea

Edit: I've now gone through the rest of the changes and have seen that it was so that DeclGroupProtocol can have a hard requirement on the type of its wrapped syntax, I've left a more open question/remark at the where clause. Not sure what the best design would be, and would love to hear your thoughts since you've been trying out a few different approaches and likely have a bit more insight into the problem than I do.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of renaming to AnyDeclGroup was to align with Swift's conventions for type-erased types, like AnyView, AnyHashable, and AnyCancellable, making the distinction between concrete types and their type-erased counterparts clear and intuitive. This consistency with the standard library and clarity in differentiation helps in understanding the purpose of AnyDeclGroup. The aim was to allow DeclGroupProtocol to have a hard requirement on the type of its wrapped syntax while maintaining flexibility, and type erasure with AnyDeclGroup achieves this by holding any type conforming to DeclGroupProtocol. If we adopt AnyDeclGroup, similar updates should be applied to other root-level enums for consistency, but this is beyond the scope of the current PR and should be handled separately. For now, I've reverted to DeclGroup to match the existing naming convention, and if we decide to use the Any prefix, we should plan a broader refactor for consistency. I appreciate your feedback and am open to further discussion to find the best design approach.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can see why the Any prefix made sense cause of DeclGroupSyntax being a protocol for whatever weird reason. The reason that I don't have the Any prefix on any of the others (e.g. Decl) is because they're still concrete and not erasing any info (since they're enums), so I guess in this case it depends what's more important to devs; the fact that it's erasing the _syntax escape hatch, or the fact that it's not erasing the underlying decl group information.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In-fact, DeclGroup can still conform to RepresentableBySyntax, but just with the underlying syntax type being Syntax instead (the main use would just be for using the syntax node in diagnostics, since all other useful decl group syntax info would unfortunately be erased)

case `struct`(Struct)
case `enum`(Enum)
case `class`(Class)
case actor(Actor)
case `actor`(Actor)
case `extension`(Extension)

/// A private computed property that returns the wrapped `DeclGroupProtocol` instance.
///
/// This property is used internally to access the underlying implementation of the declaration group.
private var wrapped: any DeclGroupProtocol {
switch self {
case .struct(let wrapped): return wrapped
Expand All @@ -20,12 +23,12 @@ public enum DeclGroup: DeclGroupProtocol {
}
}

/// Initializes a `DeclGroup` instance from a `DeclGroupSyntax`.
/// Initializes an `AnyDeclGroup` instance from a `DeclGroupSyntax`.
///
/// - Parameter rawValue: The syntax node representing the declaration group.
/// - Parameter syntax: The syntax node representing the declaration group.
/// - Note: This initializer will fatalError if the syntax node does not match any known declaration group type.
public init(_ rawValue: any DeclGroupSyntax) {
switch rawValue {
public init(_ syntax: DeclGroupSyntax) {
switch syntax {
case let syntax as ActorDeclSyntax:
self = .actor(Actor(syntax))
case let syntax as ClassDeclSyntax:
Expand All @@ -37,20 +40,12 @@ public enum DeclGroup: DeclGroupProtocol {
case let syntax as StructDeclSyntax:
self = .struct(Struct(syntax))
default:
fatalError("Unhandled decl group type '\(type(of: rawValue))'")
fatalError("Unhandled decl group type '\(type(of: syntax))'")
}
}

/// The underlying syntax node for the declaration group.
public var rawValue: any DeclGroupSyntax {
switch self {
case .struct(let wrapped): return wrapped.rawValue
case .enum(let wrapped): return wrapped.rawValue
case .class(let wrapped): return wrapped.rawValue
case .actor(let wrapped): return wrapped.rawValue
case .extension(let wrapped): return wrapped.rawValue
}
}
public var _syntax: DeclGroupSyntax { wrapped._syntax }

/// The identifier of the declaration group.
public var identifier: String { wrapped.identifier }
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacroToolkit/DeclGroup/Class.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftSyntax
/// Wraps a `class` declaration.
public struct Class: DeclGroupProtocol {
/// The underlying syntax node for the `class` declaration.
public var rawValue: ClassDeclSyntax
public var _syntax: ClassDeclSyntax

/// The identifier (name) of the `class`.
public var identifier: String {
Expand All @@ -14,6 +14,6 @@ public struct Class: DeclGroupProtocol {
///
/// - Parameter syntax: The syntax node representing the `class` declaration.
public init(_ syntax: ClassDeclSyntax) {
rawValue = syntax
_syntax = syntax
}
}
63 changes: 27 additions & 36 deletions Sources/MacroToolkit/DeclGroup/DeclGroupProtocol.swift
Original file line number Diff line number Diff line change
@@ -1,57 +1,48 @@
import SwiftSyntax

/// A protocol for declaration groups (e.g., `struct`, `class`, `enum`) that can contain members and properties.
/// Declaration groups are higher-level constructs compared to regular declarations such as `var`.
public protocol DeclGroupProtocol: RawRepresentable {
/// The underlying syntax node for the declaration group.
var rawValue: RawValue { get }

/// The declaration's identifier.
///
/// Note: SwiftSyntax's `DeclGroupSyntax` protocol does not include the declaration's identifier.
/// This must be implemented manually for each declaration wrapper. This omission might be due to
/// the fact that extensions technically do not have a name, even though they are always attached to a specific identifier.
/// A protocol that represents a declaration group, such as a `struct`, `class`, `enum`, or `protocol`.
/// This protocol defines common properties that all declaration groups should have.
///
/// Conforming types should provide the following properties:
/// - `identifier`: The identifier of the declaration group.
/// - `members`: The members of the declaration group.
/// - `properties`: The properties declared within the declaration group.
/// - `inheritedTypes`: The types that the declaration group inherits from or conforms to.
public protocol AnyDeclGroupProtocol {
/// The identifier of the declaration group.
var identifier: String { get }

/// The members of the declaration group.
/// This array contains all the members declared within the declaration group.
var members: [Decl] { get }

/// The properties declared in the declaration group.
/// This array contains all the properties declared within the declaration group.
var properties: [Property] { get }

/// The inherited types of the declaration group.
/// This array contains all the types that the declaration group inherits from or conforms to.
var inheritedTypes: [Type] { get }

/// Initializes the declaration group with the given syntax node.
///
/// - Parameter syntax: The underlying syntax node representing the declaration group.
init(_ syntax: RawValue)
}

/// Default implementations and helper initializers for `DeclGroupProtocol`.
extension DeclGroupProtocol {
/// The underlying syntax node for the declaration group.
public var _syntax: RawValue {
rawValue
}
/// A protocol that represents a declaration group with an underlying syntax node.
/// This protocol extends `AnyDeclGroupProtocol` and `RepresentableBySyntax` to provide additional
/// functionality specific to declaration groups in Swift syntax.
///
/// Conforming types must:
/// - Conform to `AnyDeclGroupProtocol`, providing properties for `identifier`, `members`, `properties`, and `inheritedTypes`.
/// - Conform to `RepresentableBySyntax`, providing an underlying syntax node of type `DeclGroupSyntax`.
public protocol DeclGroupProtocol: AnyDeclGroupProtocol, RepresentableBySyntax
where UnderlyingSyntax: DeclGroupSyntax {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the requirement in this where clause gets removed then DeclGroupProtocol and AnyDeclGroupProtocol would be able to be merged back together (basically what you had before), definitely a tricky trade off though, I'm unsure what would be the best mix of type safety, ergonomics, and simplicity.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue lies with the RepresentableBySyntax protocol, which has an associated type UnderlyingSyntax. This requires any type conforming to it to have a concrete type for UnderlyingSyntax. Since DeclGroupSyntax is a protocol, there isn't a generic concrete struct that can represent any type conforming to DeclGroupSyntax. I updated to make types with an underlying syntax type explicitly conform to RepresentableBySyntax.

Another approach could be to use a type-erased syntax, similar to the following:

public struct AnySyntax: SyntaxProtocol {
    public static var structure: SyntaxNodeStructure {
        // Returning a default or dummy structure, indicating this shouldn't be used directly
        return SyntaxNodeStructure.choices([.node(Syntax.self)])
    }
    
    public var _syntaxNode: Syntax { _syntaxNodeClosure() }
    
    private let _syntaxNodeClosure: () -> Syntax
    
    public init<S: SyntaxProtocol>(_ syntax: S) {
        self._syntaxNodeClosure = { syntax._syntaxNode }
    }
}

But perhaps it wouldn’t make sense for something like DeclGroup to have RepresentableBySyntax conformance and the DeclGroupProtcol shouldn't require it.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, strange that DeclSyntax is concrete and DeclGroupSyntax is a protocol, wonder why that is. Yeah something like that would work, or we could just set its associated syntax to be of type Syntax for now even though that's too broad.


/// Initializes the declaration group with the given raw value.
///
/// - Parameter rawValue: The raw value representing the declaration group.
public init?(rawValue: RawValue) {
self.init(rawValue)
}
}

/// Additional functionality for `DeclGroupProtocol` where the raw value conforms to `DeclGroupSyntax`.
extension DeclGroupProtocol where RawValue: DeclGroupSyntax {
extension DeclGroupProtocol {
/// Attempts to initialize the wrapper from an arbitrary declaration group.
///
/// - Parameter syntax: The syntax node representing the declaration group.
/// - Note: This initializer will return `nil` if the syntax node does not match the expected type.
public init?(_ syntax: any DeclGroupSyntax) {
guard let rawValue = syntax as? RawValue else { return nil }
self.init(rawValue: rawValue)
guard let syntax = syntax as? UnderlyingSyntax else { return nil }
self.init(syntax)
}

/// The members of the declaration group.
Expand Down Expand Up @@ -89,11 +80,11 @@ extension DeclGroupProtocol where RawValue: DeclGroupSyntax {

/// The access level of the declaration group.
public var accessLevel: AccessModifier? {
AccessModifier(modifiers: _syntax.modifiers)
AccessModifier(firstModifierOfKindIn: _syntax.modifiers)
}

/// The context-specific modifiers of the declaration group.
public var declarationContext: DeclarationContextModifier? {
DeclarationContextModifier(modifiers: _syntax.modifiers)
DeclarationContextModifier(firstModifierOfKindIn: _syntax.modifiers)
}
}
5 changes: 3 additions & 2 deletions Sources/MacroToolkit/DeclGroup/Enum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import SwiftSyntax

/// Wraps an `enum` declaration.
public struct Enum: DeclGroupProtocol {

/// The underlying syntax node for the `enum` declaration.
public var rawValue: EnumDeclSyntax
public var _syntax: EnumDeclSyntax

/// The identifier (name) of the `enum`.
public var identifier: String {
Expand All @@ -14,7 +15,7 @@ public struct Enum: DeclGroupProtocol {
///
/// - Parameter syntax: The syntax node representing the `enum` declaration.
public init(_ syntax: EnumDeclSyntax) {
rawValue = syntax
_syntax = syntax
}

/// The `enum`'s cases.
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacroToolkit/DeclGroup/Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftSyntax
/// Wraps an `extension` declaration.
public struct Extension: DeclGroupProtocol {
/// The underlying syntax node for the `extension` declaration.
public var rawValue: ExtensionDeclSyntax
public var _syntax: ExtensionDeclSyntax

/// The identifier (extended type) of the `extension`.
public var identifier: String {
Expand All @@ -14,6 +14,6 @@ public struct Extension: DeclGroupProtocol {
///
/// - Parameter syntax: The syntax node representing the `extension` declaration.
public init(_ syntax: ExtensionDeclSyntax) {
rawValue = syntax
_syntax = syntax
}
}
4 changes: 2 additions & 2 deletions Sources/MacroToolkit/DeclGroup/Struct.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import SwiftSyntax
/// Wraps a `struct` declaration.
public struct Struct: DeclGroupProtocol {
/// The underlying syntax node for the `struct` declaration.
public var rawValue: StructDeclSyntax
public var _syntax: StructDeclSyntax

/// The identifier (name) of the `struct`.
public var identifier: String {
Expand All @@ -14,6 +14,6 @@ public struct Struct: DeclGroupProtocol {
///
/// - Parameter syntax: The syntax node representing the `struct` declaration.
public init(_ syntax: StructDeclSyntax) {
rawValue = syntax
_syntax = syntax
}
}
2 changes: 1 addition & 1 deletion Sources/MacroToolkit/Modifiers/AccessLevel.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftSyntax

/// Represents access control levels in Swift (e.g., private, public).
public enum AccessModifier: RawRepresentable, Modifier, Comparable {
public enum AccessModifier: RawRepresentable, ModifierProtocol, Comparable {
case `private`
case `fileprivate`
case `internal`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import SwiftSyntax

/// Represents context-specific modifiers for declarations (e.g., static, class).
public enum DeclarationContextModifier: RawRepresentable, Modifier {
public enum DeclarationContextModifier: RawRepresentable, ModifierProtocol {
case `static`
case `class`

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import SwiftSyntax

/// A protocol for modifiers in Swift that are represented by `TokenKind`.
public protocol Modifier: RawRepresentable where RawValue == TokenKind {
public protocol ModifierProtocol: RawRepresentable where RawValue == TokenKind {
/// Initializes a `Modifier` from a list of declaration modifiers.
///
/// - Parameter modifiers: A list of declaration modifiers.
init?(modifiers: DeclModifierListSyntax)
init?(firstModifierOfKindIn: DeclModifierListSyntax)
}

extension Modifier {
extension ModifierProtocol {
/// Default implementation for initializing a `Modifier` from a list of declaration modifiers.
///
/// - Parameter modifiers: A list of declaration modifiers.
public init?(modifiers: DeclModifierListSyntax) {
for element in modifiers {
public init?(firstModifierOfKindIn: DeclModifierListSyntax) {
for element in firstModifierOfKindIn {
guard let modifier = Self(rawValue: element.name.tokenKind) else { continue }
self = modifier
return
Expand Down
19 changes: 19 additions & 0 deletions Sources/MacroToolkit/RepresentableBySyntax.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import SwiftSyntax

/// A protocol that provides a consistent interface for types that are represented by an underlying syntax node.
/// This protocol is useful for working with various SwiftSyntax types in a unified manner.
///
/// Types conforming to this protocol must define an associated `UnderlyingSyntax` type that conforms to `SyntaxProtocol`.
/// They must also provide a `_syntax` property to access the underlying syntax node and an initializer to create an instance from the syntax node.
public protocol RepresentableBySyntax {
/// The type of the underlying syntax node that this type represents.
associatedtype UnderlyingSyntax: SyntaxProtocol

/// The underlying syntax node for this type.
var _syntax: UnderlyingSyntax { get set }

/// Initializes an instance with the given underlying syntax node.
///
/// - Parameter syntax: The underlying syntax node to represent.
init(_ syntax: UnderlyingSyntax)
}
6 changes: 3 additions & 3 deletions Sources/MacroToolkitExamplePlugin/CaseDetectionMacro.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import MacroToolkit
import SwiftSyntax
import SwiftSyntaxMacros
import MacroToolkit

// Modified from: https://github.com/DougGregor/swift-macro-examples/blob/f61ac7cdca8dc3557e53f86e7e03df1353908d3e/MacroExamplesPlugin/CaseDetectionMacro.swift
public struct CaseDetectionMacro: MemberMacro {
Expand All @@ -13,7 +13,7 @@ public struct CaseDetectionMacro: MemberMacro {
) throws -> [DeclSyntax] {
guard let enum_ = Enum(declaration) else {
throw MacroError("@CaseDetectionMacro can only be attached to enum declarations")
}
}

return enum_.cases
.map { ($0.identifier, $0.identifier.initialUppercased) }
Expand All @@ -29,4 +29,4 @@ public struct CaseDetectionMacro: MemberMacro {
"""
}
}
}
}
2 changes: 1 addition & 1 deletion Sources/MacroToolkitExamplePlugin/CustomCodableMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public struct CustomCodableMacro: MemberMacro {
providingMembersOf declaration: some DeclGroupSyntax,
in context: some MacroExpansionContext
) throws -> [DeclSyntax] {
let decl = DeclGroup(declaration)
let decl = AnyDeclGroup(declaration)

let cases = decl.members.compactMap(\.asVariable).compactMap { (variable) -> String? in
guard let propertyName = destructureSingle(variable.identifiers) else {
Expand Down
Loading